SOPS + Age 和 Sealed Secrets

Lobsters Hottest 工具

摘要

一篇博客文章,解释如何将 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 做好了准备。

相似文章

NixOS 与密钥管理

Lobsters Hottest

教程介绍 NixOS 的密钥管理选项,比较 sops-nix、agenix 和 ragenix 工具,并提供使用 sops-nix 进行加密密钥管理的实际示例。

部分密钥管理应交给 HTTP 代理

Hacker News Top

博客文章建议将 API 密钥注入工作卸载到内部 HTTP 代理,使应用和代理程序永远接触不到密钥,从而简化轮换并降低外泄风险。

配置中不应包含秘密

Lobsters Hottest

本文认为秘密不应存储在配置文件中,通过对NixOS模块的审计显示,四分之一的模块需要变通方法才能将秘密与配置分离。它提倡为秘密使用专门的运行时通道。