按源仓库内容呈现,保留标题、案例、代码、表格、链接以及原文引用的演示图片。
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 againstkubesealv0.38.4. Flag names are stable across recent releases, but runkubeseal --helpif 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
- NEVER print plaintext credentials to stdout, conversation output, or any readable medium.
- NEVER write plaintext secrets to files in the repository or any version-controlled location.
- NEVER echo or log secret values — use variables and pipe directly.
- NEVER include plaintext secrets in commit messages, PR descriptions, or comments.
- ALWAYS overwrite or zero-out temp files containing plaintext secrets immediately after use.
- 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
rmtemp files (may be blocked by permission rules) — overwrite instead:
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
kubesealCLI 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.
# 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"):
CERT=certs/tls.crtIf 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: SecretlabelSelector: sealedsecrets.bitnami.com/sealed-secrets-key=activenamespace: wherever the controller runs (commonlykube-systemorsealed-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:
printf '%s' '<tls.crt base64 from the MCP response>' | base64 -d > /tmp/sealed-secrets-cert.pem
CERT=/tmp/sealed-secrets-cert.pemStep 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 thetls.crtfield. Never decode, print, echo, write, or committls.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:
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.
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"
EOFAlternative — temp file (when you must inspect/edit the plaintext first):
# 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.yaml2. 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 rewritesspec.encryptedDatain place; keepmetadata,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 intoencryptedData) 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:
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 makekubesealfail witherror: no secrets found. This is common with generated passwords and S3 keys. Never build YAML likepassword: "$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.
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-idOr 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::
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.yaml3. 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
| Flag | Purpose | ||
|---|---|---|---|
--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) | ||
--raw | Encrypt 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-encrypt | Re-encrypt an existing SealedSecret with the controller's latest key (needs cluster) | ||
--validate | Verify the sealed secret decrypts — contacts the controller; requires cluster access | ||
--fetch-cert | FORBIDDEN — 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-unseal | Disaster-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):
# 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.yamlWARNING: --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.
# 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'(notecho) to avoid appending a trailing newline to the secret value. - The
--rawciphertext 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:
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
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: OpaqueCommon Patterns
S3-Compatible Object Store Credentials
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
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
namespace: my-namespace
type: Opaque
stringData:
username: "admin"
password: "<password>"TLS Certificate
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
- Each key is encrypted independently — you can't mix encrypted values from different sealing operations into the same
encryptedDatablock without re-sealing. Use--merge-intoor re-seal the entire secret.
- 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-encryptexisting SealedSecrets onto the latest key. If old keys are ever purged, un-re-encrypted SealedSecrets become undecryptable.
- `stringData` vs `data` — use
stringDatafor plaintext values (kubeseal handles encoding). Usedatafor pre-base64-encoded values.
- 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. Usenamespace-wideorcluster-wideif name changes are expected.
- Temp file hygiene — always overwrite temp files after sealing. Never leave plaintext secrets on disk.
- Git history — if a secret was accidentally committed, it remains in git history. Use
git filter-repoto remove it, then rotate the credential.
- A cert-shaped file may not be a cert (seen in the wild).
kubeseal --fetch-cert > certs/sealed-secrets-cert.pemagainst 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.
- A private key in `certs/` is a security incident, not a convenience. If
certs/containstls.keyalongside 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 readtls.crtfor sealing.
Verification
Before deploying, validate the sealed secret against the live controller (this contacts the cluster — it does not work offline):
# --validate talks to the controller (no --cert needed); add --controller-namespace/--controller-name if non-default
kubeseal --validate -f path/to/repo/sealedsecret.yamlAfter deploying, verify it unsealed correctly:
# 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 x509validation before sealing - [ ] Temp files overwritten (not just deleted)
- [ ] No plaintext credentials in shell history (use
set +o historybefore 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
