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=10ngày — giữ cửa sổ rollback khi pipeline gãy.- Luôn bật
DryRunFlag=Truetrướ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 prefixAccount_*_User_*_AccessKey— khôngiam:*, khôngiam:PassRole.- Exemption group là cửa sau tiềm năng: bật CloudTrail alarm trên
iam:AddUserToGroupchoIAMKeyRotationExemptionGroupvà 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ể:
- 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.
- 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.
- 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
| Thành phần | Mục đích | Công nghệ |
|---|---|---|
| Kiểm kê account | Liệt kê mọi account trong Organization, fan-out | Lambda (Python 3.13) + Organizations API |
| Engine rotation | Quyết định rotate/disable/delete theo tuổi key | Lambda (Python 3.13) + IAM API |
| Lưu trữ key | Lưu key mới sau khi tạo, có mã hóa | AWS Secrets Manager (KMS CMK) |
| Thông báo | Gửi email cho người phụ trách + admin trước/sau mỗi hành động | Amazon SES + template HTML |
| Scheduler | Kích hoạt hằng ngày | EventBridge rule (cron) |
| Truy cập liên account | Assume role vào member account | IAM role (ExecutionRole) được triển khai qua StackSet |
| Miễn trừ | Loại trừ service user đặc biệt | IAM group IAMKeyRotationExemptionGroup |
| Kiểm toán | Log mọi hành động | CloudWatch 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.
Ba tham số có thể chỉnh qua CloudFormation parameter:
RotationPeriod= 90 ngàyInactivePeriod= 100 ngàyRecoveryGracePeriod= 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 accountASA-iam-key-auto-rotation-iam-assumed-roles.yaml: role liên account, triển khai qua StackSet ra tất cả member accountASA-iam-key-auto-rotation-list-accounts-role.yaml: role đọc Organizations API, triển khai ở management accountASA-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=True là bắ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 choiam:PassRole,iam:AttachPolicy,iam:CreateUser. Nếu có thể, giới hạn thêm bằng điều kiệnaws: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.
IAMKeyRotationExemptionGrouplà cơ chế tiện lợi nhưng cũng là cách để qua mặt rotation. Bật cảnh báo CloudTrail choiam:AddUserToGroupkhi 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.yamltriể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
RotationPeriodnhư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 khiRecoveryGracePeriodhế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 định | Lựa chọn | Mình chọn | Vì |
|---|---|---|---|
| Nguồn sự thật | Bả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ới | Secrets Manager vs. SSM Parameter Store | Secrets Manager | Có API rotation, kiểm toán tích hợp sẵn, KMS per-secret, chi phí chấp nhận được |
| Scheduler | EventBridge cron vs. Step Functions | EventBridge + fan-out Lambda | Đơn giản; không cần state machine cho xử lý 1 bước |
| Thông báo | SES vs. email SNS vs. Slack webhook | SES | Template HTML tuỳ biến, không bắt buộc subscribe, phù hợp nội dung nhạy cảm |
| Nhiều account | STS AssumeRole vs. Lambda triển khai per-account | AssumeRole từ security account | 1 đường code, 1 log group, 1 cảnh báo, dễ vận hành hơn N bản sao |
| Mặc định DryRun | Mặc định True vs. mặc định False | Mặc định True | An 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 group | IAM group | Group 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. TypeScript | Python 3.13 | boto3 đầ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
- vanhoangkha/aws-iam-access-key-auto-rotation: repo gốc của giải pháp mô tả trong bài
- AWS IAM Best Practices, Rotate credentials regularly
- Rotating IAM access keys (official doc)
- AWS Secrets Manager pricing & KMS considerations
- IAM Access Analyzer, unused access findings
- CIS AWS Foundations Benchmark 1.14: kiểm soát tuổi access key