sealed-secrets 实战指南:用 SealedSecret 与 kubeseal 在 Kubernetes 中安全托管加密 Secret

原创2026-09-15 14:49:23672 阅读
文章标签:云原生应用安全运维

Sealed Secrets 是运行在 Kubernetes 集群内的控制器(controller/operator)与客户端工具 kubeseal 的组合:kubeseal 使用非对称加密将普通 Secret 加密为 SealedSecret 自定义资源,只有目标集群中的控制器能够解密还原出原始 Secret,连加密者本人都无法反解。这意味着你可以放心地把加密后的 SealedSecret 提交进 Git 仓库(哪怕仓库是公开的),实现"GitOps 管一切,除了 Secret"这一经典痛点的闭环。读完本文,你将掌握 SealedSecret 的资源结构、安装方式、加密/解封/校验/合并/重加密的完整 CLI 操作,以及密封密钥轮换(key renewal)背后的设计原理与最佳实践。

Overview:Sealed Secrets 是什么

Sealed Secrets 由两部分组成(源码见 cmd/controller/main.gocmd/kubeseal/main.go):

  • 集群侧控制器 / 运维组件(controller / operator):运行在集群内,持有 RSA 私钥,负责把 SealedSecret 解封为普通 Secret
  • 客户端工具 kubeseal:使用非对称加密把 Secret 加密成只有控制器才能解密的 SealedSecret

kubeseal 加密得到的 SealedSecret 资源本质是一份"生成 Secret 的配方"。一个典型的 SealedSecret 如下:

apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  name: mysecret
  namespace: mynamespace
spec:
  encryptedData:
    foo: AgBy3i4OJSWK+PiTySYZZA9rO43cGDEq.....

一旦控制器解封,会在集群中生成一个等效的普通 Secret:

apiVersion: v1
kind: Secret
metadata:
  name: mysecret
  namespace: mynamespace
data:
  foo: YmFy  # <- base64 编码的 "bar"

这个普通 Kubernetes Secret)可以看到,SealedSecretSpec(含 encryptedData 与可选的 template)和可选的 Status 组成,status.conditions 中的 Synced 条件用于反映解封是否成功,kubectl get 时也会以 Status / Synced 列直接展示。

SealedSecrets 作为 Secret 的模板

前面的例子只关注了加密数据项本身,但 SealedSecret 自定义资源与其解封出的 Secret 之间的关系,在很多方面(并非所有方面)类似于熟悉的 DeploymentPod 的关系。

特别需要注意的是:SealedSecret 资源上的注解(annotations)和标签(labels)并不等同于由它生成出的 Secret 上的注解和标签。为了区分这一点,SealedSecret 对象包含一个 template 段,用来编码你希望控制器写入解封后 Secret 的所有字段:

  • 模板函数:除了 Go 标准库 Text Template 函数外,还可用 Sprig 函数库(但 envexpandenvgetHostByName 被排除);
  • metadata:原样复制(ownerReference 字段会被更新,除非显式跳过,见下文"跳过 owner reference");
  • 其他字段typeimmutable 字段会被复制;data 字段可用来在 Secret 上模板化复杂值(对应源码 types.go 中的 SecretTemplateSpec);其余字段目前会被忽略。
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  name: mysecret
  namespace: mynamespace
  annotations:
    "kubectl.kubernetes.io/last-applied-configuration": ....
spec:
  encryptedData:
    .dockerconfigjson: AgBy3i4OJSWK+PiTySYZZA9rO43cGDEq.....
  template:
    type: kubernetes.io/dockerconfigjson
    immutable: true
    # 以下是要添加到输出 Secret 的标签和注解示例
    metadata:
      labels:
        "jenkins.io/credentials-type": usernamePassword
      annotations:
        "jenkins.io/credentials-description": credentials from Kubernetes

控制器会将其解封为类似下面的 Secret:

apiVersion: v1
kind: Secret
metadata:
  name: mysecret
  namespace: mynamespace
  labels:
    "jenkins.io/credentials-type": usernamePassword
  annotations:
    "jenkins.io/credentials-description": credentials from Kubernetes
  ownerReferences:
  - apiVersion: bitnami.com/v1alpha1
    controller: true
    kind: SealedSecret
    name: mysecret
    uid: 5caff6a0-c9ac-11e9-881e-42010aac003e
type: kubernetes.io/dockerconfigjson
immutable: true
data:
  .dockerconfigjson: ewogICJjcmVk...

如你所见,生成的 SecretSealedSecret 的"依赖对象"(dependent object),因此当 SealedSecret 被更新或删除时,它也会被相应更新或删除(Kubernetes 的 ownerReference 垃圾回收机制,参见控制器 pkg/controller/controller.go 中关于 GC 的注释)。

公钥 / 证书

