
最近折腾 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 的 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 Secrets Manager 控制台:
{ "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"
}
myapp/production/secrets选离服务器近的区域。Hetzner 机器在德国就用 eu-central-1(法兰克福),延迟低,GDPR 也合规。
部署时笔记本需要读取密钥的权限。
kamal-deploysecrets-manager,绑定 SecretsManagerReadWrite 策略{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret", "secretsmanager:BatchGetSecretValue", "secretsmanager:ListSecrets" ], "Resource": "*" } ]
}
IAM 策略生效有延迟,首次失败就等半分钟再试。
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 的开头就说明配置没问题。
这里踩过坑。--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 欧元以下。
踩过的坑分享出来,希望你少走弯路。回去写代码了。