
你的 DB 密碼放在哪裡?環境變數?
.env檔?git repo 裡?後兩個是資安破口大家都知道。但搬進 Secrets Manager 之後,多數人以為做完了——其實才做了一半。
結論:分兩類、兩個服務、然後三件事會咬你
分兩類:會讓人進門的(DB 密碼、第三方 API key、OAuth secret、TLS 私鑰)放 Secrets Manager;只是設定的(feature flag、endpoint URL、逾時秒數)放 SSM Parameter Store。前者能自動輪替、能跨帳號分享;後者輕、大多免費、機敏的用 SecureString 也有加密。
然後三件事,每一件都是官方文件白紙黑字寫著、但多數教學沒講的:
- 秘密是容器初次啟動時注入的,輪替後跑著的 task 不會自動拿到新值
- 注入成環境變數的秘密,應用程式、容器 log、除錯工具都看得到
- private subnet 的 task 要拉秘密,得經過 NAT 或 Interface endpoint,沒有就起不來
一、放進去:不要把密碼寫在指令裡
到處都看得到的範例是 --secret-string '{"password":"SuperSecretPass123!"}'——那一行會進 shell history、會進 CloudTrail 的參數紀錄。三種不會落地的寫法:
# RDS 的密碼:讓 RDS 自己生成、自己存進 Secrets Manager,你從頭到尾不碰
aws rds create-db-instance ... --manage-master-user-password
# 第三方給你的 key:從檔案讀,讀完刪檔
aws secretsmanager create-secret --name prod/payment/api-key \
--secret-string file://./key.json && shred -u ./key.json
# 自己要生成的:讓 Secrets Manager 生成
aws secretsmanager create-secret --name prod/app/session-key \
--generate-secret-string '{"PasswordLength":64,"ExcludePunctuation":true}'命名用路徑分層,之後 IAM policy 才能用前綴給權限:
/prod/
database/postgres → Secrets Manager(自動輪替)
payment/api-key → Secrets Manager(第三方給的,設到期提醒)
app/feature-flags/* → Parameter Store(String)
app/smtp-password → Parameter Store(SecureString)二、注入到 ECS:secrets 欄位,不是 environment
"secrets": [
{"name": "DB_PASSWORD",
"valueFrom": "arn:aws:secretsmanager:ap-northeast-1:123456789012:secret:prod/database/postgres-AbCdEf:password::"},
{"name": "FEATURE_DARK_MODE",
"valueFrom": "arn:aws:ssm:ap-northeast-1:123456789012:parameter/prod/app/feature-flags/dark-mode"}
]ARN 後面那串 :password:: 是「只取 JSON 裡的 password 這個 key、用目前版本」。完整格式是 secret:名稱:json-key:version-stage:version-id,不用的段落要留冒號,這是最常打錯的地方。
注入用的是 Execution Role(ECS 自己的身分),不是 Task Role。Execution Role 要有 secretsmanager:GetSecretValue;用自訂 KMS key 加密的秘密還要 kms:Decrypt——漏了第二個,task 起不來、錯誤訊息是 AccessDenied,而你會先去懷疑 Task Role,因為那是你的程式用的。兩個 role 分清楚,見 第 12 篇第二層。
三、輪替之後,跑著的 task 還在用舊密碼
這是我整理架構卡時對到官方文件才注意到的。原文:
「敏感資料在容器初次啟動時注入。如果之後秘密被更新或輪替,容器不會自動收到新值。你必須啟動新 task,或者更新 service 並使用 Force new deployment 選項。」
翻成場景:你開了 Secrets Manager 的自動輪替,三十天到了,密碼換成新的、資料庫也接受新的了——你的 50 個 task 還握著舊密碼。它們不會馬上壞,因為已經建立的連線還在;下一次重連(連線池回收、task 重啟、擴容起新 task 用的是新密碼、舊的還在用舊的)才開始間歇性 401。時間差可能是幾天,那時候沒人會想到是輪替造成的。
輪替一定要配重啟:
# 輪替完成 → EventBridge 收到 Secrets Manager 的事件 → 觸發這一行
aws ecs update-service --cluster prod-cluster --service web-app --force-new-deployment或者反過來:程式不靠環境變數,執行時直接向 Secrets Manager 拉,SDK 快取幾分鐘。這樣輪替不用重啟,代價是程式碼要改、每個 task 啟動多一次 API 呼叫。
沒有自動輪替的秘密(第三方給的 key)反而更容易漏:它不會提醒你,到期那天才知道。把到期日記在 secret 的 tag 上,設提醒——見 第 12 篇的到期清單。
四、環境變數看得到,合規高的要換路
同一頁文件的第二條:「在容器上執行的應用程式、容器 log、除錯工具都能存取這些環境變數。」
翻譯:docker inspect 印得出來、程式一不小心把整個 process.env 印進 log 就洩了、能 ECS Exec 進容器的人就看得到。對多數業務可以接受;碰卡號、碰個資的服務不行。
AWS 自己的建議是走 sidecar:一個 init 容器從 Secrets Manager 讀秘密、寫進 task 內的 volume、退出;應用容器再從 volume 讀。volume 綁 task 生命週期,task 停就消失。這條路程式碼要改成讀檔,不是讀環境變數。
五、Private subnet 的 task 拉不到秘密
文件明講:要用 Interface VPC endpoint 讓 private subnet 連到 Secrets Manager。沒有 endpoint 的話流量走 NAT——沒有 NAT 也沒有 endpoint,task 就卡在拉秘密那一步,起不來。錯誤訊息是逾時,不是權限,很容易跟 第 2 篇的 Route Table 那個坑混在一起。
開 endpoint 之前記得它是 Interface 型、按小時和 AZ 計費(第 11 篇);流量小的環境走 NAT 反而便宜。
六、身分:不發 key,用 role
秘密管好了,另一半是「程式怎麼證明自己是誰」。答案永遠是 role,不是 access key:
- ECS:Task Role。程式用 SDK 自動拿臨時憑證,沒有 key 可以外洩。
- EKS:IRSA(IAM Roles for Service Accounts)——ServiceAccount 綁 IAM Role,Pod 自動拿臨時憑證。較新的 EKS Pod Identity 做同一件事、設定更簡單,新 cluster 可以直接用它。
spec:
serviceAccountName: s3-reader # 綁到 IAM Role,不需要 AWS_ACCESS_KEY_ID如果現在還有服務在用長期 access key:AWS 不會讓它過期,它永遠不會提醒你。用 IAM 的金鑰年齡報表找出來,一個一個換成 role。
這篇的驗證標準
| 主張 | ❌ 不算 | ✅ 算 |
|---|---|---|
| 秘密不落地 | task definition 用了 secrets | 在 git、image、CLI history、CloudTrail 參數裡搜密碼字串,零命中 |
| 輪替不會炸 | 開了自動輪替 | 手動觸發一次輪替,確認 force-new-deployment 有跑、新 task 連得上 |
| 沒人在用 key | 「我們都用 role」 | aws iam list-access-keys 掃全部使用者,沒有 90 天以上的 key |
| 環境變數沒洩 | 程式沒印 env | CloudWatch log 裡搜 DB_PASSWORD 的值,零命中 |
實戰 Checklist
- 密碼從不出現在 CLI 參數裡:RDS 用
--manage-master-user-password,其他用file://或--generate-secret-string - Execution Role 有
secretsmanager:GetSecretValue和(自訂 key 時)kms:Decrypt - 自動輪替接 force-new-deployment;第三方 key 的到期日記在 tag 上
- private subnet 有 Secrets Manager 的 Interface endpoint 或走 NAT
- 沒有長期 access key;EKS 用 IRSA 或 Pod Identity
從 K8s 過來的話
K8s Secret 是 base64 不是加密,這你知道。AWS 這邊 Secrets Manager 真的加密、真的能輪替——但**「輪替後 Pod 拿不到新值」這件事跟 K8s 一模一樣**:掛成環境變數的 Secret 改了 Pod 也不會知道,要 rollout restart。差別只在 AWS 有官方的 force-new-deployment 按鈕。
在 EKS 上最順的做法是 External Secrets Operator:把 Secrets Manager 的秘密同步成 K8s Secret,Pod 照 K8s 的方式讀,輪替時 operator 幫你更新——但 Pod 還是要重啟才吃到,同一個坑。細節在 K8s 配置管理。
相關文章
- 上一篇 X-Ray,下一篇 EventBridge
- 兩個 role 與人的身分:安全;endpoint 的費用:成本管理
- 概念層:Secrets & Config、Secrets & Certificate Management
你的系統有多安全,取決於你最弱的那個秘密怎麼管理——而最弱的那個,通常是輪替過但沒人重啟的那個。