用于加密的密钥证书(公钥部分)需要在任何使用 kubeseal 的地方可用。证书不是机密信息,但你需要确保使用的是正确的那份。

  • kubeseal 默认在运行时从控制器拉取证书(这需要能够安全访问 Kubernetes API server)。这种交互式用法很方便,但在特殊配置的集群中可能很脆弱,例如私有 GKE 集群这种控制平面与节点之间有防火墙的场景。
  • 另一种工作流是把证书存到某处(如本地磁盘):先用 kubeseal --fetch-cert >mycert.pem 抓取,再离线使用 kubeseal --cert mycert.pem。控制器启动时也会把证书打印到日志中。
  • 自 v0.9.x 起,证书每 30 天自动续期。建议你和团队定期更新离线证书。自 v0.9.2 起,kubeseal 也接受 URL,你可以搭建内部自动化,把证书发布到你信任的位置:
kubeseal --cert https://your.intranet.company.com/sealed-secrets/your-cluster.cert

它还识别 SEALED_SECRETS_CERT 环境变量(专业提示:可配合 direnv 使用)。

从源码看,证书抓取有两条路径(pkg/kubeseal/kubeseal.go):未指定 --cert 时,OpenCert 会通过 Kubernetes API 代理请求控制器的 /v1/cert.pem 端点(服务端实现见 pkg/controller/server.go);指定了本地文件或 URL 时则直接读取文件或发起 HTTP 请求。注意 ParseKey 会校验证书是否过期,过期证书将直接报错拒绝加密。

作用域(Scopes)

从终端用户的角度看,SealedSecrets 是一种"只写"设备:SealedSecret 只能被目标集群里的控制器解密,其他人(包括原作者本人)都无法从 SealedSecret 反推出原始 Secret

用户可能拥有也可能没有对目标集群的直接访问权;更具体地说,用户可能能或不能读取控制器解封出的 Secret。Kubernetes 的 RBAC 配置方式很多,但常见做法是禁止低权限用户读取 Secret,同时给用户分配一个或多个拥有更高权限(可创建/读取 Secret、可创建引用这些 Secret 的 Deployment)的命名空间。

加密的 SealedSecret 被设计为即使被查看也不会泄露其掩盖的 Secret 的任何信息。这意味着我们不能允许用户读取一个针对其无权访问命名空间的 SealedSecret,然后把它复制推送到一个他们能读取 Secret 的命名空间中。因此 Sealed Secrets 的行为表现得好像每个命名空间都有自己独立的加密密钥——一旦你为某个命名空间密封了一个 Secret,它就无法被挪到另一个命名空间并在此解密。

技术上我们并没有为每个命名空间使用独立的私钥,而是在加密过程中把命名空间名称纳入加密,从而获得同样的效果(在 pkg/crypto/crypto.goHybridEncrypt 中,命名空间/名称会作为 OAEP 的 label 参与加密)。此外,命名空间并非 RBAC 决定谁可见哪个 Secret 的唯一层级:用户可能在某个命名空间里只能访问名为 foo 的 Secret 而不能访问其他 Secret。因此默认情况下也不能让用户随意重命名 SealedSecret,否则恶意用户只需把该命名空间的任意 SealedSecret 改名覆盖掉自己有权访问的那个 Secret 即可解密。我们使用与纳入命名空间相同的机制,把Secret 名称也纳入加密。

当然,有很多场景你并不在意这一层保护。例如你的集群只有管理员能访问,或者任何人都读不了任何 Secret 资源;你可能有把密封密文挪到其他命名空间的用例(比如事先不知道命名空间名),也可能不知道 Secret 的名称(比如名称包含基于内容哈希的唯一后缀)。以下是可选的作用域:

  • strict(默认):Secret 必须以完全相同的名称和命名空间密封。名称和命名空间成为加密数据的一部分,更改名称和/或命名空间将导致"解密错误"(decryption error);
  • namespace-wide:可以在给定命名空间内自由重命名密封密文;
  • cluster-wide:可以在任意命名空间解封,也可以使用任意名称

与名称和命名空间受限相反,Secret 的数据项(即 JSON 对象键,如 spec.encryptedData.my-key)可以随意重命名而不会影响解密能力。

作用域通过 --scope 标志选择:

kubeseal --scope cluster-wide <secret.yaml >sealed-secret.json

也可以通过传递给 kubeseal 的输入 Secret 上的注解来请求作用域:

  • sealedsecrets.bitnami.com/namespace-wide: "true" -> namespace-wide
  • sealedsecrets.bitnami.com/cluster-wide: "true" -> cluster-wide

没有任何此类注解即表示 strict 模式。如果两者同时设置,cluster-wide 优先。

NOTE:下一个版本将把这些统一收敛为单个 sealedsecrets.bitnami.com/scope 注解。

Installation:安装控制器与 kubeseal

最新发布版本与详细的安装说明请参见项目 Releases 页面。云平台相关的特殊说明见 GKE

受限环境(无 RBAC)安装

在缺少创建集群级 RBAC 资源(如 ClusterRole)权限的环境中,可以使用 Releases 页面提供的 controller-norbac.yaml 清单。它是最小化的部署,只包含 Deployment、Service 和 CustomResourceDefinition,刻意省略了 ServiceAccountClusterRoleClusterRoleBinding

前提条件:

  1. 集群管理员必须已经安装好 SealedSecret 的 CRD;
  2. 你必须有一个已分配的 Service Account 来运行该 Deployment。

控制器(Controller)

