AWS IAM Access Key rotation: Lambda + Secrets Manager

Một giải pháp AWS-native để rotate, disable và delete IAM access key theo chính sách: đi sâu vào kiến trúc nhiều account, đánh đổi và vận hành thực tế.

· 11 phút đọc · Read in English

TL;DR

  • Stack AWS-native: Lambda + EventBridge + Secrets Manager + SES + StackSets — không thêm DynamoDB, không Step Functions, đọc thẳng IAM làm nguồn sự thật.
  • State machine ACTIVE → ROTATED → DISABLED → DELETED với mặc định RotationPeriod=90, InactivePeriod=100, RecoveryGracePeriod=10 ngày — giữ cửa sổ rollback khi pipeline gãy.
  • Luôn bật DryRunFlag=True trước, chạy ít nhất 2 tuần để phát hiện CI pipeline còn hardcode key cũ.
  • Member account chỉ có iam:ListUsers, iam:CreateAccessKey, iam:UpdateAccessKey, iam:DeleteAccessKey + Secrets Manager scope theo prefix Account_*_User_*_AccessKey — không iam:*, không iam:PassRole.
  • Exemption group là cửa sau tiềm năng: bật CloudTrail alarm trên iam:AddUserToGroup cho IAMKeyRotationExemptionGroup và rà soát hàng quý.
  • Rotation không thay thế việc bỏ hẳn IAM user — đặt KPI quý giảm số IAM user, không chỉ giảm tuổi key trung bình.
  • Secrets Manager $0.40/secret/tháng cộng dồn lớn ở quy mô nghìn user; cân nhắc xoá secret sau khi owner đã rotate xong.

IAM access key là một trong những thứ dễ rò rỉ nhất trong AWS: nó nằm trong dotfiles của dev, trong CI runner, trong ảnh Docker, trong Postman workspace, trong Slack DM. Khi một tổ chức có hàng chục account và hàng trăm IAM user, việc rotate key thủ công theo định kỳ gần như không ai làm. Bài này mô tả một giải pháp AWS-native (Lambda + EventBridge + Secrets Manager + SES + CloudFormation StackSets) để tự động rotate, disable, delete key theo chính sách, và quan trọng hơn: những đánh đổi bạn cần hiểu trước khi triển khai vào production. Repo tham khảo: vanhoangkha/aws-iam-access-key-auto-rotation.

Bối cảnh

Trong một AWS Organization điển hình mình thường thấy:

  • 20–100 AWS account, quản lý theo Control Tower hoặc landing zone tự dựng.
  • Phần lớn workload đã chuyển sang IAM role (EC2 instance profile, IRSA, Lambda execution role). Nhưng vẫn còn một nhóm IAM user dai dẳng: service user cho CI/CD cũ, SaaS bên thứ ba cần truy cập lập trình, tool của nhà cung cấp không hỗ trợ OIDC, hoặc dev dùng access key local vì ngại thiết lập SSO profile.
  • Mỗi account trung bình có 5–20 IAM user với access key còn hoạt động. Tuổi trung bình của key: >180 ngày.
  • Các framework tuân thủ (ISO 27001, SOC 2, PCI-DSS, CIS AWS Benchmark 1.14) đều yêu cầu rotate thông tin xác thực định kỳ: thường là 90 ngày.

Mục tiêu mà giải pháp này giải quyết: đưa con số “tuổi key trung bình” xuống dưới 90 ngày mà không cần con người can thiệp, và khi có sự cố thì có khoảng grace để khôi phục.

Vấn đề

Rotate access key bằng tay có 3 điểm đau cụ thể:

  1. Không biết ai đang dùng key ở đâu. Xoá key, pipeline CI gãy, khôi phục, không ai dám xoá lần nữa. Hệ quả: key 3 năm tuổi vẫn còn hoạt động.
  2. Chạy rải rác qua nhiều account. Admin phải assume role vào từng account, liệt kê user, tạo key mới, gửi cho người phụ trách, đợi họ xác nhận, rồi mới disable key cũ. Không mở rộng được.
  3. Thiếu audit trail. Khi kiểm toán viên hỏi “ai rotate key này, khi nào, có thông báo cho người phụ trách không?”, không có log có cấu trúc để trả lời.

