SOPS + Age 和 Sealed Secrets
摘要
一篇博客文章,解释如何将 SOPS 与 Age 结合用于在集群外加密 Secrets,并使用 Bitnami Sealed Secrets 在集群内解密,从而实现 Kubernetes 的 GitOps 工作流。
<p><a href="https://lobste.rs/s/72alqa/sops_age_sealed_secrets">评论</a></p>
查看缓存全文
缓存时间: 2026/05/31 16:21
# SOPS + Age 与 Sealed Secrets
来源:https://www.jonashietala.se/blog/2026/05/31/sops_age_and_sealed_secrets/
## 现代Kubernetes家庭实验室 (https://www.jonashietala.se/series/kube-homelab)
每当我读到其他关于Kubernetes的系列文章,一旦进入 secrets 部分,我的眼睛就开始打转。我控制不住自己;我更想读那些有趣的内容。secrets 固然是必要的,但有点枯燥...
不过,如果我想做好 GitOps,就必须管理好 secrets(并且记录整个过程)。越早设置越好。
## 集群内外 (https://www.jonashietala.se/blog/2026/05/31/sops_age_and_sealed_secrets/#In-and-outside-cluster)
Kubernetes 提供了不同的 secrets 管理方案。其中值得注意的是Sealed Secrets (https://github.com/bitnami-labs/sealed-secrets),它创建的文件可以安全地提交到 git,并且 Kubernetes 在集群内解密它们。
这很棒,但有一个大缺点:它只能管理 Kubernetes 内部的 secrets。无法用于加密诸如`talosconfig`或 Terraform 使用的 Proxmox 密码之类的内容。
这就是为什么我还要使用SOPS + Age (https://www.jonashietala.se/blog/2026/05/31/sops_age_and_sealed_secrets/#SOPS-Age),它可以加密我们想要的任何文件。思路是使用 SOPS + Age 管理引导 secrets,当ArgoCD (https://argo-cd.readthedocs.io/en/stable/)启动后,再让 Sealed Secrets 接管。这样我只需要管理一个私钥,其余部分都可以从 git 仓库获取。
## SOPS + Age (https://www.jonashietala.se/blog/2026/05/31/sops_age_and_sealed_secrets/#SOPS-Age)
首先,我们需要在本地安装`sops`和`age`(我在包管理器里找到了它们)。然后生成私钥:
``
mkdir -p ~/.config/sops/age
age-keygen -o ~/.config/sops/age/keys.txt
``
这个密钥不能丢失,请安全存储。我把它存到了Bitwarden (https://bitwarden.com/)(虽然我正在迁移到自托管的Vaultwarden (https://github.com/dani-garcia/vaultwarden),但把密钥放在那里有点风险,万一集群故障可能被锁在外面)。
然后需要一个`\.sops\.yaml`来描述要加密/解密的文件。例如,以下是`talosconfig\.yaml`的条目:
``
creation_rules:
- path_regex: infrastructure/talosconfig(\.encrypted)?.yaml$
age: age1rrkgd5yza053qk9m8lp0ww39apdarz7w0rjyq85493g8l9gufgnq9cehzx
encrypted_regex: '^(ca|crt|key)$'
``
(`age`包含公钥,可以安全分享。)
使用`encrypted\_regex`可以只加密特定字段;如果省略,则会加密整个文件。
然后加密和解密之前文章中 (https://www.jonashietala.se/blog/2026/05/22/talos_linux_on_proxmox_with_terraform/#Generating-config-files) 生成的`talosconfig\.yaml`:
``
sops --encrypt talosconfig.yaml > talosconfig.encrypted.yaml
sops --decrypt talosconfig.encrypted.yaml > talosconfig.yaml
``
`talosconfig\.encrypted\.yaml`可以安全地提交到 git,但明文`talosconfig\.yaml`应添加到`\.gitignore`中。
### Just 命令 (https://www.jonashietala.se/blog/2026/05/31/sops_age_and_sealed_secrets/#Just-commands)
这些命令我很快就会忘记,所以将它们添加到Just (https://just.systems/man/en/introduction.html)中。这些配方将处理目前为止 (https://www.jonashietala.se/blog/2026/05/22/talos_linux_on_proxmox_with_terraform/#Generating-config-files) 处理的secrets,而明文文件(`talosconfig`、`kubeconfig`、`secrets\.auto\.tfvars`、`terraform\.tfstate`)应能让我们重新获得集群控制权,或从 git 仓库和 sops 密钥重新引导集群。
``
[doc("Decrypt required files committed to git")]
decrypt_required:
just secrets decrypt_cluster_config
just secrets decrypt_terraform_secrets
just secrets decrypt_terraform_state
[doc("Encrypt talosconfig and kubeconfig")]
[working-directory('../infrastructure')]
encrypt_cluster_config:
sops --encrypt kubeconfig.yaml > kubeconfig.encrypted.yaml
sops --encrypt talosconfig.yaml > talosconfig.encrypted.yaml
[doc("Decrypt talosconfig and kubeconfig")]
[working-directory('../infrastructure')]
decrypt_cluster_config:
sops --decrypt talosconfig.encrypted.yaml > talosconfig.yaml
sops --decrypt kubeconfig.encrypted.yaml > kubeconfig.yaml
chmod 600 talosconfig.yaml kubeconfig.yaml
[doc("Encrypt secrets.auto.tfvars")]
[working-directory('../infrastructure')]
encrypt_terraform_secrets:
sops --encrypt secrets.auto.tfvars > secrets.auto.encrypted.tfvars
[doc("Decrypt secrets.auto.tfvars")]
[working-directory('../infrastructure')]
decrypt_terraform_secrets:
sops --decrypt secrets.auto.encrypted.tfvars > secrets.auto.tfvars
chmod 600 secrets.auto.tfvars
[doc("Encrypt terraform.tfstate")]
[working-directory('../infrastructure')]
encrypt_terraform_state:
sops --encrypt --input-type json --output-type json terraform.tfstate > terraform.encrypted.tfstate
[doc("Decrypt terraform.tfstate")]
[working-directory('../infrastructure')]
decrypt_terraform_state:
# 如果加密状态文件尚不存在(新仓库),则静默跳过。
test -f terraform.encrypted.tfstate || exit 0
sops --decrypt --input-type json --output-type json terraform.encrypted.tfstate > terraform.tfstate
chmod 600 terraform.tfstate
``
我还将加密操作添加到了`create\_cluster\_config`引导命令中,这样我就更难忘记将它们添加到仓库中了:
``
[doc("Create talosconfig and kubeconfig")]
[working-directory('../infrastructure')]
create_cluster_config:
terraform output -raw talosconfig > talosconfig.yaml
terraform output -raw kubeconfig > kubeconfig.yaml
just secrets::encrypt_cluster_config
``
对于 terraform 状态也是如此(使用`just tf::apply`而不是普通的`terraform apply`):
``
[doc("terraform apply, then re-encrypt state (encrypts even on failure)")]
[working-directory('../infrastructure')]
apply *args:
#!/usr/bin/env bash
set -e
trap 'just secrets::encrypt_terraform_state' EXIT
terraform apply {{args}}
[doc("terraform destroy, then re-encrypt state (encrypts even on failure)")]
[working-directory('../infrastructure')]
destroy *args:
#!/usr/bin/env bash
set -e
trap 'just secrets::encrypt_terraform_state' EXIT
terraform destroy {{args}}
``
## Sealed Secrets (https://www.jonashietala.se/blog/2026/05/31/sops_age_and_sealed_secrets/#Sealed-Secrets)
现在继续看 sealed secrets。设置步骤比 SOPS + Age 多,但也不是*那么*糟糕。
### 安装 (https://www.jonashietala.se/blog/2026/05/31/sops_age_and_sealed_secrets/#Installation)
我将使用 helm 安装 sealed secrets 控制器:
``
helm install sealed-secrets \
--repo https://bitnami-labs.github.io/sealed-secrets \
sealed-secrets \
--version 2.16.2 \
--namespace sealed-secrets \
--create-namespace \
--set fullnameOverride=sealed-secrets-controller
# 等待部署完成
kubectl rollout status deployment/sealed-secrets-controller -n sealed-secrets
``
在客户端创建 secrets 还需要`kubeseal`命令。Void Linux 的包管理器里没有它,所以用笨方法安装:
``
set -x KUBESEAL_VERSION '0.36.1'
curl -OL "https://github.com/bitnami-labs/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
rm kubeseal-$KUBESEAL_VERSION-linux-amd64.tar.gz
``
### 创建 secret (https://www.jonashietala.se/blog/2026/05/31/sops_age_and_sealed_secrets/#Create-a-secret)
创建 secret 有两种方式:通过集群(需要活动连接)或通过证书离线创建。我更喜欢证书方式,因为需要传递的参数更少(`\-\-cert` vs `\-\-controller\-name` 和 `\-\-controller\-namespace`)。以下是获取证书的方法:
``
kubeseal --fetch-cert \
--controller-name=sealed-secrets-controller \
--controller-namespace=sealed-secrets \
> infrastructure/sealed-secrets-cert.pem
``
(这是公钥,可以安全提交到 git。)
以下是使用该证书生成包含`username`和`password`两个字段的 secret`my\-secret`的方法:
``
kubectl create secret generic my-secret \
--namespace some-namespace \
--from-literal=username="user" \
--from-literal=password="password1" \
--dry-run=client -o yaml \
| kubeseal --cert infrastructure/sealed-secrets-cert.pem \
--format yaml \
> gitops/apps/myapp/my-secret.yaml
``
(可能还有其他方法,比如生成 json 文件,但这样做没问题,我也懒得去研究。)
它将存储在`gitops/apps/myapp/my\-secret\.yaml`中,我们可以应用它:
``
kubectl apply -f gitops/apps/myapp/my-secret.yaml
``
将来当我们设置好 GitOps 后,流程是一样的,只是我们不需要手动`apply` secret;只需创建、提交并推送,它会自动被应用。虽然看起来有点繁琐,但日常使用中体验还不错。
更新 secret 的流程也完全相同:用新内容更新文件,然后重新应用。
### 查看 secret (https://www.jonashietala.se/blog/2026/05/31/sops_age_and_sealed_secrets/#View-a-secret)
检查 secret 是否已应用:
``
kubectl get secret my-secret -n some-namespace -o yaml
``
上面显示的`username`和`password`数据字段将是 base64 编码的。以下是明文输出密码的方法:
``
kubectl get secret my-secret -n some-namespace -o jsonpath='{.data.password}' | base64 -d
``
或者您可以在诸如Headlamp (https://headlamp.dev/)之类的仪表板中查看 secrets,这可以说更容易。
### 应对集群重置 (https://www.jonashietala.se/blog/2026/05/31/sops_age_and_sealed_secrets/#Surviving-a-cluster-reset)
Sealed secrets 有一个陷阱:当控制器安装时,它会生成新的公钥/私钥对,因此所有现有的 sealed secrets 都将失效。重置集群后,我们必须重新密封所有 secrets,这非常烦人。
我们可以通过导出私钥,用 SOPS + Age 加密,并存储在 git 中来规避这个问题。然后在引导过程中,我们可以将私钥导入控制器,使其能够重用所有现有的 sealed secrets。
首先导出密钥并用 SOPS 加密,以便将其保留在 git 中(将`sealed\-secrets\-key\.yaml`加入 gitignore):
``
kubectl get secret -n sealed-secrets \
-l sealedsecrets.bitnami.com/sealed-secrets-key=active -o yaml \
> sealed-secrets-key.yaml
sops --encrypt sealed-secrets-key.yaml > sealed-secrets-key.encrypted.yaml
``
这需要一个`\.sops\.yaml`规则:
``
creation_rules:
- path_regex: infrastructure/sealed-secrets-key(\.encrypted)?.yaml$
age: age1rrkgd5yza053qk9m8lp0ww39apdarz7w0rjyq85493g8l9gufgnq9cehzx
``
然后导入时只需`apply`它:
``
kubectl apply -f sealed-secrets-key.yaml
``
### Just 命令 (https://www.jonashietala.se/blog/2026/05/31/sops_age_and_sealed_secrets/#Just-commands-1)
我们在引导过程中添加了几个步骤来设置 sealed secrets 控制器:
``
[doc("Bootstrap everything from zero")]
full:
just bootstrap::cluster
just bootstrap::cilium
just secrets::restore_sealed_secrets_private_key
just bootstrap::sealed_secrets
``
``
[doc("Restore sealed-secrets-key.yaml")]
[working-directory('../infrastructure')]
restore_sealed_secrets_private_key:
# 如果加密私钥不存在,则跳过整个配方。
test -f sealed-secrets-key.encrypted.yaml || exit 0
just secrets::decrypt_sealed_secrets_private_key
# 如果命名空间已存在则不退出。
kubectl create namespace sealed-secrets || true
# 恢复私钥。
kubectl apply -f sealed-secrets-key.yaml
rm sealed-secrets-key.yaml
# 可能需要重启控制器,但如果控制器不存在也没关系。
kubectl rollout restart deployment/sealed-secrets-controller -n sealed-secrets || true
``
我尽量保护了配方,使其在尚未创建密钥或引导控制器时不会崩溃。
``
[doc("Install sealed secrets controller")]
sealed_secrets:
helm install sealed-secrets \
--repo https://bitnami-labs.github.io/sealed-secrets \
sealed-secrets \
--version 2.16.2 \
--namespace sealed-secrets \
--create-namespace \
--set fullnameOverride=sealed-secrets-controller
# 等待部署完成
kubectl rollout status deployment/sealed-secrets-controller -n sealed-secrets
``
以及一些额外的管理配方:
``
[doc("Fetch sealed-secrets-cert.pem, necessary to encrypt secrets offline")]
[working-directory('../infrastructure')]
fetch_sealed_secrets_cert:
kubeseal --fetch-cert \
--controller-name=sealed-secrets-controller \
--controller-namespace=sealed-secrets \
> sealed-secrets-cert.pem
[doc("Fetch sealed-secrets-key.yaml")]
[working-directory('../infrastructure')]
fetch_sealed_secrets_private_key:
kubectl get secret -n sealed-secrets -l sealedsecrets.bitnami.com/sealed-secrets-key=active -o yaml > sealed-secrets-key.yaml
just secrets::encrypt_sealed_secrets_private_key
rm sealed-secrets-key.yaml
[doc("Encrypt sealed-secrets-key.yaml")]
[working-directory('../infrastructure')]
encrypt_sealed_secrets_private_key:
sops --encrypt sealed-secrets-key.yaml > sealed-secrets-key.encrypted.yaml
[doc("Decrypt sealed-secrets-key.yaml")]
[working-directory('../infrastructure')]
decrypt_sealed_secrets_private_key:
sops --decrypt sealed-secrets-key.encrypted.yaml > sealed-secrets-key.yaml
``
这样,我们就为下一篇博文中使用ArgoCD (https://argo-cd.readthedocs.io/en/stable/)设置 GitOps 做好了准备。
相似文章
使用sops-nix在NixOS上管理机密
使用sops-nix在NixOS配置中管理机密的指南,涵盖设置、加密以及与Samba等服务的集成。
NixOS 与密钥管理
教程介绍 NixOS 的密钥管理选项,比较 sops-nix、agenix 和 ragenix 工具,并提供使用 sops-nix 进行加密密钥管理的实际示例。
部分密钥管理应交给 HTTP 代理
博客文章建议将 API 密钥注入工作卸载到内部 HTTP 代理,使应用和代理程序永远接触不到密钥,从而简化轮换并降低外泄风险。
你是如何让编码代理访问外部 API,同时不把原始密钥交给它们?
这是一场关于开发者如何为编码代理处理凭据的讨论,探讨了一种让代理无需接收原始密钥即可使用 API 的方案:在请求时注入凭据,并限制允许的目标地址。作者正将这一方案构建为 Stashbase 的一部分,并邀请大家分享各自的实践。
配置中不应包含秘密
本文认为秘密不应存储在配置文件中,通过对NixOS模块的审计显示,四分之一的模块需要变通方法才能将秘密与配置分离。它提倡为秘密使用专门的运行时通道。