部署清单后,它会创建 SealedSecret 资源,并把控制器安装到 kube-system 命名空间,同时创建 Service Account 和所需的 RBAC 角色。片刻之后控制器启动、生成密钥对并准备就绪。如果没就绪,请检查控制器日志。

Kustomize

官方控制器清单的安装机制就是一个 YAML 文件。某些场景下你可能需要做自定义(如设置自定义命名空间或环境变量)。kubectl 原生支持这一点,参见 kustomize

Helm Chart

Sealed Secrets 的 Helm Chart 现已由官方支持并托管在本仓库中:

helm repo add sealed-secrets https://bitnami.github.io/sealed-secrets

NOTE:Helm Chart 的版本号方案与 sealed secrets 项目自身的版本号方案不同。Chart 最初由社区维护,第一个版本采用了主版本号 1,而项目本身仍处于主版本 0。Chart 的版本不必与应用版本一致,但确实容易混淆,因此当前版本规则是:

  1. SealedSecret 控制器版本方案:0.X.Y
  2. Helm Chart 版本方案:1.X.Y-rZ

因此可以有多个 Chart 修订版,只修 Chart 自身而不影响静态 YAML 清单或控制器镜像。

NOTE:Helm Chart 默认以名称 sealed-secrets 安装控制器,而 kubeseal CLI 默认尝试访问名为 sealed-secrets-controller 的控制器。你可以显式给 CLI 传 --controller-name

kubeseal --controller-name sealed-secrets <args>

或者,在安装 Chart 时设置 fullnameOverride 覆盖名称。另外注意 kubeseal 默认假定控制器安装在 kube-system 命名空间。所以如果你想不用每次传控制器名称和命名空间,可以这样安装 Chart:

helm install sealed-secrets -n kube-system --set-string fullnameOverride=sealed-secrets-controller sealed-secrets/sealed-secrets
受限环境下的 Helm Chart

有些公司可能只给你单个命名空间的权限,而不是整个集群。最受限的环境通常是:

  • 分配了一个 namespace 和某个 service account
  • 无法访问集群其他部分,甚至无法访问集群级 CRD;
  • 甚至可能无法在自己的命名空间里创建更多 service account 或角色;
  • 所有 Deployment 都必须包含资源限制(resource limits)。

即便如此你仍然可以安装 sealed secrets Helm Chart,只有一个前提:集群必须已经安装好 sealed secrets 的 CRD。管理员装好 CRD 后(如果之前没有的话),你可以准备这样一个 YAML 配置文件来安装 Chart:

serviceAccount:
  create: false
  name: {allocated-service-account}
rbac:
  create: false
  clusterRole: false
resources:
  limits:
    cpu: 150m
    memory: 256Mi

注意:

  • 不创建任何 service account,而是使用分配给你的那个({allocated-service-account} 是你被分配的 service account 名称);
  • 不在命名空间或集群内创建任何 RBAC 角色;
  • 必须指定资源限制(上面是示例值,通常可用,但建议针对你的环境复核)。

假设文件命名为 config.yaml,现在可以这样安装:

helm install sealed-secrets -n {allocated-namespace} sealed-secrets/sealed-secrets --skip-crds -f config.yaml

其中 {allocated-namespace} 是你被分配的命名空间名称。

Chart 的更多可调参数(keyrenewperiodkeyttlkeycutofftimeadditionalNamespacesmaxRetriesrateLimit 等)可以在 helm/sealed-secrets/values.yaml 中查看。

Kubeseal(客户端)

Homebrew

brew install kubeseal

MacPorts

port install kubeseal

Nixpkgs

nix-env -iA nixpkgs.kubeseal

免责声明:Nixpkgs 包并非由 Bitnami 维护)

Linux

KUBESEAL_VERSION='' # 例如设置为 KUBESEAL_VERSION='0.23.0'
curl -OL "https://github.com/bitnami/sealed-secrets/releases/download/v${KUBESEAL_VERSION:?}/kubeseal-${KUBESEAL_VERSION:?}-linux-amd64.tar.gz"
tar -xvzf kubeseal-${KUBESEAL_VERSION:?}-linux-amd64.tar.gz kubeseal
sudo install -m 755 kubeseal /usr/local/bin/kubeseal

如果机器上装有 curljq,可以动态获取最新版本号(适合自动化环境):