Những giải pháp nửa vời không đủ:

  • IAM Access Analyzer, unused access finder: chỉ phát hiện key không dùng, không rotate.
  • Secrets Manager rotation cho RDS/Aurora: tích hợp sẵn, nhưng IAM access key không có mẫu rotation tích hợp sẵn tương tự: phải tự viết Lambda.
  • Chỉ bỏ hẳn IAM user, chuyển 100% sang IAM Identity Center (SSO) + IAM role: đúng hướng, nhưng chuyển đổi mất hàng quý và bạn vẫn cần giải pháp trung gian cho khoảng thời gian đó.

Giải pháp mô tả ở đây lấp khoảng trống đó: nó giả định bạn vẫn còn IAM user, và tự động hoá vòng đời key cho tới khi bạn xoá được user đó.

Kiến trúc

Kiến trúc IAM access key auto-rotation: EventBridge cron trong security account trigger Lambda inventory để list account từ Organizations, fan-out sang Lambda rotation engine. Rotation engine AssumeRole vào member account, dùng IAM để rotate key và Secrets Manager để lưu key mới, rồi SES notify người phụ trách và security ops.

Thành phầnMục đíchCông nghệ
Kiểm kê accountLiệt kê mọi account trong Organization, fan-outLambda (Python 3.13) + Organizations API
Engine rotationQuyết định rotate/disable/delete theo tuổi keyLambda (Python 3.13) + IAM API
Lưu trữ keyLưu key mới sau khi tạo, có mã hóaAWS Secrets Manager (KMS CMK)
Thông báoGửi email cho người phụ trách + admin trước/sau mỗi hành độngAmazon SES + template HTML
SchedulerKích hoạt hằng ngàyEventBridge rule (cron)
Truy cập liên accountAssume role vào member accountIAM role (ExecutionRole) được triển khai qua StackSet
Miễn trừLoại trừ service user đặc biệtIAM group IAMKeyRotationExemptionGroup
Kiểm toánLog mọi hành độngCloudWatch Logs + CloudTrail (mặc định)

State machine vòng đời

Trạng thái của một access key đi qua 4 mốc: ACTIVE → ROTATED → DISABLED → DELETED, điều khiển bằng 3 tham số RotationPeriod, InactivePeriod, RecoveryGracePeriod.

Vòng đời của một access key: bắt đầu ở ACTIVE, đến RotationPeriod (mặc định 90 ngày) thì sang ROTATED và có key mới lưu trong Secrets Manager, sau InactivePeriod thì key cũ bị DISABLED nhưng vẫn bật lại được, hết grace period thì DELETED vĩnh viễn.

Ba tham số có thể chỉnh qua CloudFormation parameter:

  • RotationPeriod = 90 ngày
  • InactivePeriod = 100 ngày
  • RecoveryGracePeriod = 10 ngày (khoảng giữa disable và delete)

Cửa sổ 10 ngày giữa “disable” và “delete” là lưới an toàn, nếu user vẫn đang hardcode key cũ ở đâu đó, pipeline của họ sẽ fail ngay ở ngày 100 thay vì chờ đến khi key bị xoá vĩnh viễn. Bật lại key bị disable nhanh hơn nhiều so với tạo lại user.

Triển khai

Toàn bộ giải pháp triển khai bằng CloudFormation. Repo có 4 template chính:

  • ASA-iam-key-auto-rotation-and-notifier-solution.yaml: template chính, triển khai ở security/audit account
  • ASA-iam-key-auto-rotation-iam-assumed-roles.yaml: role liên account, triển khai qua StackSet ra tất cả member account
  • ASA-iam-key-auto-rotation-list-accounts-role.yaml: role đọc Organizations API, triển khai ở management account
  • ASA-iam-key-auto-rotation-vpc-endpoints.yaml: tuỳ chọn, nếu Lambda chạy trong VPC

1. Chuẩn bị SES identity

SES ở chế độ sandbox chỉ gửi được tới email đã verify. Nếu dùng cho production, yêu cầu production access trước.

aws ses verify-email-identity \
  --email-address security-ops@example.com \
  --region us-east-1

2. Upload artifact lên S3

