site logo

Marico's space

在 AWS Secrets Manager 中存储 Kamal 密钥并部署到廉价的 Hetzner VPS

AI技术与应用 2026-05-24 12:02:51 6

最近折腾 Kamal 部署,踩了一个安全相关的坑。我的 .kamal/secrets 文件里全是明文的 API 密钥,放在笔记本上,谁拿到都能用,这明显不对劲。

直接说方案:用 Kamal 搭配 AWS Secrets Manager(Secrets Manager,AWS 的密钥托管服务)来管理密钥,部署目标选 Hetzner(欧洲的云服务商)的便宜 VPS。整个流程没有明文密钥,托管费用低,审计也能过。

问题是什么

Kamal 用来部署应用很方便,但默认情况下密钥存在明文文件里。过了等保三级或者要做 GDPR 合规,这种做法直接毙掉。必须上托管的密钥存储服务。

我选了 AWS Secrets Manager。但紧接着遇到另一个坑:用 kamal secrets fetch --adapter aws_secrets_manager 配合 --from 参数时,每个密钥都必须是独立的 AWS secret。如果你像我一样把所有密钥存成一个 JSON 对象,会直接报错:

ERROR (RuntimeError): myapp/production/secrets//DEEPGRAM_API_KEY: Secrets Manager can't find the specified secret.

第一步:Hetzner VPS

Hetzner 的 CAX 系列最低月付 4 欧元左右。我用的是 CX22,2 核 4G 内存,跑生产环境够用。

# 在 Hetzner 服务器上执行
apt update && apt install -y docker.io # 复制 SSH 公钥,方便 Kamal 连接服务器
ssh-copy-id root@你的服务器IP

deploy.yml 配置如下:

servers: web: hosts: - runtime.yourdomain.com proxy: ssl: true hosts: - runtime.yourdomain.com healthcheck: path: /health/ready registry: server: docker.io username: your-docker-user password: - KAMAL_REGISTRY_PASSWORD

还需要一个 Docker Hub 账号和 Personal Access Token 作为 KAMAL_REGISTRY_PASSWORD

第二步:在 AWS 创建密钥

打开 AWS Secrets Manager 控制台:

  1. 进入 Secrets Manager > Store a new secret
  2. 选择 "Other type of secret"
  3. 切到 plaintext 标签页,粘贴 JSON 内容
{ "DEEPGRAM_API_KEY": "your_deepgram_key", "ASSEMBLY_AI_API_KEY": "your_assemblyai_key", "REDIS_URL": "redis://:password@your-redis:6379", "KAMAL_REGISTRY_PASSWORD": "your_docker_token"
}
  1. 命名填 myapp/production/secrets
  2. 点 Store 保存

选离服务器近的区域。Hetzner 机器在德国就用 eu-central-1(法兰克福),延迟低,GDPR 也合规。

第三步:为笔记本创建 IAM 用户

部署时笔记本需要读取密钥的权限。

  1. 进入 IAM > Users > Create user
  2. 用户名填 kamal-deploy
  3. 取消控制台访问(只用 CLI)
  4. 建一个组叫 secrets-manager,绑定 SecretsManagerReadWrite 策略
  5. 添加内联策略,支持批量读取:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret", "secretsmanager:BatchGetSecretValue", "secretsmanager:ListSecrets" ], "Resource": "*" } ]
}
  1. 把用户加到这个组里

IAM 策略生效有延迟,首次失败就等半分钟再试。

第四步:配置 AWS CLI

aws configure
# AWS Access Key ID: 粘贴 IAM 用户的访问密钥 ID
# AWS Secret Access Key: 粘贴访问密钥
# Default region name: eu-central-1
# Default output format: json

测试一下:

aws secretsmanager get-secret-value --secret-id myapp/production/secrets --query SecretString --output text | head -c 50

能看到 JSON 的开头就说明配置没问题。

第五步:格式化 .kamal/secrets 文件