# 通过 GitHub API 获取最新的 sealed-secrets 版本
KUBESEAL_VERSION=$(curl -s https://api.github.com/repos/bitnami/sealed-secrets/tags | jq -r '.[0].name' | cut -c 2-)

# 检查版本是否获取成功
if [ -z "$KUBESEAL_VERSION" ]; then
    echo "Failed to fetch the latest KUBESEAL_VERSION"
    exit 1
fi

curl -OL "https://github.com/bitnami/sealed-secrets/releases/download/v${KUBESEAL_VERSION}/kubeseal-${KUBESEAL_VERSION}-linux-amd64.tar.gz"
tar -xvzf kubeseal-${KUBESEAL_VERSION}-linux-amd64.tar.gz kubeseal
sudo install -m 755 kubeseal /usr/local/bin/kubeseal

其中 KUBESEAL_VERSION 是你想用的 kubeseal 发布版本的版本标签,例如 v0.18.0

从源码安装

如果只要最新的客户端工具,可以用以下命令安装到 $GOPATH/bin

go install github.com/bitnami/sealed-secrets/cmd/kubeseal@main

可以用发布标签或 commit SHA 代替 maingo install 会把 kubeseal 二进制放到 $GOPATH/bin

$(go env GOPATH)/bin/kubeseal

Upgrade:升级与版本兼容性

升级客户端工具和/或控制器前,请务必查看发布说明,了解可能的破坏性变更(breaking changes)。

支持的版本

目前仅最新版本的 Sealed Secrets 受支持用于生产环境。

与 Kubernetes 版本的兼容性

Sealed Secrets 控制器通过依赖稳定的 Kubernetes API 来保证与不同版本 Kubernetes 的兼容性。通常 Kubernetes 1.16 以上版本视为兼容。不过官方支持的是当前推荐的 Kubernetes 版本。此外,1.24 以上的版本在每次发布时都会通过我们的 CI 流程进行充分验证。

Usage:加密、部署与管理 Secret

核心用法:

# 以某种方式创建一个 json/yaml 编码的 Secret:
# (注意使用 `--dry-run` —— 这只是本地文件!)
echo -n bar | kubectl create secret generic mysecret --dry-run=client --from-file=foo=/dev/stdin -o json >mysecret.json

# 关键一步:
kubeseal -f mysecret.json -w mysealedsecret.json

# 此时 mysealedsecret.json 已可安全上传到 Github、发到 Twitter 等。

# 最终:
kubectl create -f mysealedsecret.json

# 完成!
kubectl get secret mysecret

注意 SealedSecretSecret 必须具有相同的命名空间和名称。这是为了防止集群内其他用户复用你的密封密文(详见上文 Scopes 部分)。

kubeseal 读取命名空间的顺序是:输入 Secret 中指定的命名空间 → 显式传入的 --namespace 参数 → kubectl 默认命名空间。原 Secret 上的标签、注解等会被保留,但不会自动反映到 SealedSecret 中。

从设计上看,该方案不认证用户身份。换句话说,任何人都可以创建包含任意 Secret 内容的 SealedSecret(只要命名空间/名称匹配)。这需要依赖你现有的配置管理工作流、集群 RBAC 规则等来确保只有预期的 SealedSecret 被上传到集群。与原有 Kubernetes 相比,唯一的变化是 Secret内容在集群之外被隐藏了。

管理已存在的 Secret

如果你希望 Sealed Secrets 控制器管理一个已存在的 Secret,可以给该 Secret 打上 sealedsecrets.bitnami.com/managed: "true" 注解。当解封同名称同命名空间的 SealedSecret 时,现有 Secret 会被覆盖,并且 SealedSecret 会取得该 Secret 的所有权(删除 SealedSecretSecret 也会被删除)。

修补(Patch)已存在的 Secret

自 v0.23.0 新增

有些场景你不想整体替换 Secret,只想新增或修改其中的部分键。此时可以给 Secret 打上 sealedsecrets.bitnami.com/patch: "true" 注解。使用该注解后,Secret 中那些不存在于 SealedSecret 的密钥、标签和注解不会被删除,而 SealedSecret 中存在的项会被添加Secret 上(两边都有的密钥、标签和注解会被 SealedSecret 的值修改)。

该注解不会SealedSecret 取得 Secret 的所有权。你可以同时加上 patchmanaged 两个注解,在获得修补行为的同时取得所有权。控制器源码中对应的实现逻辑见 pkg/controller/controller.gopatch 模式下只合并 DataLabelsAnnotations 而保留原有键,非 patch 模式则整体替换。

密封时跳过设置 owner references

如果你希望 SealedSecretSecret 相互独立(删除 SealedSecretSecret 不会跟着消失),需要在执行上述 Usage 步骤之前,给 Secret 打上 sealedsecrets.bitnami.com/skip-set-owner-references: "true" 注解。你仍然可以同时给 Secretsealedsecrets.bitnami.com/managed: "true",这样 SealedSecret 更新时 Secret 也会被更新。

更新已存在的 Secret

如果要在不掌握其他数据项明文的情况下新增或更新密封密文,可以把新的加密数据项复制粘贴合并进已有的 sealed secret 中。注意新密封的数据项必须使用兼容的名称和命名空间(参考上文关于作用域的说明)。

如果你不想复制粘贴,可以使用 --merge-into 命令更新已有的 sealed secret:

echo -n bar | kubectl create secret generic mysecret --dry-run=client --from-file=foo=/dev/stdin -o json \
  | kubeseal > mysealedsecret.json
echo -n baz | kubectl create secret generic mysecret --dry-run=client --from-file=bar=/dev/stdin -o json \
  | kubeseal --merge-into mysealedsecret.json

原始模式(Raw mode,实验性)

先用 kubectl 创建一个临时 Secret 再丢进管道交给 kubeseal,这种体验并不友好。项目正在重构 CLI 体验。在此期间提供一种替代模式:kubeseal 只负责把一个值加密后输出到 stdout,由你自己把它放进 SealedSecret 资源(与任何其他 k8s 资源无异)。它也可以作为编辑器/IDE 集成的构建块。

缺点是必须自己小心保持密封作用域、命名空间和名称的一致性(参见 Scopes)。

strict 作用域(默认):

$ echo -n foo | kubeseal --raw --namespace bar --name mysecret
AgBChHUWLMx...

namespace-wide 作用域:

$ echo -n foo | kubeseal --raw --namespace bar --scope namespace-wide
AgAbbFNkM54...

同时需要在 SealedSecret 中加上 sealedsecrets.bitnami.com/namespace-wide 注解:

metadata:
  annotations:
    sealedsecrets.bitnami.com/namespace-wide: "true"

cluster-wide 作用域:

$ echo -n foo | kubeseal --raw --scope cluster-wide
AgAjLKpIYV+...

同时需要在 SealedSecret 中加上 sealedsecrets.bitnami.com/cluster-wide 注解:

metadata:
  annotations:
    sealedsecrets.bitnami.com/cluster-wide: "true"

从源码看(cmd/kubeseal/main.go),--raw 模式下 strict/namespace-wide 作用域必须提供命名空间,strict 还要求提供 --name;数据可来自 stdin 或单个 --from-file(例如 /dev/stdin 读取管道输入)。

校验一个 Sealed Secret

kubeseal 提供了 --validate 标志来校验已有的密封密文。假设文件 sealed-secrets.yaml 内容如下:

apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  name: mysecret
  namespace: mynamespace
spec:
  encryptedData:
    foo: AgBy3i4OJSWK+PiTySYZZA9rO43cGDEq.....

你可以校验该密封密文是否被正确创建:

$ cat sealed-secrets.yaml | kubeseal --validate

如果密封密文无效,kubeseal 会显示:

$ cat sealed-secrets.yaml | kubeseal --validate
error: unable to decrypt sealed secret

Secret Rotation:密钥轮换与 Secret 轮换

你应该始终轮换你的 Secret。但由于你的 Secret 是用另一把密钥加密的,你需要理解这两层之间的关系才能做出正确决策。

TL;DR:

如果密封私钥被泄露,你需要先按下面"Early key renewal"一节的说明操作,再轮换任何真实 Secret 值。

SealedSecret 的密钥续期(key renewal)和重加密功能不能替代对你真实 Secret 值的定期轮换。

密封密钥续期(Sealing key renewal)

密封密钥每 30 天自动续期。也就是说,会生成一把新密钥并追加到控制器可用于解封 SealedSecret 资源的活跃密钥集合中。最新生成的密钥用于你使用 kubeseal 时密封新 Secret,也是 kubeseal --fetch-cert 下载的证书对应的密钥。

30 天是合理的默认值,但可以通过 SealedSecret 控制器 Pod 模板中的 --key-renew-period=<value> 标志按需调整,该值可用 Go duration 格式给出(例如 720h30m)。假设已把 Sealed Secrets 安装到 kube-system 命名空间,用下面的命令编辑控制器 Deployment 并添加 --key-renew-period 参数。关闭编辑器后 Deployment 会被修改,新 Pod 会自动替换旧 Pod:

kubectl edit deployment/sealed-secrets-controller --namespace=kube-system

值为 0 将停用自动密钥续期。当然,停用自动续期可能也有合理用例,但经验表明新用户往往在完全理解 sealed secrets 工作原理之前就急于认为需要控制密钥续期。更多内容参见下文关于密钥续期的常见误解

注意:不能用例如 "d" 作为天数的单位,因为 Go 标准库不支持。与其捂脸,不如借此机会思考一下程序员对时间的错误认知

一个常见误解是把密钥续期当作一种密钥轮换(rotation):旧密钥不仅过时了,而且有害,因此想把它清除。这个功能历史上确实被叫做 "key rotation",更增加了混淆。Sealed secrets 不会自动轮换,生成新密钥时旧密钥也不会被删除。旧的 SealedSecret 资源仍然可以解密(因为旧密封密钥没有被删除)。

密钥注册表初始化优先级顺序(Key registry init priority order)

控制器启动时会初始化密钥注册表(key registry),最新密钥用于密封 Secret。默认情况下,该证书根据证书的 NotBefore 属性选择。如果想改变注册表中密钥的优先级顺序,可以使用 --key-order-priority 标志:

  • CertNotBefore:(默认)密钥注册表基于密钥证书的 NotBefore 属性排序;
  • SecretCreationTimestamp:密钥注册表基于 Secret 的创建时间戳排序。

该标志影响用于加密 Secret 的公钥,以及 kubeseal --fetch-cert 获取的证书。对应实现见 pkg/controller/main.goregistryNewKeyWithSecretgetKeyOrderPriority 函数。

用户 Secret 轮换(User secret rotation)

密封密钥续期和 SealedSecret 轮换不能替代轮换你的真实 Secret。

这个工具的核心价值主张是:

把你的 Secret 加密成 SealedSecret,它可以安全存储——哪怕在公共仓库里。

如果你把任何东西存进版本控制系统(尤其是公开仓库),就必须假定你永远无法真正删除那条信息

如果某把密封密钥以某种方式泄露到集群之外,你必须把用该密钥加密的所有 SealedSecret 资源都视为已泄露。在集群内做多少密封密钥轮换,甚至对现有 SealedSecrets 文件做重加密,都无法改变这一点。

最佳实践是定期轮换你的所有真实 Secret(例如修改密码)并且用这些新 Secret 制作新的 SealedSecret 资源。但如果 SealedSecret 控制器没有在续期密封密钥,那次轮换就毫无意义,因为攻击者同样能解密新 Secret。所以两者都要做:定期续期密封密钥,并轮换你的真实 Secret!

提前密钥续期(Early key renewal)

如果你知道或怀疑某把密封密钥已被泄露,应该在开始密封新轮换出的 Secret 之前尽快续期密钥,否则等于把新 Secret 也暴露给攻击者。

可以通过向控制器传入 --key-cutoff-time 标志或 SEALED_SECRETS_KEY_CUTOFF_TIME 环境变量,把当前时间戳传给控制器来提前生成一把新密钥。期望格式为 RFC1123,可以用 date -R 生成。对应的解析逻辑见 pkg/controller/main.goinitKeyRenewalmain.go):当最近密钥的生成时间早于 cutoff time 时,会立即生成新密钥。

