mcpField
K
kubeseal

kubeseal

github.com/mmontes11云与 DevOps1369 次安装1 天前更新
代码仓库

关于 kubeseal

kubeseal 是用于将 Kubernetes Secret 加密为 Bitnami SealedSecret 的技能,供处理集群密钥和 Secret 的开发者使用。它强调安全地封存及更新密钥,并保护明文和私钥不外泄。

主要功能

  1. 01将 Secret 封存为 SealedSecret
  2. 02使用本地证书离线加密
  3. 03更新已有 SealedSecret 的密钥
  4. 04以 --raw 加密单个值
  5. 05遵循明文与私钥安全规范

适用场景

  1. 01将应用凭据安全存入 Git
  2. 02更新 SealedSecret 中的部分密钥
  3. 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 控制器才能解密(解封)。

已针对 kubeseal v0.38.4 验证。近期版本的标志名称保持稳定,但如果命令行为异常,请运行 kubeseal --help。

关键要点: 公共证书(tls.crt)不是秘密——它是公钥,可以安全共享,甚至提交到代码库。只有控制器的私钥(tls.key)属于敏感信息。拿到证书后,封存操作完全在本地离线进行;封存无需访问集群。

关键安全规则

绝不泄露凭据
  1. 绝不将明文凭据打印到 stdout、对话输出或任何可读介质。
  2. 绝不将明文 Secret 写入代码库或任何受版本控制的位置。
  3. 绝不回显或记录 Secret 值——使用变量并直接通过管道传递。
  4. 绝不在提交消息、PR 描述或评论中包含明文 Secret。
  5. 使用后立即覆盖或清零包含明文 Secret 的临时文件。
  6. 绝不泄露封存密钥材料——私钥(tls.key)绝不能打印、提交或共享。封存只需要公钥(tls.crt)。
临时文件处理

优先采用完全不会将明文写入磁盘上有名称的文件的方法——例如下文“1. 新建 SealedSecret”中的 stdin heredoc,或“加密单个值(--raw)”。如果无法避免使用临时文件:

  • 将临时 Secret 文件写入 /tmp/,并使用描述性名称。
  • 封存后,使用空数据或垃圾数据覆盖临时文件内容。
  • 绝不要 rm 临时文件(可能会被权限规则阻止)——改用覆盖:
    echo "" > /tmp/secret-temp.yaml
    
  • 绝不要将临时文件提交到代码库。

注意:公共证书(tls.crt / .pem)不是敏感信息,无需清理——将其覆盖只会导致之后必须重新获取。只覆盖包含明文 Secret 值或私钥的文件。

前置条件

  • 已安装并可使用 kubeseal CLI
  • 公钥证书(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: Secret
  • labelSelector: sealedsecrets.bitnami.com/sealed-secrets-key=active
  • namespace:控制器运行所在的命名空间(通常是 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-----

陷阱与注意事项

  1. 每个键都会单独加密 — 你不能将不同密封操作生成的加密值混入同一个 encryptedData 块中,而不重新密封。使用 --merge-into,或重新密封整个密钥。

  2. SealedSecret 与控制器的密钥对绑定 — 控制器在轮换后会保留旧密钥,因此现有 SealedSecret 仍可解密。但新密封操作需要使用当前的公钥证书,最佳做法是使用 kubeseal --re-encrypt,将现有 SealedSecret 重新加密到最新密钥。如果旧密钥被清除,未重新加密的 SealedSecret 将无法解密。

  3. stringData 与 data — 明文值使用 stringData(kubeseal 会处理编码)。预先进行过 base64 编码的值使用 data。

  4. 作用域很重要 — strict 作用域的 SealedSecret 只能使用完全匹配的名称和命名空间解封。如果重命名密钥,就无法解封。如果可能会更改名称,请使用 namespace-wide 或 cluster-wide。

  5. 临时文件卫生 — 密封后务必覆盖临时文件。切勿将明文密钥留在磁盘上。

  6. Git 历史记录 — 如果密钥意外提交,它仍会保留在 Git 历史记录中。使用 git filter-repo 将其移除,然后轮换凭据。

  7. 看起来像证书的文件未必真的是证书(实际中确有此事)。 如果集群中没有控制器,使用 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----- 行,就是这种故障。

  8. 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 能在集群上正确解封

评论

支持 Markdown