关于 kubeseal
kubeseal 是用于将 Kubernetes Secret 加密为 Bitnami SealedSecret 的技能,供处理集群密钥和 Secret 的开发者使用。它强调安全地封存及更新密钥,并保护明文和私钥不外泄。
主要功能
- 01将 Secret 封存为 SealedSecret
- 02使用本地证书离线加密
- 03更新已有 SealedSecret 的密钥
- 04以 --raw 加密单个值
- 05遵循明文与私钥安全规范
适用场景
- 01将应用凭据安全存入 Git
- 02更新 SealedSecret 中的部分密钥
- 03凭据变更后重新封存 Secret
MCP 相关
技能可通过 Kubernetes MCP 获取控制器的公钥证书,并读取集群中已解密的 Secret,以支持封存或重新封存。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ mmontes11/ skills --skill kubeseal安装后在 Claude Code 中输入/kubeseal确认已加载。
详细介绍
Kubeseal:封存和重新封存 Kubernetes Secret
概述
将 Kubernetes Secret 封存为 Bitnami SealedSecret,以便安全地存储在 Git 中。SealedSecret 使用非对称加密——任何人都可以加密(封存),但只有集群中的 sealed-secrets 控制器才能解密(解封)。
已针对
kubesealv0.38.4 验证。近期版本的标志名称保持稳定,但如果命令行为异常,请运行kubeseal --help。
关键要点: 公共证书(tls.crt)不是秘密——它是公钥,可以安全共享,甚至提交到代码库。只有控制器的私钥(tls.key)属于敏感信息。拿到证书后,封存操作完全在本地离线进行;封存无需访问集群。
关键安全规则
绝不泄露凭据
- 绝不将明文凭据打印到 stdout、对话输出或任何可读介质。
- 绝不将明文 Secret 写入代码库或任何受版本控制的位置。
- 绝不回显或记录 Secret 值——使用变量并直接通过管道传递。
- 绝不在提交消息、PR 描述或评论中包含明文 Secret。
- 使用后立即覆盖或清零包含明文 Secret 的临时文件。
- 绝不泄露封存密钥材料——私钥(
tls.key)绝不能打印、提交或共享。封存只需要公钥(tls.crt)。
临时文件处理
优先采用完全不会将明文写入磁盘上有名称的文件的方法——例如下文“1. 新建 SealedSecret”中的 stdin heredoc,或“加密单个值(--raw)”。如果无法避免使用临时文件:
- 将临时 Secret 文件写入
/tmp/,并使用描述性名称。 - 封存后,使用空数据或垃圾数据覆盖临时文件内容。
- 绝不要
rm临时文件(可能会被权限规则阻止)——改用覆盖:echo "" > /tmp/secret-temp.yaml - 绝不要将临时文件提交到代码库。
注意:公共证书(
tls.crt/.pem)不是敏感信息,无需清理——将其覆盖只会导致之后必须重新获取。只覆盖包含明文 Secret 值或私钥的文件。
前置条件
- 已安装并可使用
kubesealCLI - 公钥证书(
tls.crt)——只能按下文“获取证书”中的说明获取 - 可通过 Kubernetes MCP 访问 Kubernetes 集群(用于证书备用获取方式,以及检索现有 Secret 的值)
获取证书
本节为权威说明。证书必须且只能来自以下两个来源之一,并按顺序尝试。 不要自行采用第三种来源。
来源 1(首选)——项目的 certs/ 文件夹
先在代码库中查找证书。封存是离线操作;如果代码库中已包含证书,则完全无需访问集群。
# 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/*'
将 CERT 设为找到的文件,然后在使用前验证证书(参见“始终验证证书”):
CERT=certs/tls.crt
如果 certs/ 中有多个候选证书,请逐一验证,并优先选择能通过验证、与代码库中现有 SealedSecret 相符的证书。
来源 2(备用)——通过 Kubernetes MCP 获取集群中的证书
仅当 certs/ 中没有有效证书时使用。使用 Kubernetes MCP 工具读取控制器当前使用的密钥 Secret:
步骤 1。 调用 mcp__kubernetes__resources_list,并提供:
apiVersion: v1、kind: SecretlabelSelector: sealedsecrets.bitnami.com/sealed-secrets-key=activenamespace:控制器运行所在的命名空间(通常是kube-system或sealed-secrets);省略该参数则搜索所有命名空间
如果返回多个 active 密钥 Secret,请选择 metadata.creationTimestamp 最新的那个——这是控制器当前用于封装的密钥。
步骤 2。 从该 Secret 中取出 data["tls.crt"],并进行 base64 解码,写入本地文件:
printf '%s' '<tls.crt base64 from the MCP response>' | base64 -d > /tmp/sealed-secrets-cert.pem
CERT=/tmp/sealed-secrets-cert.pem
步骤 3。 使用前先验证(见下一节)。
如果 MCP 未返回此类 Secret,说明 MCP 指向的集群上没有安装控制器。停止并告知用户——不要回退到其他方法。
危险——该 Secret 还包含
tls.key,即控制器的私钥。 只能读取和使用tls.crt字段。绝不要解码、打印、回显、写入或提交tls.key。不要在任何地方粘贴原始 MCP 响应。
始终验证证书
文件存在并不意味着它就是证书。 重定向可能会把错误文本写入看似证书的文件,而使用这些垃圾内容进行封装,会生成稍后才会失败的 SealedSecret,并报错 illegal base64 data at input byte N 或 no key could decrypt secret——此时明文早已丢失。
每次封装前都要运行此命令。如果失败,立即停止——改用下一个来源,不要继续封装:
openssl x509 -in "$CERT" -noout -subject -dates || echo "NOT A VALID CERT — do not seal with this file"
如果 certs/ 文件未通过验证,请告知用户并转到来源 2。绝不要靠猜测来“修复”它。
确认密钥正确(推荐)
有效证书仍可能对应错误的密钥,或是已轮换的密钥。如果仓库中已有可正常工作的 SealedSecret,请将证书的公钥与已知的正确密钥对进行比较,或检查新封装的测试值能否往返还原,以确认该证书与用于封装它们的证书匹配。如果仓库中还包含私钥,请将此视为安全问题并记录下来(见“陷阱”),不要依赖该私钥。
工作流
1. 新建 SealedSecret
首选——通过标准输入进行封装(磁盘上不留明文文件):
封装后的输出可以直接安全地写入仓库。只有输入是敏感信息,通过 heredoc 管道传递可以完全避免将其写入文件系统。
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
替代方案——临时文件(必须先检查或编辑明文时):
# 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. 重新封装/更新现有 SealedSecret(保留字段)
此流程适用于重新封装请求——用户要求重新封装现有 SealedSecret,通常是因为它无法解封(
illegal base64 data at input byte N、no key could decrypt secret),或凭据发生了变化。重新封装会原地重写spec.encryptedData;除非用户另有要求,否则应使metadata、spec.template(标签、注释、type)、名称和命名空间与原始内容逐字节一致,并遵循周边文件的惯例。
更新 SealedSecret 中的部分键(例如更换 S3 凭据但保留数据库密码)时,有两种选择:
- 更新单个键:使用
--raw(粘贴到encryptedData中)或--merge-into——只需要你要更改的键的值。 - 重新封装整个 Secret:需要从集群中获取所有键的明文。
步骤 1:从集群中获取现有 Secret
优先使用 mcp__kubernetes__resources_get MCP 工具读取解密后的 Secret——SealedSecret 控制器会自动将其解密为集群中的普通 Secret(apiVersion: v1、kind: Secret,以及名称和命名空间)。
或者通过 kubectl:
kubectl get secret <name> -n <namespace> -o jsonpath='{.data}'
步骤 2:使用更新后的值解码并重新密封
危险——不要将机密值插入 YAML。 如果值包含
"、:、\n、前导空格或$,就会破坏引号,导致机密损坏或让kubeseal失败并报错error: no secrets found。生成的密码和 S3 密钥经常会出现这种情况。切勿构建如下 YAML:password: "$EXISTING_PASS"。
安全做法——使用 --raw 分别密封每个键,然后合并。 --raw 会将 stdin 中的值作为原始字节读取(不涉及 YAML 引号),因此可以正确处理特殊字符。在默认的 strict 范围下,必须传入 --name 和 --namespace,且二者必须与目标 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-id
也可以使用 --merge-into(见下文)就地更新单个键,而不影响其他键。
如果必须构建完整的 Secret 对象(例如,从头创建一个新的 SealedSecret),请将值进行 base64 编码并放入 data: 字段,而不是 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.yaml
3. 获取公钥
请参阅上文的 “获取证书”——优先使用 certs/,其次使用 Kubernetes MCP。不允许使用其他来源;密封前必须使用 openssl x509 验证证书。
kubeseal 命令参考
常用标志
| 标志 | 用途 |
|---|---|
--cert <file> | 用于加密的公钥文件。务必显式传入此项——省略后,kubeseal 会自动检测控制器。只能使用本地文件,绝不可使用 URL |
-o, --format yaml|json | 输出格式(默认:json) |
-f, --secret-file <file> | 输入 Secret YAML 文件(使用 /dev/stdin 进行管道传输) |
-n, --namespace <ns> | 待密封机密本身的命名空间范围(不是控制器所在位置) |
--scope strict|namespace-wide|cluster-wide | 密封机密的范围(默认:strict) |
--merge-into <file> | 将已密封的键合并到现有 SealedSecret 文件中(就地修改) |
--raw | 加密 --from-file 提供的单个原始值;需要 --scope,且在 strict 范围下还需要 --name 和 --namespace |
--from-file <file> | (配合 --raw)从文件读取值;管道传输时使用 /dev/stdin |
--name <name> | 密封机密的名称(在 strict 范围下与 --raw 配合使用时为必填项) |
--re-encrypt | 使用控制器最新的密钥重新加密现有 SealedSecret(需要集群) |
--validate | 验证密封机密是否能够解密——会联系控制器;需要集群访问权限 |
--fetch-cert | 禁止使用。 请改为从 certs/ 或 Kubernetes MCP 获取证书(见“获取证书”)。此命令失败时会向 stdout 输出错误消息,重定向后会生成错误的证书文件 |
--controller-namespace <ns> | 控制器运行所在的命名空间(默认:kube-system)。仅适用于 --validate/--re-encrypt——绝不可用于获取证书 |
--controller-name <name> | 控制器名称(默认:sealed-secrets-controller)。限制同上 |
--recovery-unseal | 使用 --recovery-private-key 进行灾难恢复解密(会处理私钥——请格外谨慎) |
范围模式
strict(默认):只能使用完全匹配的命名空间和名称解封。namespace-wide:可在指定命名空间内使用任意名称解封。cluster-wide:可在任意命名空间内使用任意名称解封。
合并到现有文件
若要更新单个键,而不重新密封整个机密(适用于无法获取所有明文值的情况):
# 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
警告:--merge-into 会添加或更新键,但不会删除键。若要删除某个键,必须从头重新密封整个机密。
加密单个值(--raw)
--raw 会加密一个值并输出密文字符串 — 你需要自行将其粘贴到 spec.encryptedData.<key> 下。这是处理包含特殊字符的值最安全的方式(它会从 stdin 读取原始字节,完全绕过 YAML 引号处理),并且绝不会将明文写入文件。
# 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
- 使用
printf '%s'(而不是echo),避免在密钥值末尾追加换行符。 --raw密文与传入的作用域/名称/命名空间绑定。如果它们与粘贴密文的 SealedSecret 不匹配,控制器将拒绝解封。
轮换为新的控制器密钥(--re-encrypt)
控制器密钥对轮换后,现有 SealedSecret 仍然可以解密(控制器会保留旧密钥),但应使用最新密钥重新加密。这需要访问集群:
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 绝不会暴露明文 — 重新加密是在控制器内部完成的。
SealedSecret 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
常见模式
S3 兼容对象存储凭据
apiVersion: v1
kind: Secret
metadata:
name: s3-credentials
namespace: my-namespace
type: Opaque
stringData:
access-key-id: "<access-key>"
secret-access-key: "<secret-key>"
数据库凭据
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
namespace: my-namespace
type: Opaque
stringData:
username: "admin"
password: "<password>"
TLS 证书
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-----
陷阱与注意事项
-
每个键都会单独加密 — 你不能将不同密封操作生成的加密值混入同一个
encryptedData块中,而不重新密封。使用--merge-into,或重新密封整个密钥。 -
SealedSecret 与控制器的密钥对绑定 — 控制器在轮换后会保留旧密钥,因此现有 SealedSecret 仍可解密。但新密封操作需要使用当前的公钥证书,最佳做法是使用
kubeseal --re-encrypt,将现有 SealedSecret 重新加密到最新密钥。如果旧密钥被清除,未重新加密的 SealedSecret 将无法解密。 -
stringData与data— 明文值使用stringData(kubeseal 会处理编码)。预先进行过 base64 编码的值使用data。 -
作用域很重要 —
strict作用域的 SealedSecret 只能使用完全匹配的名称和命名空间解封。如果重命名密钥,就无法解封。如果可能会更改名称,请使用namespace-wide或cluster-wide。 -
临时文件卫生 — 密封后务必覆盖临时文件。切勿将明文密钥留在磁盘上。
-
Git 历史记录 — 如果密钥意外提交,它仍会保留在 Git 历史记录中。使用
git filter-repo将其移除,然后轮换凭据。 -
看起来像证书的文件未必真的是证书(实际中确有此事)。 如果集群中没有控制器,使用
kubeseal --fetch-cert > certs/sealed-secrets-cert.pem会将以下内容写入文件:error: cannot get sealed secret service: services "sealed-secrets-controller" not found.使用它进行密封看似会成功,但损坏问题只会在解封时显现为
illegal base64 data at input byte N或no key could decrypt secret— 到那时,明文可能已经丢失。这正是禁止使用--fetch-cert,并且每次密封前都必须使用openssl x509验证的原因。证书文件只有几百字节,且没有-----BEGIN CERTIFICATE-----行,就是这种故障。 -
certs/中出现私钥是安全事件,不是便利之举。 如果certs/中除了证书还包含tls.key,任何有仓库访问权限的人都能解密其中的每个 SealedSecret。请上报此事:控制器密钥应轮换,并从 Git 历史记录中清除该文件。密封时只能读取tls.crt。
验证
部署前,使用正在运行的控制器验证密封后的密钥(此操作会连接集群 — 离线时无法使用):
# --validate talks to the controller (no --cert needed); add --controller-namespace/--controller-name if non-default
kubeseal --validate -f path/to/repo/sealedsecret.yaml
部署后,验证它是否已正确解封:
# 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()]"
或者使用 mcp__kubernetes__resources_get MCP 工具检查解密后的 Secret。
封装后检查清单
- 证书来自
certs/或 Kubernetes MCP,而非--fetch-cert、kubectl或 URL - 封装前已通过
openssl x509验证证书 - 已覆盖临时文件(而非仅删除)
- Shell 历史记录中没有明文凭据(执行敏感操作前使用
set +o history) - git diff 输出中没有凭据
- SealedSecret YAML 已提交到正确的分支
- 仓库或输出中没有封装私钥(
tls.key) - 已验证 Sealed Secret 能在集群上正确解封