常见误解(关于密钥续期)

Sealed secrets 的密封密钥不是访问控制密钥(如密码)。它更像你用来阅读加密邮件的 GPG 密钥。沿用邮件类比:

假设你有理由相信自己的 GPG 私钥可能泄露了。如果第一件事就是删除私钥,那将得不偿失:之前用那把密钥加密的所有邮件你都读不了了(除非有解密副本),朋友们还不知道要改用新密钥,他们发给你的新邮件也读不了。

当然,那些加密邮件的内容已经不安全,攻击者现在可能能解密它们,但木已成舟。突然失去读信能力并不会挽回损失;相反更糟,因为你不再确切知道攻击者掌握了什么秘密。你真正要做的是确保朋友停止使用你的旧密钥,从此以后所有通信都用新密钥对加密(即朋友必须知道新公钥)。

同样的逻辑适用于 SealedSecrets。最终目标是保护你真正的"用户"Secret。"密封"密钥只是机制、是"信封"。如果某个 Secret 泄露了,没有回头路,木已成舟。

第一步是确保新 Secret 不再用那把已泄露的旧密钥加密(在邮件类比中就是:生成新密钥对并把新公钥发给所有朋友)。第二步是消除损失,这取决于 Secret 的性质。简单的例子是数据库密码:如果不小心泄露了数据库密码,正确做法是直接修改数据库密码(在数据库上改!并吊销旧的!)并且用新密码更新 SealedSecret 资源(即再次运行 kubeseal)。这两个步骤在前面章节都有描述,现在你对底层原理有了更深入的理解,不妨重读一遍。

