mmontes11/skills

kubeseal

Seal or reseal Kubernetes Secrets into SealedSecrets using kubeseal for secure GitOps storage.

查看源码
仓库原始内容

按源仓库内容呈现,保留标题、案例、代码、表格、链接以及原文引用的演示图片。

Kubeseal: Seal and Reseal Kubernetes Secrets

Overview

Seal Kubernetes Secrets into Bitnami SealedSecrets that are safe to store in Git. SealedSecrets are asymmetrically encrypted — anyone can encrypt (seal), but only the sealed-secrets controller in the cluster can decrypt (unseal).

Validated against kubeseal v0.38.4. Flag names are stable across recent releases, but run kubeseal --help if a command behaves unexpectedly.

Key insight: the public certificate (tls.crt) is not secret — it is a public key, safe to share and even commit. Only the controller's private key (tls.key) is sensitive. Sealing is a purely local, offline operation once you have the cert; no cluster access is needed to seal.

CRITICAL SECURITY RULES

NEVER LEAK CREDENTIALS

  1. NEVER print plaintext credentials to stdout, conversation output, or any readable medium.
  2. NEVER write plaintext secrets to files in the repository or any version-controlled location.
  3. NEVER echo or log secret values — use variables and pipe directly.
  4. NEVER include plaintext secrets in commit messages, PR descriptions, or comments.
  5. ALWAYS overwrite or zero-out temp files containing plaintext secrets immediately after use.
  6. NEVER leak seal key material — the private key (tls.key) must never be printed, committed, or shared. Only the public key (tls.crt) is needed for sealing.

Temp File Handling

Prefer approaches that never write plaintext to a named file on disk — the stdin heredoc under "1. New SealedSecret", or "Encrypt a Single Value (--raw)", both below. When a temp file is unavoidable:

  • Write temp secret files to /tmp/ with descriptive names.
  • After sealing, overwrite the temp file contents with empty or garbage data.
  • Never rm temp files (may be blocked by permission rules) — overwrite instead:
bash
  echo "" > /tmp/secret-temp.yaml
  • Never commit temp files to the repository.
Note: the public cert (tls.crt / .pem) is not sensitive and does not need sanitizing — sealing it away only forces a re-fetch. Only overwrite files that contain plaintext secret values or the private key.

Prerequisites

  • kubeseal CLI installed and available
  • Public key certificate (tls.crt) — obtained only as described in "Obtaining the Certificate" below
  • Access to the Kubernetes cluster via the Kubernetes MCP (for the cert fallback, and for retrieving existing secret values)

Obtaining the Certificate

This section is authoritative. The certificate MUST come from one of exactly two sources, tried in this order. Do not improvise a third.

Source 1 (preferred) — the project's certs/ folder

Look for the cert in the repository first. Sealing is offline; if the repo ships the cert, no cluster access is needed at all.

bash
# From the project root; typical names: tls.crt, sealed-secrets-cert.pem, sealed-secrets.crt
ls certs/

# If certs/ is not at the root, locate it:
find . -type d -name certs -not -path '*/.git/*'

Set CERT to the file you found, then validate it before use (see "Always Validate the Certificate"):

bash
CERT=certs/tls.crt

If certs/ holds more than one candidate, validate each and prefer the one that verifies against the repo's existing SealedSecrets.

Source 2 (fallback) — the cluster, via the Kubernetes MCP

Only if certs/ has no valid cert. Use the Kubernetes MCP tools to read the controller's active key Secret:

Step 1. Call mcp__kubernetes__resources_list with:

  • apiVersion: v1, kind: Secret
  • labelSelector: sealedsecrets.bitnami.com/sealed-secrets-key=active
  • namespace: wherever the controller runs (commonly kube-system or sealed-secrets); omit to search all namespaces

If several active key Secrets come back, pick the one with the most recent metadata.creationTimestamp — that is the key the controller currently seals with.

Step 2. Take data["tls.crt"] from that Secret and base64-decode it into a local file:

bash
printf '%s' '<tls.crt base64 from the MCP response>' | base64 -d > /tmp/sealed-secrets-cert.pem
CERT=/tmp/sealed-secrets-cert.pem

Step 3. Validate it before use (next section).

If the MCP returns no such Secret, the controller is not installed on the cluster the MCP points at. Stop and tell the user — do not fall back to another method.

DANGER — that Secret also contains `tls.key`, the controller's private key. Read and use only the tls.crt field. Never decode, print, echo, write, or commit tls.key. Do not paste the raw MCP response anywhere.

Always Validate the Certificate

A file is not a certificate just because it exists. Redirects capture error text into cert-shaped files, and sealing against that garbage produces a SealedSecret that fails later with illegal base64 data at input byte N or no key could decrypt secret — long after the plaintext is gone.