Lambda package và template lấy từ S3, không nhúng inline. Tạo bucket ở security account:

export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
export BUCKET_NAME="asa-iam-rotation-${AWS_ACCOUNT_ID}-us-east-1"

aws s3 mb "s3://$BUCKET_NAME" --region us-east-1
aws s3 cp Lambda/    "s3://$BUCKET_NAME/asa/asa-iam-rotation/Lambda/"   --recursive
aws s3 cp template/  "s3://$BUCKET_NAME/asa/asa-iam-rotation/Template/" --recursive

3. Triển khai execution role ra tất cả member account

Dùng CloudFormation StackSet với mô hình quyền service-managed (yêu cầu Organizations + trusted access cho CloudFormation đã bật):

aws cloudformation create-stack-set \
  --stack-set-name asa-iam-rotation-member-role \
  --template-body file://CloudFormation/ASA-iam-key-auto-rotation-iam-assumed-roles.yaml \
  --permission-model SERVICE_MANAGED \
  --auto-deployment Enabled=true,RetainStacksOnAccountRemoval=false \
  --capabilities CAPABILITY_NAMED_IAM

aws cloudformation create-stack-instances \
  --stack-set-name asa-iam-rotation-member-role \
  --deployment-targets OrganizationalUnitIds=ou-xxxx-yyyyyyyy \
  --regions us-east-1

Role này có policy tối thiểu: iam:ListUsers, iam:ListAccessKeys, iam:CreateAccessKey, iam:UpdateAccessKey, iam:DeleteAccessKey, iam:GetGroup, và secretsmanager:CreateSecret/PutSecretValue giới hạn theo prefix. Không có iam:*.

4. Triển khai stack lõi: bật DryRun trước

aws cloudformation deploy \
  --template-file CloudFormation/ASA-iam-key-auto-rotation-and-notifier-solution.yaml \
  --stack-name iam-key-auto-rotation \
  --capabilities CAPABILITY_NAMED_IAM \
  --parameter-overrides \
    S3BucketName="$BUCKET_NAME" \
    S3BucketPrefix="asa/asa-iam-rotation" \
    AdminEmailAddress="security-ops@example.com" \
    AWSOrgID="o-xxxxxxxxxx" \
    OrgListAccount="111111111111" \
    DryRunFlag="True" \
    RotationPeriod="90" \
    InactivePeriod="100"

DryRunFlag=Truebắt buộc cho lần chạy đầu tiên. Ở chế độ này Lambda sẽ:

  • Liệt kê user và key
  • Tính tuổi từng key
  • Gửi email mô phỏng hành động sẽ thực hiện
  • Không tạo/sửa/xoá gì trên IAM

Đây là cơ hội để phát hiện: có bao nhiêu key vượt ngưỡng, user nào sẽ bị ảnh hưởng, email người phụ trách đã verify chưa.

5. Thử một account cụ thể

Gọi trực tiếp để không phải chờ EventBridge cron:

aws lambda invoke \
  --function-name ASA-IAM-Access-Key-Rotation-Function \
  --payload '{"account":"222222222222","email":"owner@example.com","name":"prod-workload"}' \
  --cli-binary-format raw-in-base64-out \
  /tmp/out.json

cat /tmp/out.json

Kiểm tra CloudWatch Logs của function xem log quyết định rotation:

[INFO] User: deploy-bot
[INFO] Oldest active key: AKIA...  age=137d
[INFO] Decision: DISABLE (age >= InactivePeriod=100)
[INFO] DryRun=True → skipping iam:UpdateAccessKey

6. Chuyển sang bắt buộc thực thi

Khi đã xác minh kết quả DryRun:

aws cloudformation update-stack \
  --stack-name iam-key-auto-rotation \
  --use-previous-template \
  --capabilities CAPABILITY_NAMED_IAM \
  --parameters \
    ParameterKey=DryRunFlag,ParameterValue=False \
    ParameterKey=S3BucketName,UsePreviousValue=true \
    ParameterKey=S3BucketPrefix,UsePreviousValue=true \
    ParameterKey=AdminEmailAddress,UsePreviousValue=true \
    ParameterKey=AWSOrgID,UsePreviousValue=true \
    ParameterKey=OrgListAccount,UsePreviousValue=true

