2019, Capital One: attacker khai thác lỗi SSRF trong WAF, lấy IAM role credential từ EC2 metadata service (IMDSv1 – plain HTTP GET, không token). Role này có quyền đọc S3 quá rộng – 100 triệu hồ sơ credit card bị lộ. Không malware, không zero-day. Chỉ là một IAM role quá nhiều quyền.
2022, Uber: nhóm Lapsus$ spam push-MFA cho một contractor đến khi người đó bấm “Approve” (MFA fatigue). Vào được internal network, họ tìm thấy hard-coded privileged token trong shared network drive – full access vào source code, internal tools, cloud infrastructure. Token quá mạnh, quá lâu, không phishing-resistant MFA.
2016, Uber (lại): developers commit AWS access keys vào private GitHub repo. Attacker tìm thấy, dùng key đó truy cập production data. $148 triệu settlement.
Cả ba incident đều có chung một gốc: IAM policy quá rộng. Và AWS có đủ công cụ để ngăn chúng – nếu bạn hiểu cách nó hoạt động từ tận cùng.
flowchart TB
Request["API Request"]
Step1{"1. Explicit Deny<br/>ở bất kỳ policy nào?"}
Step2{"2. RCP Allow?"}
Step3{"3. SCP Allow ở<br/>mọi cấp OU?"}
Step4{"4. Resource-based<br/>policy Allow?"}
Step5{"5. Permission<br/>Boundary Allow?"}
Step6{"6. Session Policy<br/>Allow?"}
Step7{"7. Identity-based<br/>policy Allow?"}
Denied["DENIED<br/>không ghi đè được"]
Allowed["ALLOWED"]
Request --> Step1
Step1 -->|"Có"| Denied
Step1 -->|"Không"| Step2
Step2 -->|"Không"| Denied
Step2 -->|"Có"| Step3
Step3 -->|"Không ở bất kỳ cấp"| Denied
Step3 -->|"Có ở mọi cấp"| Step4
Step4 --> Step5
Step5 -->|"Allow"| Step6
Step5 -->|"Không Allow"| Denied
Step6 -->|"Allow / Không có"| Step7
Step6 -->|"Không Allow"| Denied
Step7 -->|"Ít nhất 1 Allow"| Allowed
Step7 -->|"Không Allow nào"| Denied
```text
---
## Policy evaluation 7 bước: không phải "merge rồi evaluate"
IAM không merge tất cả policy rồi evaluate. Nó duyệt **tuần tự 7 lớp**, mỗi lớp là một gatekeeper độc lập. Nếu bất kỳ lớp nào chặn → request bị deny, các lớp sau không được evaluate.
### Bước 1: Explicit Deny -- quét TẤT CẢ policy
**Đây là rule mạnh nhất trong IAM.** AWS quét tất cả policy types (identity-based, resource-based, permission boundary, SCP, RCP, session policy). Chỉ cần **MỘT** policy có `"Effect": "Deny"` khớp với request → **DENIED ngay lập tức**. Không có ngoại lệ. Không có Allow nào ghi đè được Deny.
Hệ quả quan trọng: nếu bạn muốn chặn một hành động cụ thể cho **mọi người**, đặt Deny trong SCP. SCP được evaluate ở bước 2-3, nhưng explicit deny scan ở bước 1 sẽ bắt được nó.
### Bước 2-3: RCP và SCP (Organization-level)
- **RCP (Resource Control Policy)**: Mới từ 11/2024. Kiểm soát resource-side -- ai được truy cập S3 bucket, KMS key, Secrets Manager secret từ bên ngoài organization. Mặc định `RCPFullAWSAccess`.
- **SCP (Service Control Policy)**: Filter quyền. **SCP không cấp quyền** -- nó chỉ giới hạn quyền tối đa. Phải có Allow ở **mọi cấp** trong OU hierarchy (root → OU → sub-OU → account).
```text
SCP Root: Allow EC2, S3, Lambda
SCP OU-Dev: Allow S3, Lambda (KHÔNG có EC2)
→ Account trong OU-Dev: KHÔNG có EC2, dù SCP Root cho phép
```text
### Bước 4: Resource-based policy
- **Resource-based policy:** Gắn vào resource. Có `Principal` field. Cho phép cross-account access không cần AssumeRole.
Resource-based policy được evaluate độc lập. Nếu Allow, request có thể được phép nếu identity-based policy cũng Allow (xem cross-account note bên dưới).
> **Cùng account vs Cross-account:** Cùng account: **một trong hai** (resource-based hoặc identity-based) Allow là đủ. Cross-account: **cả hai** đều phải Allow. Resource-based policy cho phép principal từ account khác, identity-based policy cho phép user/role trong account đó thực hiện action.
### Bước 5-6: Permission Boundary + Session Policy
- **Permission Boundary**: Trần quyền tối đa. Effective permissions = identity policy ∩ boundary. Mình thấy Permission Boundary là safety net đáng đầu tư nhất cho team nhiều người -- nó cho phép delegate IAM mà không sợ ai đó tạo role quyền vô hạn.
- **Session Policy**: Chỉ có thể **thu hẹp** quyền (intersection), không bao giờ mở rộng. Dùng cho Just-in-Time access.
### Bước 7: Identity-based policy
- **Identity-based policy:** Gắn vào IAM user/group/role. Đây là policy bạn viết hàng ngày.
- Identity-based policy được evaluate **cuối cùng**, sau khi permission boundary và session policy đã giới hạn quyền. Effective permissions = identity policy ∩ permission boundary ∩ session policy, sau đó kết hợp với resource-based policy (cùng account: một trong hai Allow là đủ; cross-account: cả hai phải Allow).
```mermaid
flowchart LR
Identity["Identity Policy<br/>Allow: S3:* + EC2:*"]
Boundary["Permission Boundary<br/>Allow: S3:* + Lambda:*"]
Result["Effective = S3:*<br/>(EC2 bị boundary cắt)"]
Identity --> Result
Boundary --> Result
```text
---
## Policy JSON: từng field, từng use case
```json
{
"Version": "2012-10-17",
"Id": "S3ReadOnly-production",
"Statement": [
{
"Sid": "AllowGetObject",
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:GetObjectVersion"],
"Resource": "arn:aws:s3:::myapp-prod-data/*",
"Condition": {
"StringEquals": {
"s3:ResourceAccount": "111111111111"
},
"Bool": {
"aws:SecureTransport": "true",
"aws:MultiFactorAuthPresent": "true"
}
}
},
{
"Sid": "DenyDeleteObject",
"Effect": "Deny",
"Action": ["s3:DeleteObject", "s3:DeleteObjectVersion"],
"Resource": "arn:aws:s3:::myapp-prod-data/*"
}
]
}
```text
| Field | Required? | Purpose |
|-------|-----------|---------|
| `Version` | Yes | Luôn `"2012-10-17"` -- policy language version |
| `Id` | No | ID cho audit, tracking |
| `Statement` | Yes | Array of statements (có thể nhiều) |
| `Sid` | No | Label từng statement, dùng cho audit log |
| `Effect` | Yes | `Allow` hoặc `Deny` |
| `Action` | Yes (trừ 1 số edge case) | API actions: `s3:GetObject`, `ec2:RunInstances` |
| `NotAction` | Alternative | Tất cả actions NGOẠI TRỪ những cái liệt kê |
| `Resource` | Yes (trừ 1 số edge case) | ARN của resource, hoặc `"*"` |
| `NotResource` | Alternative | Tất cả resources NGOẠI TRỪ những cái liệt kê |
| `Condition` | No | Điều kiện: IP, thời gian, tag, MFA, VPC endpoint, PrincipalOrgID... |
| `Principal` | Chỉ resource-based | Ai được phép (không dùng trong identity-based policy) |
### Condition operators quan trọng nhất
| Operator | Dùng để | Ví dụ |
|----------|---------|-------|
| `StringEquals` | Exact match | `"aws:PrincipalTag/Team": "backend"` |
| `StringLike` | Pattern match | `"aws:PrincipalArn": "arn:aws:iam::*:role/admin-*"` |
| `ArnEquals` / `ArnLike` | ARN comparison | `"aws:SourceArn": "arn:aws:cloudfront::..."` |
| `IpAddress` | IP check | `"aws:SourceIp": "203.0.113.0/24"` |
| `Bool` | Boolean check | `"aws:SecureTransport": "true"` |
| `NumericEquals` / `NumericGreaterThan` | Numeric | `"s3:max-keys": "100"` |
| `DateEquals` / `DateGreaterThan` | Date | `"aws:CurrentTime": "2026-12-31T23:59:59Z"` |
| `Null` | Check existence | `"aws:RequestTag/Team": "false"` (bắt buộc có tag) |
| `ForAnyValue:StringEquals` | Multi-value match | Một trong các giá trị khớp |
| `ForAllValues:StringEquals` | All values match | Tất cả giá trị phải khớp |
---
## User vs Group vs Role: credential mechanics
Đây là điểm gây nhầm lẫn nhất trong IAM. Sự khác biệt nằm ở **cơ chế credential**:
| | IAM User | IAM Role | IAM Group |
|---|----------|----------|-----------|
| **Credential loại** | Access key (long-term) + password (console) | STS temporary token (1-12h) | Không có credential |
| **Credential lifetime** | Vô hạn (đến khi rotate/delete) | Tự động expire (1-12h) | N/A |
| **Principal?** | Có (có ARN, có thể là Principal trong policy) | Có | **Không** (Group không phải principal) |
| **Assume được?** | Không (user không có trust policy) | Có (bất kỳ ai được trust đều assume được) | N/A |
| **Dùng cho** | Break-glass, legacy, on-prem server | **Mọi thứ khác** -- AWS service, CI/CD, cross-account | Tổ chức user |
### Tại sao role tốt hơn user?
1. **Temporary credential:** Token expire sau 1-12h. Nếu lộ, attacker có vài giờ. Access key: vô hạn, đến khi bạn rotate
2. **Không cần rotate:** Token tự expire, tự refresh. Access key phải manual rotate mỗi 90 ngày
3. **Assume từ nhiều nơi:** Cùng một role, EC2 assume được, Lambda assume được, developer assume được qua SSO
4. **Audit tập trung:** Mọi AssumeRole đều được ghi CloudTrail. Biết ai assume, khi nào, từ đâu
```bash
# Tạo role cho Lambda
aws iam create-role \
--role-name app-s3-reader \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "lambda.amazonaws.com"},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {"aws:SourceAccount": "123456789012"}
}
}]
}'
# Attach least privilege policy
aws iam attach-role-policy \
--role-name app-s3-reader \
--policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
```text
---
## Managed vs Inline Policy: chọn đúng
| | AWS Managed | Customer Managed | Inline |
|---|-------------|------------------|--------|
| **Tạo bởi** | AWS | Bạn | Bạn |
| **Reusable?** | Có | Có | Không (1 principal) |
| **Update** | AWS tự update (có thể thay đổi behavior!) | Bạn update | Bạn update |
| **Giới hạn** | -- | 5,000 policies/account | 1 policy/principal, tổng size giới hạn |
| **Dùng khi** | Standard permission ("S3 read-only") | Policy tùy chỉnh, dùng chung nhiều role | Permission tightly-coupled với role lifecycle |
AWS managed policy có thể thay đổi. AWS tự động update managed policy khi service có API mới. AmazonS3ReadOnlyAccess hôm nay có thể thêm s3:GetObjectVersion ngày mai – thường là tốt, nhưng đôi khi thêm quyền bạn không muốn. Nếu cần strict control, dùng customer managed policy – copy từ AWS managed, rồi tự maintain.
---
## Permission Boundary: safety net chống privilege escalation
Permission boundary là **trần quyền tối đa** cho IAM user/role. Dù bạn có gán `AdministratorAccess`, boundary vẫn chặn.
Mình thấy Permission Boundary là safety net đáng đầu tư nhất cho team nhiều người. Nó cho phép developer tự do tạo role mà không sợ ai đó vô tình tạo một role full admin.
Use case chính: **delegate IAM management cho developer team mà không sợ họ escalate**. Bạn cho team quyền `iam:CreateRole`, nhưng gán permission boundary để role họ tạo không bao giờ vượt quá quyền của chính họ.
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowDevServices",
"Effect": "Allow",
"Action": [
"s3:*", "lambda:*", "logs:*",
"dynamodb:*", "sqs:*", "sns:*"
],
"Resource": "*"
},
{
"Sid": "DenyIAMPermissions",
"Effect": "Deny",
"Action": [
"iam:CreateUser", "iam:CreateAccessKey",
"iam:PutUserPolicy", "iam:AttachUserPolicy",
"iam:DeleteRolePermissionsBoundary",
"iam:PutRolePermissionsBoundary",
"organizations:*", "account:*"
],
"Resource": "*"
}
]
}
```text
```bash
aws iam create-user --user-name junior-dev \
--permissions-boundary arn:aws:iam::123456789012:policy/dev-boundary
```text
---
## IAM Access Analyzer: công cụ least privilege 2025-2026
### Unused Access Analyzer
Tìm permissions không dùng: một engagement thực tế phát hiện **60% actions được grant không bao giờ gọi** trong 90 ngày trên 3,100 roles.
### Policy Generation từ CloudTrail
```text
1. Deploy với policy permissive ở non-prod
2. Chạy workload 7-14 ngày → CloudTrail ghi API calls thực tế
3. Access Analyzer generate policy dựa trên usage
4. Human review → promote production
```text
### CI/CD Integration
```text
PR → Terraform Plan → Extract IAM policies →
CheckNoNewAccess (policy mới thêm quyền gì?)
CheckAccessNotGranted (có cấp forbidden actions?)
CheckNoPublicAccess (expose resource ra public?)
→ Comment PR → Block merge nếu vi phạm
```text
---
## Real incidents & bài học
| Incident | IAM Root Cause | Prevention |
|----------|---------------|------------|
| **Capital One 2019** | SSRF → IMDSv1 → role quá rộng đọc S3 | IMDSv2 + least privilege role |
| **Uber 2022** | MFA fatigue + hard-coded privileged token | FIDO2 MFA + STS short-lived token + Secrets Manager |
| **Uber 2016** | Access key lộ GitHub | OIDC (Bài 24), không dùng access key |
| **Accenture 2017** | S3 bucket public + key lộ | Block Public Access + key rotation |
| **Tesla 2018** | Cryptojacking qua console access không bảo vệ | GuardDuty + CloudTrail real-time alert |
---
- **IAM evaluation 7 bước tuần tự** -- Explicit Deny, RCP, SCP, Resource Policy, Permission Boundary, Session Policy, Identity Policy. Deny luôn thắng Allow, không ngoại lệ
- **Role cho mọi thứ, user chỉ cho break-glass** -- temporary token > long-term access key
- **Permission Boundary = trần quyền** -- delegate IAM an toàn cho team developer
- **Access Analyzer** -- tìm unused permissions, generate policy từ usage, check trong CI/CD
- **Học từ incident**: Capital One (role quá rộng + IMDSv1), Uber 2022 (MFA fatigue), Uber 2016 (key lộ GitHub)
Bài sau: [Phần 3: IAM nâng cao -- cross-account, ABAC, SCP, Identity Center](/posts/aws/03-iam-nang-cao-cross-account-abac-scp/)
## Câu hỏi hay gặp
**Q: Explicit Deny có thực sự không ghi đè được không?**
A: Không. Đây là rule cứng của IAM engine -- explicit Deny luôn thắng, kể cả AdministratorAccess. AWS làm vậy vì security-first: thà block nhầm còn hơn allow sai.
**Q: Khi nào dùng inline policy thay vì managed?**
A: Khi policy tightly-coupled với lifecycle của một role cụ thể -- xóa role → policy tự xóa. Hoặc khi policy chứa dynamic value (ARN của resource chỉ role đó cần). Còn lại dùng customer managed để tái sử dụng.
**Q: Permission boundary khác SCP thế nào?**
A: SCP ở cấp Organization -- giới hạn mọi principal trong account. Permission boundary ở cấp IAM user/role -- giới hạn principal cụ thể. Boundary dùng để **delegate IAM management** (cho team tạo role nhưng không vượt quyền). SCP dùng để **enforce guardrail toàn organization** (cấm public S3, chặn region ngoài danh sách).