Run this before every seal. If it fails, STOP — fall back to the next source rather than sealing:

bash
openssl x509 -in "$CERT" -noout -subject -dates || echo "NOT A VALID CERT — do not seal with this file"

If a certs/ file fails validation, report it to the user and move to Source 2. Never "fix" it by guessing.

Confirm It Is the Right Key (recommended)

A valid cert may still be the wrong or a rotated key. When the repo already contains working SealedSecrets, confirm the cert matches the one they were sealed with by comparing public keys against a known-good pair, or by checking that a newly sealed test value round-trips. If the repo also contains the private key, note that as a security problem (see Pitfalls) rather than relying on it.

Workflow

1. New SealedSecret

Preferred — seal via stdin (no plaintext file on disk):

The sealed output is safe to write directly into the repo. Only the input is sensitive, and piping it via a heredoc keeps it off the filesystem entirely.

bash
kubeseal --cert "$CERT" -o yaml -f /dev/stdin > path/to/repo/sealedsecret.yaml <<'EOF'
apiVersion: v1
kind: Secret
metadata:
  name: my-secret
  namespace: my-namespace
type: Opaque
stringData:
  key1: "value1"
  key2: "value2"
EOF

Alternative — temp file (when you must inspect/edit the plaintext first):

bash
# Write secret to temp file (NEVER to repo)
cat > /tmp/my-secret.yaml <<'EOF'
apiVersion: v1
kind: Secret
metadata:
  name: my-secret
  namespace: my-namespace
type: Opaque
stringData:
  key1: "value1"
  key2: "value2"
EOF

# Seal and output YAML (sealed output is safe to write straight to the repo)
kubeseal --cert "$CERT" --format yaml -f /tmp/my-secret.yaml > path/to/repo/sealedsecret.yaml

# SANITIZE the plaintext temp file immediately (the sealed output is not sensitive)
echo "" > /tmp/my-secret.yaml

2. Reseal / Update an Existing SealedSecret (Preserving Fields)

This is the path for a reseal request — the user asks to reseal an existing SealedSecret, usually because it fails to unseal (illegal base64 data at input byte N, no key could decrypt secret) or because a credential changed. Resealing rewrites spec.encryptedData in place; keep metadata, spec.template (labels, annotations, type), name, and namespace byte-identical to the original unless the user asks otherwise, and match the surrounding files' conventions.

When updating some keys in a SealedSecret (e.g., changing S3 credentials but keeping a database password), you have two options:

  • Update individual keys with --raw (paste into encryptedData) or --merge-into — you only need the values of the keys you're changing.
  • Re-seal the whole secret — you need the plaintext of ALL keys, retrieved from the cluster.

Step 1: Retrieve existing secret from the cluster

Prefer the mcp__kubernetes__resources_get MCP tool to read the decrypted Secret — the SealedSecret controller auto-decrypts into a regular Secret in the cluster (apiVersion: v1, kind: Secret, plus the name and namespace).

Or via kubectl:

bash
kubectl get secret <name> -n <namespace> -o jsonpath='{.data}'

Step 2: Decode and re-seal with updated values

DANGER — do not interpolate secret values into YAML. A value containing ", :, \n, leading spaces, or $ will break the quoting and either corrupt the secret or make kubeseal fail with error: no secrets found. This is common with generated passwords and S3 keys. Never build YAML like password: "$EXISTING_PASS".

Safe approach — seal each key individually with `--raw`, then merge. --raw reads the value from stdin as raw bytes (no YAML quoting involved), so special characters are handled correctly. Under the default strict scope you must pass --name and --namespace, and they must match the target SealedSecret.

bash
NS=my-namespace
NAME=my-secret

# Decode a value you want to PRESERVE (capture in variable, NEVER echo)
EXISTING_PASS=$(printf '%s' '<base64value>' | base64 -d)

# Re-seal the preserved value and the updated value straight into the repo file.
# printf '%s' avoids adding a trailing newline to the secret.
printf '%s' "$EXISTING_PASS" | kubeseal --cert "$CERT" \
  --raw --namespace "$NS" --name "$NAME" --from-file=/dev/stdin
# -> paste the output under spec.encryptedData.password in path/to/repo/sealedsecret.yaml

printf '%s' 'new-value' | kubeseal --cert "$CERT" \
  --raw --namespace "$NS" --name "$NAME" --from-file=/dev/stdin
# -> paste the output under spec.encryptedData.access-key-id

Or use --merge-into (see below) to update individual keys in place without touching the others.

If you must build a full Secret object (e.g. a fresh SealedSecret from scratch), avoid quoting pitfalls by base64-encoding values into the data: field instead of stringData::