7. Lấy key mới đã được tạo

Key mới được lưu ở member account:

aws secretsmanager get-secret-value \
  --secret-id Account_222222222222_User_deploy-bot_AccessKey \
  --query SecretString --output text

Secret này được mã hóa bằng KMS. Chỉ role có quyền kms:Decrypt trên CMK tương ứng mới đọc được, thường là bản thân người phụ trách hoặc Lambda trong account đó.

Cân nhắc bảo mật

Một giải pháp rotate thông tin xác thực tự nó là một mục tiêu giá trị cao. Nếu kẻ tấn công chiếm được Lambda rotation, họ có thể: tạo key mới cho bất kỳ IAM user nào, xoá key đang dùng (DoS), hoặc đọc secret chứa key vừa tạo. Nên khi rà soát giải pháp này, mình kiểm tra các bề mặt sau:

  • Ít quyền nhất trên ExecutionRole. Role ở member account chỉ có IAM write phạm vi hẹp + Secrets Manager giới hạn theo prefix Account_*_User_*_AccessKey. Không cho iam:PassRole, iam:AttachPolicy, iam:CreateUser. Nếu có thể, giới hạn thêm bằng điều kiện aws:PrincipalArn để chỉ Lambda role ở security account mới assume được.
  • Không log secret vào CloudWatch. Trong code Lambda, kiểm tra kỹ không print() access key secret. CloudWatch logs mặc định có thời gian lưu trữ dài và có thể đồng bộ sang SIEM: rò rỉ ở đây là rò rỉ thật.
  • KMS CMK thay vì AWS-managed key. Dùng CMK để có log truy cập (sự kiện CloudTrail kms:Decrypt) và policy kiểm soát ai chia sẻ lại được secret.
  • Exemption group không phải cửa sau. IAMKeyRotationExemptionGroup là cơ chế tiện lợi nhưng cũng là cách để qua mặt rotation. Bật cảnh báo CloudTrail cho iam:AddUserToGroup khi group đích là exemption group, và đưa vào rà soát tuân thủ định kỳ.
  • VPC endpoint khi thật sự cần. Template vpc-endpoints.yaml triển khai endpoint cho IAM, Secrets Manager, STS, SES, CloudWatch Logs: để Lambda chạy trong VPC không cần ra internet. Hữu ích cho account có giới hạn egress; thừa với account đơn giản.
  • Grace period là bắt buộc. Nếu đặt InactivePeriod == RecoveryGracePeriod + RotationPeriod (tức disable và delete cùng ngày), mọi lỗi cấu hình sẽ thành gián đoạn dịch vụ. 10 ngày là số mặc định hợp lý; có nhóm mình thấy đặt 14–30 ngày cho production.

Những thứ giải pháp này không bảo vệ:

  • Rò rỉ key qua kênh không phải AWS (GitHub public repo, Slack, ảnh chụp màn hình). Cần GitHub secret scanning + Access Analyzer unused finder song song.
  • Key bị xâm nhập giữa 2 lần rotate. Rotation 90 ngày nghĩa là một key bị lộ có thể được dùng tối đa 90 ngày trước khi bị thay: đây là thoả hiệp, không phải biện pháp giảm thiểu.
  • IAM user tồn tại mà không ai biết (shadow user do admin cũ tạo). Đây là bài toán của kiểm kê IAM & rà soát access, không phải rotation.

Vận hành

Cron chạy hằng ngày, không phải thời gian thực. Thiết lập giám sát như sau:

Cảnh báo CloudWatch bắt buộc:

  • Tỉ lệ lỗi của Lambda rotation > 0 trong 24h → pager on-call.
  • Duration > 50% timeout → fan-out quá chậm, nghi vấn throttling IAM API.
  • Metric Throttles > 0 → cần tăng reserved concurrency hoặc chia nhỏ batch.

Báo cáo định kỳ:

  • Mỗi tuần, xuất danh sách user có key vượt RotationPeriod nhưng nằm trong exemption group → rà soát ngược lại: có còn cần miễn trừ không?
  • Mỗi tháng, báo cáo số key đã rotate / disable / delete → đưa vào dashboard tuân thủ.