手动密钥管理(高级)

SealedSecret 控制器及相关工作流被设计为保留旧密封密钥并定期添加新密钥。除非你清楚自己在做什么,否则不应删除旧密钥。

当然,如果你愿意,也可以手动管理(创建、移动、删除)密封密钥。它们只是普通的 k8s secrets,存在于 SealedSecret 控制器所在的命名空间(通常是 kube-system,可配置)。通过创造性地管理密封密钥可以解决一些高级用例。例如,可以在几个集群间共享同一把密封密钥,从而在多个集群中应用完全相同的 sealed secret。由于密封密钥只是普通 k8s secrets,你甚至可以用 sealed secrets 本身 + GitOps 工作流来管理你的密封密钥(在多个集群间共享同一把密钥时非常有用)!

密封密钥 secret 打上除 active 以外的任何标签值,实际上就会把该密钥从 SealedSecret 控制器中删除,但它仍保留在 k8s 中,必要时可用于手动加密/解密。从源码看(pkg/controller/keys.go),控制器通过 sealedsecrets.bitnami.com/sealed-secrets-key: active 标签来发现密钥,密钥以 TLS secret 形式存储(tls.key + tls.crt),启动时用 pkg/controller/main.goinitKeyRegistry 按标签拉取并注册。

注意SealedSecret 控制器目前不会自动拾取手动创建、删除或重新打标签的密封密钥。管理员必须重启控制器后效果才会生效。

重加密(Re-encryption,高级)

在清除某些旧密封密钥之前,你需要用最新的私钥重加密你的 SealedSecrets:

kubeseal --re-encrypt <my_sealed_secret.json >tmp.json \
  && mv tmp.json my_sealed_secret.json

上面的调用会生成一份用最新密钥全新加密的 sealed secret 文件,且整个过程 Secret 明文不会离开集群到达客户端。之后你可以把该文件保存到你的版本控制系统(kubeseal --re-encrypt 不会更新集群内的对象)。目前旧密钥不会自动垃圾回收。建议定期重加密你的 SealedSecrets。但正如上文所说,不要自欺欺人地陷入虚假安全感:你必须假定旧版本的 SealedSecret 资源(即你用认为已"死亡"的密钥加密的那份)仍可能存在于某处并对攻击者可见。也就是说,重加密不能替代定期轮换你的真实 Secret。

Details(高级):控制器内部机制