bash
cat > /tmp/updated-secret.yaml <<EOF
apiVersion: v1
kind: Secret
metadata:
  name: $NAME
  namespace: $NS
type: Opaque
data:
  access-key-id: $(printf '%s' 'new-value' | base64 -w0)
  password: $(printf '%s' "$EXISTING_PASS" | base64 -w0)
EOF

kubeseal --cert "$CERT" --format yaml -f /tmp/updated-secret.yaml > path/to/repo/sealedsecret.yaml

# SANITIZE the plaintext temp file
echo "" > /tmp/updated-secret.yaml

3. Obtain the Public Key

See "Obtaining the Certificate" above — certs/ first, then the Kubernetes MCP. No other source is permitted, and the cert must be validated with openssl x509 before sealing.

kubeseal Command Reference

Common Flags

FlagPurpose
--cert <file>Public key file for encryption. Always pass this explicitly — omitting it makes kubeseal auto-detect the controller. Use a local file only, never a URL
`-o, --format yaml\json`Output format (default: json)
-f, --secret-file <file>Input Secret YAML file (use /dev/stdin to pipe)
-n, --namespace <ns>Namespace scope for the secret being sealed (not the controller's location)
`--scope strict\namespace-wide\cluster-wide`Scoping of the sealed secret (default: strict)
--merge-into <file>Merge sealed keys into existing SealedSecret file (in-place)
--rawEncrypt a single raw value from --from-file; requires --scope, plus --name+--namespace for strict scope
--from-file <file>(with --raw) Source the value from a file; use /dev/stdin for a pipe
--name <name>Name of the sealed secret (required with --raw under strict scope)
--re-encryptRe-encrypt an existing SealedSecret with the controller's latest key (needs cluster)
--validateVerify the sealed secret decrypts — contacts the controller; requires cluster access
--fetch-certFORBIDDEN — do not use. Get the cert from certs/ or the Kubernetes MCP instead (see "Obtaining the Certificate"). On failure it emits an error message to stdout that a redirect turns into a bogus cert file
--controller-namespace <ns>Namespace where the controller runs (default: kube-system). Applies to --validate/--re-encrypt only — never for fetching the cert
--controller-name <name>Controller name (default: sealed-secrets-controller). Same restriction as above
--recovery-unsealDisaster-recovery decrypt using --recovery-private-key (handles the private key — use with extreme caution)

Scope Modes

  • `strict` (default): Can only be unsealed with the exact namespace and name.
  • `namespace-wide`: Can be unsealed in the specified namespace with any name.
  • `cluster-wide`: Can be unsealed in any namespace with any name.

Merge Into Existing

To update individual keys without re-sealing the entire secret (useful when you can't retrieve all plaintext values):

bash
# Seal only the keys you want to update
cat > /tmp/delta-secret.yaml <<'EOF'
apiVersion: v1
kind: Secret
metadata:
  name: my-secret
  namespace: my-namespace
type: Opaque
stringData:
  new-key: "new-value"
EOF

# Merge into existing sealed secret
kubeseal --cert "$CERT" --merge-into path/to/repo/sealedsecret.yaml -f /tmp/delta-secret.yaml

# SANITIZE
echo "" > /tmp/delta-secret.yaml

WARNING: --merge-into adds or updates keys but does NOT remove keys. To remove a key, you must re-seal the entire secret from scratch.

Encrypt a Single Value (--raw)

--raw encrypts one value and prints the ciphertext string — you paste it under spec.encryptedData.<key> yourself. This is the safest way to handle values with special characters (it reads raw bytes from stdin, bypassing YAML quoting entirely) and it never writes plaintext to a file.

bash
# strict scope (default): --name AND --namespace are REQUIRED and must match the target SealedSecret
printf '%s' 'p@ss"word:with$pecial' | kubeseal --cert "$CERT" \
  --raw --namespace my-namespace --name my-secret --from-file=/dev/stdin

# namespace-wide: only --namespace required
printf '%s' 'value' | kubeseal --cert "$CERT" \
  --raw --scope namespace-wide --namespace my-namespace --from-file=/dev/stdin

# cluster-wide: neither required
printf '%s' 'value' | kubeseal --cert "$CERT" \
  --raw --scope cluster-wide --from-file=/dev/stdin
  • Use printf '%s' (not echo) to avoid appending a trailing newline to the secret value.
  • The --raw ciphertext is bound to the scope/name/namespace you pass. If they don't match the SealedSecret it's pasted into, the controller will refuse to unseal it.

Rotate to a New Controller Key (--re-encrypt)

After the controller's key pair is rotated, existing SealedSecrets still decrypt (the controller keeps old keys) but should be re-encrypted to the latest key. This requires cluster access:

bash
kubeseal --re-encrypt -o yaml -f path/to/repo/sealedsecret.yaml > /tmp/reencrypted.yaml
cp /tmp/reencrypted.yaml path/to/repo/sealedsecret.yaml

--re-encrypt never exposes plaintext — the re-encryption happens inside the controller.

SealedSecret YAML Structure

yaml
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  name: my-secret
  namespace: my-namespace
spec:
  encryptedData:
    key1: AgC...base64encrypteddata...==
    key2: AgC...base64encrypteddata...==
  template:
    metadata:
      name: my-secret
      namespace: my-namespace
    type: Opaque

Common Patterns

S3-Compatible Object Store Credentials

yaml
apiVersion: v1
kind: Secret
metadata:
  name: s3-credentials
  namespace: my-namespace
type: Opaque
stringData:
  access-key-id: "<access-key>"
  secret-access-key: "<secret-key>"

Database Credentials

yaml
apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
  namespace: my-namespace
type: Opaque
stringData:
  username: "admin"
  password: "<password>"

TLS Certificate

yaml
apiVersion: v1
kind: Secret
metadata:
  name: tls-cert
  namespace: my-namespace
type: kubernetes.io/tls
stringData:
  tls.crt: |
    -----BEGIN CERTIFICATE-----
    ...
    -----END CERTIFICATE-----
  tls.key: |
    -----BEGIN PRIVATE KEY-----
    ...
    -----END PRIVATE KEY-----

Pitfalls and Gotchas

  1. Each key is encrypted independently — you can't mix encrypted values from different sealing operations into the same encryptedData block without re-sealing. Use --merge-into or re-seal the entire secret.
  1. SealedSecrets are bound to the controller's key pair — the controller retains old keys after rotation, so existing SealedSecrets keep decrypting. But new seals need the current public cert, and best practice is to kubeseal --re-encrypt existing SealedSecrets onto the latest key. If old keys are ever purged, un-re-encrypted SealedSecrets become undecryptable.
  1. `stringData` vs `data` — use stringData for plaintext values (kubeseal handles encoding). Use data for pre-base64-encoded values.
  1. Scope matters — a strict-scoped SealedSecret can only be unsealed with the exact name and namespace. If you rename the secret, it won't unseal. Use namespace-wide or cluster-wide if name changes are expected.
  1. Temp file hygiene — always overwrite temp files after sealing. Never leave plaintext secrets on disk.
  1. Git history — if a secret was accidentally committed, it remains in git history. Use git filter-repo to remove it, then rotate the credential.
  1. A cert-shaped file may not be a cert (seen in the wild). kubeseal --fetch-cert > certs/sealed-secrets-cert.pem against a cluster with no controller writes this into the file:
   error: cannot get sealed secret service: services "sealed-secrets-controller" not found.

Sealing against it appears to succeed, and the corruption only surfaces at unseal time as illegal base64 data at input byte N or no key could decrypt secret — by which point the plaintext may be gone. This is exactly why --fetch-cert is forbidden and why openssl x509 validation is mandatory before every seal. A cert file whose size is a few hundred bytes with no -----BEGIN CERTIFICATE----- line is this failure.

  1. A private key in `certs/` is a security incident, not a convenience. If certs/ contains tls.key alongside the cert, anyone with repo access can decrypt every SealedSecret in it. Report it: the controller key should be rotated and the file purged from git history. Only ever read tls.crt for sealing.

Verification

Before deploying, validate the sealed secret against the live controller (this contacts the cluster — it does not work offline):

bash
# --validate talks to the controller (no --cert needed); add --controller-namespace/--controller-name if non-default
kubeseal --validate -f path/to/repo/sealedsecret.yaml

After deploying, verify it unsealed correctly:

bash
# Check the Secret exists and has the expected keys
kubectl get secret <name> -n <namespace> -o jsonpath='{.data}' | python3 -c "import sys,json; [print(k) for k in json.load(sys.stdin).keys()]"

Or use the mcp__kubernetes__resources_get MCP tool to inspect the decrypted Secret.

Post-Sealing Checklist

  • [ ] Cert came from certs/ or the Kubernetes MCP — never --fetch-cert, kubectl, or a URL
  • [ ] Cert passed openssl x509 validation before sealing
  • [ ] Temp files overwritten (not just deleted)
  • [ ] No plaintext credentials in shell history (use set +o history before sensitive operations)
  • [ ] No credentials in git diff output
  • [ ] SealedSecret YAML committed to the correct branch
  • [ ] No seal private keys (tls.key) in the repository or output
  • [ ] Verified the sealed secret unseals correctly on the cluster