这里踩过坑。--from 参数要求每个密钥对应一个独立的 AWS secret。如果有 20 个密钥就得建 20 个 secret,管理起来烦死人。

我的解法是用 AWS CLI 配合 Python 提取,每行独立完成:

# AWS Secrets Manager: myapp/production/secrets (eu-central-1)
DEEPGRAM_API_KEY=$(python3 -c "import json,sys; print(json.loads(sys.argv[1])['DEEPGRAM_API_KEY'])" "$(aws secretsmanager get-secret-value --secret-id myapp/production/secrets --query SecretString --output text)")
ASSEMBLY_AI_API_KEY=$(python3 -c "import json,sys; print(json.loads(sys.argv[1])['ASSEMBLY_AI_API_KEY'])" "$(aws secretsmanager get-secret-value --secret-id myapp/production/secrets --query SecretString --output text)")
REDIS_URL=$(python3 -c "import json,sys; print(json.loads(sys.argv[1])['REDIS_URL'])" "$(aws secretsmanager get-secret-value --secret-id myapp/production/secrets --query SecretString --output text)")
KAMAL_REGISTRY_PASSWORD=$(python3 -c "import json,sys; print(json.loads(sys.argv[1])['KAMAL_REGISTRY_PASSWORD'])" "$(aws secretsmanager get-secret-value --secret-id myapp/production/secrets --query SecretString --output text)")

每行都取完整的 JSON 再提取其中一条密钥。Kamal 在独立子 shell 里求值每行,所以行与行之间没有共享变量。这个写法亲测能用。

如果习惯用 jq 也可以:

DEEPGRAM_API_KEY=$(aws secretsmanager get-secret-value --secret-id myapp/production/secrets --query SecretString --output text | jq -r '.DEEPGRAM_API_KEY')

第六步:部署

kamal deploy

Kamal 会在部署时从 AWS 取密钥,注入到容器里。服务器上永远不会出现明文密钥文件。

生产环境和预发环境

我的做法是每个环境对应一个独立的 AWS secret。所有环境都从 AWS 读取,不存在任何明文。

# .kamal/secrets (执行 kamal deploy 时使用)
DEEPGRAM_API_KEY=$(python3 -c "import json,sys; print(json.loads(sys.argv[1])['DEEPGRAM_API_KEY'])" "$(aws secretsmanager get-secret-value --secret-id myapp/production/secrets --query SecretString --output text)")
KAMAL_REGISTRY_PASSWORD=$(python3 -c "import json,sys; print(json.loads(sys.argv[1])['KAMAL_REGISTRY_PASSWORD'])" "$(aws secretsmanager get-secret-value --secret-id myapp/production/secrets --query SecretString --output text)") # .kamal/secrets.staging (执行 kamal deploy -d staging 时使用)
DEEPGRAM_API_KEY=$(python3 -c "import json,sys; print(json.loads(sys.argv[1])['DEEPGRAM_API_KEY'])" "$(aws secretsmanager get-secret-value --secret-id myapp/staging/secrets --query SecretString --output text)")
KAMAL_REGISTRY_PASSWORD=$(python3 -c "import json,sys; print(json.loads(sys.argv[1])['KAMAL_REGISTRY_PASSWORD'])" "$(aws secretsmanager get-secret-value --secret-id myapp/staging/secrets --query SecretString --output text)")

两个文件只有 secret 名称不同。生产用 myapp/production/secrets,预发用 myapp/staging/secrets。执行 kamal deploy -d staging,Kamal 就会读取 staging 对应的密钥文件。

预发环境的密钥同样存在 AWS 里,不会出现明文。等保三级审计的时候每个环境都要查一遍,这点必须做到。

搞定收工

密钥不再明文存储,等保和 GDPR 要求都满足,Hetzner 的账单一直压在每月 5 欧元以下。

踩过的坑分享出来,希望你少走弯路。回去写代码了。

原文链接:https://dev.to/techstorytitle/storing-kamal-secrets-in-aws-secrets-manager-and-deploying-to-a-cheap-hetzner-vps-4f1l