Runbook khi có sự cố:

  • “Pipeline X fail sau khi key bị disable” → bật lại key cũ bằng iam:UpdateAccessKey Status=Active, đồng thời gửi key mới từ Secrets Manager cho người phụ trách, thúc ép chuyển đổi trước khi RecoveryGracePeriod hết hạn.
  • “Lambda rotation không chạy hôm nay” → kiểm tra trạng thái EventBridge rule, kiểm tra quota Lambda concurrent. Giải pháp idempotent: bỏ qua một ngày sẽ không hỏng gì, key chỉ bị rotate trễ.
  • “Email SES bounce” → tỉ lệ bounce cao sẽ khiến uy tín SES rớt; verify email người phụ trách trước khi áp dụng, và có phương án dự phòng tới alias nhóm.

Đừng xem rotation là bật xong rồi quên. Giá trị của giải pháp nằm ở chỗ nó biến mỗi key thành một sự kiện có thể nhận biết được. Nếu không ai đọc email, không ai nhìn dashboard, key vẫn rotate nhưng khi có sự cố bạn không truy được ai động vào cái gì.

Đánh đổi

Quyết địnhLựa chọnMình chọn
Nguồn sự thậtBảng trạng thái DynamoDB vs. đọc trực tiếp IAM mỗi lầnĐọc IAM trực tiếpÍt hạ tầng hơn; IAM là nguồn sự thật thật, không cần đồng bộ
Lưu key mớiSecrets Manager vs. SSM Parameter StoreSecrets ManagerCó API rotation, kiểm toán tích hợp sẵn, KMS per-secret, chi phí chấp nhận được
SchedulerEventBridge cron vs. Step FunctionsEventBridge + fan-out LambdaĐơn giản; không cần state machine cho xử lý 1 bước
Thông báoSES vs. email SNS vs. Slack webhookSESTemplate HTML tuỳ biến, không bắt buộc subscribe, phù hợp nội dung nhạy cảm
Nhiều accountSTS AssumeRole vs. Lambda triển khai per-accountAssumeRole từ security account1 đường code, 1 log group, 1 cảnh báo, dễ vận hành hơn N bản sao
Mặc định DryRunMặc định True vs. mặc định FalseMặc định TrueAn toàn; buộc admin đọc báo cáo trước khi áp dụng
Miễn trừTag trên IAM user vs. IAM groupIAM groupGroup có audit trail rõ hơn khi thêm/bớt user; tag dễ bị sửa lén
Ngôn ngữPython 3.13 vs. TypeScriptPython 3.13boto3 đầy đủ nhất, cộng đồng AWS có sẵn mẫu code

Bài học

Sau khi triển khai và chạy một thời gian, những thứ mình sẽ làm khác lần sau:

  • Dành 2 tuần DryRun đầy đủ, không phải 2 ngày. Lần đầu mình bật áp dụng sớm và phát hiện có khoảng 7 pipeline CI vẫn dùng key cũ mà không ai nhớ: khôi phục và giải thích với nhóm mất vui hơn là chờ thêm.
  • Rotation tốt hơn là không có rotation, nhưng không thay thế được việc bỏ hẳn IAM user. Mỗi quý nên đặt KPI giảm X% số IAM user, thay vì chỉ giảm tuổi key trung bình. Cái trước mới là tiến bộ thật.
  • Exemption group càng nhỏ càng tốt. Mình đặt quy tắc: mỗi user trong exemption phải có ticket giải trình và ngày hết hạn; tự động gỡ hàng quý, buộc người phụ trách giải trình lại.
  • Gửi email không đủ. Người phụ trách bỏ qua email nội bộ là chuyện thường. Tích hợp thêm Slack DM hoặc Jira ticket tự tạo khi key vượt RotationPeriod - 14 → người ta mới hành động.
  • Đo chi phí ngay từ đầu. Lambda + Secrets Manager ở quy mô nhỏ gần như miễn phí, nhưng nếu tổ chức có hàng nghìn IAM user, Secrets Manager ($0.40/secret/tháng) cộng dồn không nhỏ. Cân nhắc vòng đời xoá secret sau khi người phụ trách đã rotate sang key mới hoàn toàn.

Tham khảo