该控制器新增了一个 SealedSecret 自定义资源。SealedSecret 的核心是一个 base64 编码的、非对称加密的 Secret。控制器把一组私钥/公钥对以 kubernetes secrets 的形式维护。密钥带有 sealedsecrets.bitnami.com/sealed-secrets-key 标签,标签值标识为 activecompromised。控制器启动时会:

  1. 搜索这些密钥,若标记为 active 则加入本地存储;
  2. 创建一把新密钥;
  3. 启动密钥轮换周期。

keySelector 的定义与启动流程见 pkg/controller/main.goinitKeyRenewal。控制器还会监听 /v1/cert.pem/v1/verify/v1/rotate 等 HTTP 端点(pkg/controller/server.go),分别用于证书下发、解封校验和手动轮换触发。

Crypto(加密原理)

更多加密细节参见 docs/developer/crypto.md。从 pkg/crypto/crypto.go 可以看出,HybridEncrypt 采用的是一次性 AES-GCM + RSA-OAEP 混合加密:随机生成 32 字节会话密钥(session key)用于 AES-GCM 加密明文,会话密钥再用 RSA-OAEP(SHA-256)加密;输出字节串格式为「RSA 密文长度(2 字节大端) ‖ RSA 密文 ‖ AES 密文」。由于会话密钥只用一次,GCM 的 nonce 可以全零。解密时 HybridDecrypt 会遍历控制器持有的全部私钥逐一尝试(crypto.go),这也解释了为什么旧密钥不删除时旧 SealedSecrets 依然能解封。

Developing:开发指南

开发指南见 Developer Guide

FAQ

能否在一个 YAML / JSON 文件中一次加密多个 Secret?

可以!把任意多个 secret 放进一个文件即可。YAML 用 --- 分隔,JSON 则作为多个独立对象。

如果我不再能访问集群,还能解密吗?

不能。私钥只存储于控制器管理的 Secret 中(除非你有 k8s 对象的其他备份)。没有后门——没有加密某份 SealedSecrets 所用的私钥,就无法解密。如果你既拿不到含加密密钥的 Secrets,也拿不到集群中存活的解密版 Secrets,那么只能为所有东西重新生成密码,再用新密封密钥重新密封,等等。

如何备份我的 SealedSecrets?

如果你确实想备份加密私钥,用有合适权限的账户很容易做到:

kubectl get secret -n kube-system -l sealedsecrets.bitnami.com/sealed-secrets-key -o yaml >main.key

echo "---" >> main.key
kubectl get secret -n kube-system sealed-secrets-key -o yaml >>main.key

NOTE:只有当你的集群安装过早于 0.9.x 的 sealed-secrets 时,才需要第二条语句。

NOTE:该文件包含控制器的公钥 + 私钥,请务必妥善保管(omg-safe)!

NOTE:密封密钥续期后应重新制作备份,否则备份将无法解密新的密封密文。

灾难后要从备份恢复,只需在启动控制器前把这些 secrets 放回去;如果控制器已启动,则替换掉新建的 secrets 并重启控制器:

  • Helm 部署方式:

    kubectl apply -f main.key
    kubectl delete pod -n kube-system -l app.kubernetes.io/name=sealed-secrets
    
  • 通过 controller.yaml 清单部署:

    kubectl apply -f main.key
    kubectl delete pod -n kube-system -l name=sealed-secrets-controller
    

能否用备份密钥离线解密我的 Secret?

虽然不建议把 sealed-secrets 当作长期存储系统,但确实有些人的合理需求是:集群宕机时能恢复 Secret,而把备份恢复到新的 SealedSecret 控制器部署中又不现实。如果你备份了一把或多把私钥(见上一问),可以用 kubeseal --recovery-unseal --recovery-private-key file1.key,file2.key,... 命令解密 sealed secrets 文件。该命令支持 PEM 编码私钥,也支持备份出来的 json/yaml 编码的控制器 secret(及 v1.List),见 cmd/kubeseal/main.go

kubeseal 有哪些可用的 flag?

可以使用 kubeseal --help 查看全部可用标志。常见标志(cmd/kubeseal/main.go)包括:--cert(证书文件/URL)、--controller-namespace(默认 kube-system)、--controller-name(默认 sealed-secrets-controller)、-o/--format(json 或 yaml)、-w/--sealed-secret-file-f/--secret-file--fetch-cert--allow-empty-data--validate--merge-into--raw--name--from-file--scope--re-encrypt--recovery-unseal--recovery-private-key 等。

如何更新已用 sealed secrets 加密的 JSON/YAML/TOML/… 文件中的部分内容?

一个 Kubernetes Secret 资源包含多个数据项,本质是键/值对的扁平映射。SealedSecrets 在这一层操作,不关心值里放的是什么。换句话说,它无法理解你放进 secret 里的任何结构化配置文件,因此也无法帮你更新其中的单个字段。由于这是一个常见问题(尤其是面对遗留应用时),我们提供了一个示例展示可能的变通方案。

能否使用自己(预先生成的)证书?

可以,你可以给控制器提供自己的证书,它会直接使用。请参考 docs/bring-your-own-certificates.md 中的变通方案。

控制器不在 kube-system 命名空间时如何使用 kubeseal?

