cover

你的 DB 密碼放在哪裡?環境變數?.env 檔?git repo 裡?後兩個是資安破口大家都知道。但搬進 Secrets Manager 之後,多數人以為做完了——其實才做了一半。

結論:分兩類、兩個服務、然後三件事會咬你

分兩類:會讓人進門的(DB 密碼、第三方 API key、OAuth secret、TLS 私鑰)放 Secrets Manager;只是設定的(feature flag、endpoint URL、逾時秒數)放 SSM Parameter Store。前者能自動輪替、能跨帳號分享;後者輕、大多免費、機敏的用 SecureString 也有加密。

然後三件事,每一件都是官方文件白紙黑字寫著、但多數教學沒講的:

  1. 秘密是容器初次啟動時注入的,輪替後跑著的 task 不會自動拿到新值
  2. 注入成環境變數的秘密,應用程式、容器 log、除錯工具都看得到
  3. 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
環境變數沒洩程式沒印 envCloudWatch 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 配置管理。

相關文章


你的系統有多安全,取決於你最弱的那個秘密怎麼管理——而最弱的那個,通常是輪替過但沒人重啟的那個。