如果你把控制器安装在非默认的 kube-system 命名空间,需要给 kubeseal 命令行工具提供该命名空间。有两个办法:

  1. 通过命令行选项 --controller-namespace <namespace>
kubeseal --controller-namespace sealed-secrets <mysecret.json >mysealedsecret.json
  1. 通过环境变量 SEALED_SECRETS_CONTROLLER_NAMESPACE
export SEALED_SECRETS_CONTROLLER_NAMESPACE=sealed-secrets
kubeseal <mysecret.json >mysealedsecret.json

如何校验镜像?

我们的镜像使用 cosign 签名,签名保存在 GitHub Container Registry 中。v0.20.2(含)之前的镜像使用 Cosign v1 签名,新镜像用 Cosign v2 签名。

# 导出 COSIGN_REPOSITORY 变量,指向 GHCR signs 路径
export COSIGN_REPOSITORY=ghcr.io/bitnami/sealed-secrets-controller/signs

# 校验上传到 GHCR 的镜像
cosign verify --key .github/workflows/cosign.pub ghcr.io/bitnami/sealed-secrets-controller:latest

# 校验上传到 Dockerhub 的镜像
cosign verify --key .github/workflows/cosign.pub docker.io/bitnami/sealed-secrets-controller:latest

如何让一个控制器只管理部分命名空间?

如果你想让一个控制器管理多个(但不是全部)命名空间,可以用命令行标志 --additional-namespaces=<namespace1>,<namespace2>,<...> 提供额外命名空间。请确保在目标命名空间中提供了合适的 roles 和 rolebindings,以便控制器管理其中的 secrets(cmd/controller/main.go)。

能否配置控制器解封重试次数?

可以,用 --max-unseal-retries 标志配置,默认值为 5(见 cmd/controller/main.go)。该标志控制解封 Sealed Secrets 时的最大重试次数。

如何在集群范围或指定命名空间范围内管理 SealedSecrets?

默认情况下,控制器使用 --all-namespaces 标志(默认值为 true)监听所有命名空间中的 SealedSecret 资源。如果需要限制控制器范围,有两个选项:

  • 监听部分命名空间:用 --additional-namespaces=<ns1>,<ns2> 提供逗号分隔的命名空间列表;
  • 只监听本地命名空间:设置 --all-namespaces=false(或环境变量 SEALED_SECRETS_ALL_NAMESPACES=false)。这在多租户集群中很有用,每个命名空间可以有独立密封密钥的隔离控制器。

Community:社区与相关项目

  • Kubernetes Slack 上的 #sealed-secrets 频道(可到 http://slack.k8s.io 注册 Kubernetes Slack org)。

相关项目(社区生态):

  • kseal:kubeseal 的伴侣工具(Kubeseal Companion)
  • kubeseal-convert:转换工具
  • Visual Studio Code 扩展:codecontemplator.kubeseal
  • WebSeal:在浏览器中生成 secrets
  • HybridEncrypt TypeScript 实现:aes-gcm-rsa-oaep
  • [已废弃] Sealed Secrets Operator

延伸阅读:CRD 定义见 helm/sealed-secrets/crds/bitnami.com_sealedsecrets.yamlschema-v1alpha1.yaml;类型定义与注解常量见 pkg/apis/sealedsecrets/v1alpha1/types.go;控制器核心解封/合并逻辑见 pkg/controller/controller.go;加密实现见 pkg/crypto/crypto.go;kubeseal 业务逻辑见 pkg/kubeseal/kubeseal.go

sealed-secrets

目录

Overview:Sealed Secrets 是什么SealedSecrets 作为 Secret 的模板公钥 / 证书作用域(Scopes)Installation:安装控制器与 kubeseal受限环境(无 RBAC)安装控制器(Controller)KustomizeHelm ChartKubeseal(客户端)HomebrewMacPortsNixpkgsLinux从源码安装Upgrade:升级与版本兼容性支持的版本与 Kubernetes 版本的兼容性Usage:加密、部署与管理 Secret管理已存在的 Secret修补(Patch)已存在的 Secret密封时跳过设置 owner references更新已存在的 Secret原始模式(Raw mode,实验性)校验一个 Sealed SecretSecret Rotation:密钥轮换与 Secret 轮换密封密钥续期(Sealing key renewal)密钥注册表初始化优先级顺序(Key registry init priority order)用户 Secret 轮换(User secret rotation)提前密钥续期(Early key renewal)常见误解(关于密钥续期)手动密钥管理(高级)重加密(Re-encryption,高级)Details(高级):控制器内部机制Crypto(加密原理)Developing:开发指南FAQ能否在一个 YAML / JSON 文件中一次加密多个 Secret?如果我不再能访问集群,还能解密吗?如何备份我的 SealedSecrets?能否用备份密钥离线解密我的 Secret?kubeseal 有哪些可用的 flag?如何更新已用 sealed secrets 加密的 JSON/YAML/TOML/… 文件中的部分内容?能否使用自己(预先生成的)证书?控制器不在 kube-system 命名空间时如何使用 kubeseal?如何校验镜像?如何让一个控制器只管理部分命名空间?能否配置控制器解封重试次数?如何在集群范围或指定命名空间范围内管理 SealedSecrets?Community:社区与相关项目