IAM & IdentityIAM & Identity
Part of the Cloud knowledge base — AWS deep dive. Complements DevOps Roadmap.
Tổng quan
AWS Identity and Access Management (IAM) là hệ thống phân quyền quyết định ai được làm gì với tài nguyên nào và trong điều kiện nào. Mọi lời gọi API của AWS — từ console, CLI, SDK, hay từ một service gọi sang service khác — đều được IAM đánh giá trước khi thực thi. Nếu bạn hiểu sâu IAM thì bảo mật AWS về cơ bản đã được giải quyết; nếu không, bạn sẽ hoặc liên tục bị chặn bởi Access Denied, hoặc tệ hơn nhiều, cấp quyền quá rộng một cách nguy hiểm.
IAM là một global service (một IAM role hay user hoạt động ở mọi region) và nó miễn phí. Nó không thuộc availability zone nào; control plane của nó nằm ở us-east-1. Mô hình tư duy nên có: một policy engine khổng lồ luôn bật, nó nhận một request (“principal alice muốn thực hiện s3:GetObject trên arn:aws:s3:::reports/q3.pdf”), gom mọi policy có thể áp dụng, rồi trả về allow hoặc deny.
Xuyên suốt trang này là một tiến trình: từ user tồn tại lâu dài (nên tránh) hướng tới role ngắn hạn và federation (nên ưu tiên). Bảo mật AWS hiện đại được xây trên credentials tạm thời do STS cấp, danh tính được federated từ một IdP bên ngoài, và các policy least-privilege được giới hạn chặt đúng theo những gì một workload thực sự cần. Access key tồn tại lâu dài là con đường cũ (legacy) và là nguồn gốc của phần lớn các vụ rò rỉ credentials.
Kiến thức nền tảng
Bốn thành phần cấu tạo
| Entity | Nó là gì | Có credentials? | Dùng cho |
|---|---|---|---|
| User | Một danh tính người dùng hoặc ứng dụng có tên, với credentials tồn tại lâu dài | Có (password và/hoặc access key) | Legacy; tránh dùng cho con người — ưu tiên SSO |
| Group | Một tập hợp các user; policy gắn vào group | Không | Tổ chức user theo chức năng công việc |
| Role | Một danh tính được assume tạm thời; cấp ra credentials ngắn hạn | Không có creds cố định — được cấp khi assume | Cách được ưu tiên để cấp quyền truy cập cho mọi thứ |
| Policy | Một tài liệu JSON liệt kê các quyền | N/A | Gắn vào user, group, hoặc role |
Một principal là bất cứ ai đang thực hiện một request — một IAM user, một role đã được assume, một AWS service, hoặc một danh tính federated. Một role có hai nửa: một trust policy (ai được phép assume nó) và các permission policy (nó được làm gì sau khi đã assume). Cấu trúc hai phần này là trái tim của gần như tất cả các mẫu truy cập AWS an toàn.
Các loại policy
| Loại | Gắn vào | Mục đích |
|---|---|---|
| Identity-based policy | User, group, hoặc role | Cấp quyền cho danh tính đó |
| Resource-based policy | Một tài nguyên (S3 bucket, SQS queue, KMS key, Lambda) | Cấp quyền trên tài nguyên đó, và chỉ định principal |
| Managed policy | Có thể tái sử dụng, có version, gắn vào nhiều danh tính | Do AWS quản lý (ví dụ AdministratorAccess) hoặc do khách hàng quản lý |
| Inline policy | Nhúng vào một danh tính, vòng đời 1:1 | Quyền dùng một lần gắn với một danh tính duy nhất |
| Permissions boundary | Đặt mức tối đa mà một danh tính có thể có | Rào chắn ủy quyền — giới hạn trần quyền hiệu lực |
| SCP (Organizations) | OU hoặc account | Trần quyền toàn tổ chức; lọc bớt, không bao giờ cấp quyền |
| Session policy | Truyền vào lúc AssumeRole | Thu hẹp thêm quyền của một session |
Identity-based vs resource-based là một cặp then chốt. Để cho account B đọc S3 bucket của bạn ở account A, bạn có thể gắn một bucket policy dạng resource-based chỉ định principal của account B — không cần assume role. Với đa số truy cập cross-account, resource-based policy (ở những service có hỗ trợ: S3, SQS, SNS, KMS, Lambda, Secrets Manager) là công cụ gọn gàng hơn.
Managed vs inline: ưu tiên customer-managed policy — chúng tái sử dụng được, có version (bạn có thể roll back), và hiển thị như những đối tượng hạng nhất. Inline policy dành cho những quyền thực sự dùng một lần, sinh ra và mất đi cùng một danh tính duy nhất. Các AWS-managed policy như AdministratorAccess thì tiện lợi nhưng thường quá rộng; hãy coi PowerUserAccess và các policy *FullAccess là điểm khởi đầu để siết chặt, chứ không phải điểm dừng.
Cấu trúc JSON của policy
Mỗi IAM policy là một tài liệu JSON. Hãy học đọc nó thật trôi chảy — đó là ngôn ngữ của quyền trong AWS.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowReadSpecificBucket",
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::my-app-data",
"arn:aws:s3:::my-app-data/*"
],
"Condition": {
"IpAddress": { "aws:SourceIp": "203.0.113.0/24" },
"Bool": { "aws:SecureTransport": "true" }
}
}
]
}
| Element | Ý nghĩa |
|---|---|
Version | Luôn là 2012-10-17 (phiên bản ngôn ngữ policy hiện hành) |
Sid | Id statement tùy chọn, dễ đọc cho con người |
Effect | Allow hoặc Deny |
Action | Các thao tác API, ví dụ s3:GetObject (service:Operation); hỗ trợ wildcard |
Resource | (Các) ARN mà action áp dụng lên; * nghĩa là tất cả |
Condition | Các phép kiểm tra dựa trên key tùy chọn, tất cả phải đúng |
Principal | (chỉ resource-based) statement áp dụng cho ai |
Lưu ý hai ARN của S3: my-app-data (bucket, dùng cho ListBucket) và my-app-data/* (các object, dùng cho GetObject). Action ở cấp bucket và cấp object cần các ARN khác nhau — một lỗi thường gặp. Condition giới hạn theo một dải IP nguồn và bắt buộc dùng TLS.
Logic đánh giá — cách allow/deny được quyết định
Thuật toán ra quyết định của IAM, được đơn giản hóa:
- Mặc định = implicit deny. Không gì được phép cho tới khi có thứ gì đó cho phép.
- Một
Denytường minh ở bất cứ đâu luôn thắng — trong bất kỳ policy, SCP, hay boundary nào. KhôngAllownào ghi đè được nó. - Một request chỉ được phép nếu có một
Allowáp dụng và nó không bị cắt bởi bất kỳ thứ nào trong: trần của SCP, permissions boundary, session policy, hay resource policy (với cross-account, cả hai phía đều phải cho phép).
- ?Explicit Deny?yesDENY
- ?SCP allows?noDENY
- ?Boundary allows?noDENY
- ?Identity/resource Allow present?noDENY (implicit)
Hai quy tắc quan trọng nhất trong thực tế: explicit deny luôn thắng, và truy cập cross-account đòi hỏi một allow ở cả phía identity lẫn phía resource.
Khái niệm chính
STS và AssumeRole — credentials tạm thời
AWS Security Token Service (STS) cấp credentials ngắn hạn (một access key, secret key, và session token) hết hạn trong khoảng từ 15 phút đến 12 giờ. Role được assume thông qua STS. Đây là cơ chế đứng sau truy cập cross-account, federation, service role của EC2/Lambda, và break-glass admin.
Trust policy của một role nói ai được phép assume nó:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::111122223333:root" },
"Action": "sts:AssumeRole",
"Condition": { "StringEquals": { "sts:ExternalId": "unique-shared-secret" } }
}]
}
Điều này tin tưởng account 111122223333 được assume role, nhưng chỉ khi nó trình ra ExternalId đã thỏa thuận — biện pháp phòng thủ tiêu chuẩn chống lại vấn đề “confused deputy” khi cấp quyền cho bên thứ ba.
Luồng assume từ CLI:
# 1. Assume the role — STS returns temporary credentials
aws sts assume-role \
--role-arn arn:aws:iam::444455556666:role/CrossAccountReadOnly \
--role-session-name alice-audit \
--external-id unique-shared-secret
# Response contains Credentials { AccessKeyId, SecretAccessKey, SessionToken, Expiration }
# 2. Export them and act as the role
export AWS_ACCESS_KEY_ID=ASIA...
export AWS_SECRET_ACCESS_KEY=...
export AWS_SESSION_TOKEN=...
aws s3 ls # now runs with the assumed role's permissions
Trong thực tế bạn không bao giờ copy-paste credentials — bạn cấu hình một named profile với role_arn + source_profile trong ~/.aws/config và CLI sẽ assume role một cách trong suốt:
[profile audit]
role_arn = arn:aws:iam::444455556666:role/CrossAccountReadOnly
source_profile = default
external_id = unique-shared-secret
Sau đó aws s3 ls --profile audit chỉ đơn giản là chạy được.
Truy cập cross-account — hai mẫu
- Assume role (phổ biến nhất): Account A tạo một role tin tưởng Account B; các danh tính của B gọi
sts:AssumeRole. Tốt khi B cần hành động bên trong A. Đây là cách CI/CD deploy vào prod, cách một audit account đọc mọi thứ, cách một tổ chức hub-and-spoke vận hành. - Resource-based policy: S3 bucket / SQS queue / KMS key của Account A chỉ định trực tiếp principal của B trong policy của nó. Tốt để chia sẻ một tài nguyên cụ thể mà không cần B assume bất cứ thứ gì. Ít thành phần chuyển động hơn khi chia sẻ một tài nguyên đơn lẻ.
Hệ thống phân cấp giới hạn quyền
Nhiều lớp có thể giới hạn một danh tính. Quyền hiệu lực = phần giao (intersection) của mọi thứ có thể cho phép, trừ đi mọi explicit deny:
| Lớp | Được đặt bởi | Hiệu ứng |
|---|---|---|
| SCP | Org admin | Trần quyền toàn tổ chức áp lên account |
| Permissions boundary | Admin ủy quyền | Mức tối đa một danh tính cá nhân có thể có |
| Identity policy | Người quản lý danh tính đó | Những gì nó được cấp |
| Session policy | Truyền vào lúc AssumeRole | Thu hẹp riêng session này |
| Resource policy | Chủ sở hữu tài nguyên | Cấp quyền trên tài nguyên (và bật cross-account) |
Một quyền chỉ có hiệu lực nếu mọi trần áp dụng được (SCP, boundary, session) đều cho phép nó và một identity hoặc resource policy cấp nó và không gì deny nó. Permissions boundary là cách để bạn an toàn cho phép developer tự tạo role của họ: bạn cấp cho họ iam:CreateRole nhưng yêu cầu mọi role họ tạo phải mang một boundary, để họ không bao giờ leo thang quyền vượt quá nó.
IAM Identity Center (trước đây là AWS SSO)
IAM Identity Center là cổng vào hiện đại cho việc truy cập của con người xuyên suốt một tổ chức nhiều account. Thay vì có IAM user ở mọi account, người dùng đăng nhập một lần (đối chiếu với directory tích hợp sẵn hoặc một IdP bên ngoài như Okta/Entra ID/Google), rồi nhận được một portal gồm các account và permission set mà họ được truy cập. Ở hậu trường, nó cấp phát role và cấp credentials STS ngắn hạn — không có key tồn tại lâu dài ở bất cứ đâu.
- Permission set là các template role tái sử dụng được (ví dụ
AdministratorAccess,ReadOnly,Billing) gán cho user/group theo từng account. aws configure ssokết nối CLI với nó; credentials tự làm mới và hết hạn.- Đây là mặc định được khuyến nghị cho mọi truy cập của con người vào AWS hiện nay.
Federation và OIDC — bao gồm cả GitHub Actions
Federation cho phép các danh tính từ một hệ thống bên ngoài assume AWS role mà không cần bất kỳ credentials tồn tại lâu dài nào ở phía AWS. Hai giao thức:
- SAML 2.0 — các IdP doanh nghiệp (ADFS, Okta) cho SSO của console/CLI.
- OIDC — cách tiếp cận hiện đại dựa trên token, lý tưởng cho CI/CD và workload.
Trường hợp sử dụng đắt giá nhất là GitHub Actions → AWS với zero secret được lưu trữ. Thay vì đặt một AWS access key vào GitHub, bạn đăng ký OIDC provider của GitHub trong IAM và tạo một role tin tưởng các token từ repo cụ thể của bạn:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com" },
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:ref:refs/heads/main"
}
}
}]
}
Condition sub là dòng then chốt về bảo mật: nó giới hạn việc assume chỉ cho các workflow trên nhánh main của đúng repo my-org/my-repo. Nới lỏng nó một cách bất cẩn (ví dụ repo:my-org/*) và bất kỳ repo nào trong tổ chức của bạn — hoặc một pull request từ fork — đều có thể assume role. Trong workflow:
permissions:
id-token: write # required to request the OIDC token
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubDeploy
aws-region: ap-southeast-1
Không có secret nào được lưu ở bất cứ đâu; GitHub tạo ra một OIDC token ngắn hạn, AWS xác minh nó với trust policy, và STS trả về credentials tạm thời được giới hạn theo role. Cùng một mẫu này (AssumeRoleWithWebIdentity) cũng vận hành EKS IRSA/Pod Identity và federation cho ứng dụng di động.
Least privilege và Access Analyzer
Least privilege nghĩa là chỉ cấp những quyền mà một principal thực sự dùng — không hơn. Cách đạt được điều đó:
- Bắt đầu hẹp rồi mở rộng dựa trên các lỗi Access Denied thực tế, thay vì bắt đầu với
*và không bao giờ siết lại. - IAM Access Analyzer làm phần việc nặng nhọc: nó gắn cờ các tài nguyên được chia sẻ ra bên ngoài (public bucket, cross-account role), và tính năng policy generation của nó đọc lịch sử CloudTrail để tạo ra một policy least-privilege từ những gì một danh tính thực sự đã làm trong N ngày qua.
- Các phát hiện unused-access của Access Analyzer làm nổi bật các role, user, và quyền chưa được dùng, để bạn có thể cắt tỉa chúng.
- Dùng
aws iam simulate-principal-policyđể kiểm tra xem một policy có cho phép một action hay không trước khi deploy nó.
MFA và xử lý root account
- Root user: mỗi account có một danh tính root toàn quyền gắn với email đăng ký. Hãy khóa chặt nó: bật MFA (lý tưởng là hardware key), xóa các access key của nó, không bao giờ dùng nó hàng ngày. Một vài tác vụ thực sự cần root (thay đổi cài đặt account, đóng account) — mọi thứ còn lại dùng IAM.
- MFA ở mọi nơi: yêu cầu MFA cho tất cả người dùng là con người và cho các action nhạy cảm. Bạn có thể chặn các quyền rủi ro cao sau một condition MFA:
"Condition": { "Bool": { "aws:MultiFactorAuthPresent": "true" } }. - MFA có điều kiện cho AssumeRole: trust policy có thể yêu cầu người gọi phải đã xác thực bằng MFA trước khi assume một role đặc quyền.
Best Practices
-
Loại bỏ credentials tồn tại lâu dài, chuyển sang role và STS. Credentials tạm thời tự hết hạn, nên một cái bị rò rỉ trở nên vô giá trị trong vòng vài giờ. Mỗi con người nên đăng nhập qua IAM Identity Center, và mỗi workload (EC2, Lambda, ECS, EKS) nên dùng một role được gắn — không bao giờ nhúng access key.
-
Khóa chặt root user và không bao giờ dùng nó cho công việc hàng ngày. Bật hardware MFA, xóa các access key của nó, cất password vào két sắt vật lý, và thiết lập cảnh báo cho bất kỳ hoạt động nào của root. Root có thể vượt qua đa số các biện pháp kiểm soát, nên bất kỳ lần dùng nào cũng phải là một sự kiện break-glass có chủ đích, được ghi log.
-
Bắt buộc MFA cho mọi truy cập của con người. Yêu cầu MFA khi đăng nhập và chặn các action nhạy cảm sau một condition
aws:MultiFactorAuthPresent. MFA là biện pháp kiểm soát có đòn bẩy cao nhất chống lại việc đánh cắp credentials. -
Cấp least privilege và lặp lại để tiệm cận nó. Bắt đầu với quyền hẹp và chỉ mở rộng khi có nhu cầu được chứng minh. Dùng policy generation của Access Analyzer để suy ra policy dựa trên sử dụng thực tế từ CloudTrail, và các phát hiện unused-access của nó để cắt tỉa những gì đã lỗi thời.
-
Ưu tiên customer-managed policy hơn inline, và hơn các AWS-managed policy quá rộng. Managed policy tái sử dụng được, có version, và roll back được; inline policy giấu quyền bên trong các danh tính. Hãy coi
*FullAccessvàPowerUserAccesslà điểm khởi đầu để siết chặt, không phải câu trả lời cuối cùng. -
Dùng group (hoặc permission set) cho con người, role cho workload. Không bao giờ gắn policy trực tiếp vào từng user riêng lẻ ở quy mô lớn — hãy gán chúng qua group hoặc permission set của Identity Center để việc truy cập nhất quán, kiểm toán được, và dễ thu hồi bằng cách chuyển ai đó ra khỏi một group.
-
Đặt permissions boundary khi ủy quyền IAM. Nếu developer có thể tạo role, hãy yêu cầu mọi role họ tạo phải mang một boundary policy. Điều này cho phép các team tự phục vụ mà không có đường leo thang đặc quyền vượt quá trần bạn đã đặt.
-
Dùng SCP làm rào chắn toàn tổ chức. Áp đặt các bất biến một cách tập trung — giới hạn vào các region đã duyệt, deny việc tắt CloudTrail/Config, chặn việc rời khỏi tổ chức, bảo vệ tài nguyên bảo mật khỏi bị xóa. SCP ngăn chặn cả những lớp lỗi lớn bất kể admin của account làm gì.
-
Giới hạn condition thật chặt, đặc biệt trong trust policy. Với GitHub OIDC, ghim claim
subvào đúng một repo và nhánh; với cross-account role, dùngExternalIdđể đánh bại vấn đề confused-deputy. MộtPrincipalhoặc condition quá rộng là cách mà sự tin tưởng của một team trở thành cửa hậu của tất cả mọi người. -
Áp dụng OIDC federation cho CI/CD thay vì key lưu trữ. GitHub Actions, GitLab, và những cái khác có thể assume AWS role qua các OIDC token ngắn hạn. Điều này loại bỏ secret bị rò rỉ phổ biến nhất — một AWS access key tĩnh nằm trong trang cài đặt của CI.
-
Yêu cầu cả hai phía cho phép với truy cập cross-account, và giữ nó tường minh. Hiểu rằng một resource policy và identity policy của người gọi đều phải cho phép action. Ghi tài liệu cho mọi quan hệ tin tưởng cross-account; những trust policy không giải thích được là cách kẻ tấn công xoay chuyển giữa các account.
-
Xoay vòng bất kỳ key tồn tại lâu dài nào còn lại và cảnh báo khi chúng được dùng. Ở nơi access key là không thể tránh khỏi (tích hợp legacy), hãy xoay vòng chúng theo lịch, giới hạn phạm vi tối thiểu, và giám sát bằng CloudTrail. Dùng Access Analyzer và credential report để tìm các key cũ, không dùng, hoặc quá nhiều đặc quyền.
-
Bật IAM Access Analyzer ở mọi account. Nó liên tục gắn cờ các tài nguyên bị phơi ra ngoài account hoặc tổ chức của bạn — public S3 bucket, KMS key chia sẻ quá mức, cross-account role quá rộng — biến một cuộc kiểm toán thủ công thành một cuộc kiểm tra luôn bật.
-
Kiểm thử policy trước khi deploy chúng. Dùng
iam simulate-principal-policyvà policy simulator trên console để xác nhận một thay đổi cấp đúng những gì bạn dự định và không hơn. Các tương tác explicit-deny và giao của boundary rất dễ sai một cách tinh vi. -
Ghi log và kiểm toán tất cả hoạt động IAM một cách tập trung. Gửi CloudTrail tới một Log Archive account được khóa chặt để mọi AssumeRole, thay đổi policy, và lần dùng access key đều được ghi lại bất biến. Dấu vết này là bằng chứng chính của bạn trong một sự cố và là bằng chứng kiểm soát của bạn trong một cuộc kiểm toán.
-
Rà soát IAM định kỳ và loại bỏ những gì không dùng. Tạo credential report, xóa các user không hoạt động, cắt tỉa role và quyền không dùng, và xác minh lại rằng các permission set vẫn khớp với chức năng công việc. Việc cấp quyền tích tụ âm thầm; không có sự cắt tỉa có chủ đích, least privilege sẽ suy thoái thành over-privilege.
Tài liệu tham khảo
- IAM User Guide
- IAM security best practices
- Policies and permissions in IAM
- IAM JSON policy elements reference
- How IAM policies are evaluated
- IAM roles
- AWS STS AssumeRole
- Cross-account access with IAM roles
- Permissions boundaries
- Service Control Policies (SCPs)
- IAM Identity Center User Guide
- IAM Access Analyzer
- Configuring OpenID Connect in AWS (GitHub Actions docs)
- aws-actions/configure-aws-credentials
- Root user best practices
- Using MFA in AWS
Part of the Cloud knowledge base — AWS deep dive. Complements DevOps Roadmap.
Overview
AWS Identity and Access Management (IAM) is the authorization system that decides who can do what to which resource under what conditions. Every single AWS API call — from the console, CLI, SDK, or one service calling another — is evaluated by IAM before it runs. If you understand IAM deeply, AWS security is largely a solved problem; if you don’t, you will either be perpetually blocked by Access Denied or, far worse, dangerously over-permissive.
IAM is a global service (an IAM role or user works in every region) and it is free. It has no availability zone; its control plane lives in us-east-1. The mental model is a giant, always-on policy engine: it receives a request (“principal alice wants s3:GetObject on arn:aws:s3:::reports/q3.pdf”), gathers every policy that could apply, and returns allow or deny.
The through-line of this page is a progression: from long-lived users (avoid) toward short-lived roles and federation (prefer). Modern AWS security is built on temporary credentials issued by STS, identities federated from an external IdP, and least-privilege policies scoped tightly to what a workload actually needs. Long-lived access keys are the legacy path and the source of most credential leaks.
Fundamentals
The four building blocks
| Entity | What it is | Has credentials? | Use for |
|---|---|---|---|
| User | A named human or app identity with long-lived credentials | Yes (password and/or access keys) | Legacy; avoid for humans — prefer SSO |
| Group | A collection of users; policies attach to the group | No | Organizing users by job function |
| Role | An identity assumed temporarily; yields short-lived credentials | No permanent creds — issued on assume | The preferred way to grant access to anything |
| Policy | A JSON document listing permissions | N/A | Attached to users, groups, or roles |
A principal is whoever is making a request — an IAM user, an assumed role, an AWS service, or a federated identity. A role has two halves: a trust policy (who is allowed to assume it) and permission policies (what it can do once assumed). This two-part structure is the heart of nearly all secure AWS access patterns.
Policy types
| Type | Attached to | Purpose |
|---|---|---|
| Identity-based policy | User, group, or role | Grants permissions to that identity |
| Resource-based policy | A resource (S3 bucket, SQS queue, KMS key, Lambda) | Grants permissions on that resource, and names the principal |
| Managed policy | Reusable, versioned, attach to many identities | AWS-managed (e.g., AdministratorAccess) or customer-managed |
| Inline policy | Embedded in one identity, 1:1 lifecycle | One-off permissions tied to a single identity |
| Permissions boundary | Sets the maximum an identity can have | Delegation guardrail — caps effective permissions |
| SCP (Organizations) | OU or account | Org-wide ceiling; filters, never grants |
| Session policy | Passed at AssumeRole time | Further narrows a session’s permissions |
Identity-based vs resource-based is a crucial pairing. To let account B read your S3 bucket in account A, you can attach a resource-based bucket policy naming account B’s principal — no role assumption needed. For most cross-account access, resource-based policies (where the service supports them: S3, SQS, SNS, KMS, Lambda, Secrets Manager) are the cleaner tool.
Managed vs inline: prefer customer-managed policies — they’re reusable, versioned (you can roll back), and visible as first-class objects. Inline policies are for genuinely one-off permissions that should live and die with a single identity. AWS-managed policies like AdministratorAccess are convenient but often too broad; treat PowerUserAccess and *FullAccess policies as starting points to tighten, not endpoints.
Policy JSON anatomy
Every IAM policy is a JSON document. Learn to read this fluently — it’s the language of AWS permissions.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowReadSpecificBucket",
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::my-app-data",
"arn:aws:s3:::my-app-data/*"
],
"Condition": {
"IpAddress": { "aws:SourceIp": "203.0.113.0/24" },
"Bool": { "aws:SecureTransport": "true" }
}
}
]
}
| Element | Meaning |
|---|---|
Version | Always 2012-10-17 (the current policy language version) |
Sid | Optional human-readable statement id |
Effect | Allow or Deny |
Action | The API operations, e.g. s3:GetObject (service:Operation); supports wildcards |
Resource | ARN(s) the actions apply to; * means all |
Condition | Optional key-based tests that must all be true |
Principal | (resource-based only) who the statement applies to |
Note the two S3 ARNs: my-app-data (the bucket, for ListBucket) and my-app-data/* (objects, for GetObject). Bucket-level and object-level actions need different ARNs — a common mistake. The condition restricts to a source IP range and requires TLS.
The evaluation logic — how allow/deny is decided
IAM’s decision algorithm, simplified:
- Default = implicit deny. Nothing is allowed until something allows it.
- An explicit
Denyanywhere always wins — in any policy, SCP, or boundary. NoAllowcan override it. - A request is allowed only if an
Allowapplies and it isn’t cut off by any of: an SCP ceiling, a permissions boundary, a session policy, or a resource policy (for cross-account, both sides must allow).
- ?Explicit Deny?yesDENY
- ?SCP allows?noDENY
- ?Boundary allows?noDENY
- ?Identity/resource Allow present?noDENY (implicit)
The two rules that matter most in practice: explicit deny always wins, and cross-account access requires an allow on both the identity side and the resource side.
Key Concepts
STS and AssumeRole — temporary credentials
AWS Security Token Service (STS) issues short-lived credentials (an access key, secret key, and session token) that expire in 15 minutes to 12 hours. Roles are assumed via STS. This is the mechanism behind cross-account access, federation, EC2/Lambda service roles, and break-glass admin.
A role’s trust policy says who may assume it:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::111122223333:root" },
"Action": "sts:AssumeRole",
"Condition": { "StringEquals": { "sts:ExternalId": "unique-shared-secret" } }
}]
}
This trusts account 111122223333 to assume the role, but only if it presents the agreed ExternalId — the standard defense against the “confused deputy” problem when granting access to third parties.
The assume flow from the CLI:
# 1. Assume the role — STS returns temporary credentials
aws sts assume-role \
--role-arn arn:aws:iam::444455556666:role/CrossAccountReadOnly \
--role-session-name alice-audit \
--external-id unique-shared-secret
# Response contains Credentials { AccessKeyId, SecretAccessKey, SessionToken, Expiration }
# 2. Export them and act as the role
export AWS_ACCESS_KEY_ID=ASIA...
export AWS_SECRET_ACCESS_KEY=...
export AWS_SESSION_TOKEN=...
aws s3 ls # now runs with the assumed role's permissions
In practice you never copy-paste credentials — you configure a named profile with role_arn + source_profile in ~/.aws/config and the CLI assumes the role transparently:
[profile audit]
role_arn = arn:aws:iam::444455556666:role/CrossAccountReadOnly
source_profile = default
external_id = unique-shared-secret
Then aws s3 ls --profile audit just works.
Cross-account access — the two patterns
- Role assumption (most common): Account A creates a role trusting Account B; B’s identities call
sts:AssumeRole. Good when B needs to act inside A. This is how CI/CD deploys into prod, how an audit account reads everything, how a hub-and-spoke org works. - Resource-based policy: Account A’s S3 bucket / SQS queue / KMS key directly names B’s principal in its policy. Good for sharing a specific resource without B assuming anything. Fewer moving parts for single-resource sharing.
The permission-scoping hierarchy
Multiple layers can restrict an identity. Effective permissions = the intersection of everything that could allow, minus any explicit deny:
| Layer | Set by | Effect |
|---|---|---|
| SCP | Org admin | Org-wide ceiling on the account |
| Permissions boundary | Delegating admin | Max an individual identity can have |
| Identity policy | Whoever manages the identity | What it’s granted |
| Session policy | Passed at AssumeRole | Narrows this one session |
| Resource policy | Resource owner | Grants on the resource (and enables cross-account) |
A permission is effective only if every applicable ceiling (SCP, boundary, session) permits it and an identity or resource policy grants it and nothing denies it. Permissions boundaries are how you safely let developers create their own roles: you grant them iam:CreateRole but require every role they make to carry a boundary, so they can never escalate beyond it.
IAM Identity Center (formerly AWS SSO)
IAM Identity Center is the modern front door for human access across a multi-account org. Instead of IAM users in every account, users log in once (against the built-in directory or an external IdP like Okta/Entra ID/Google), then get a portal of accounts and permission sets they can access. Behind the scenes it provisions roles and issues short-lived STS credentials — no long-lived keys anywhere.
- Permission sets are reusable role templates (e.g.,
AdministratorAccess,ReadOnly,Billing) assigned to users/groups per account. aws configure ssowires the CLI to it; credentials auto-refresh and expire.- This is the recommended default for all human access to AWS today.
Federation and OIDC — including GitHub Actions
Federation lets identities from an external system assume AWS roles without any AWS-side long-lived credentials. Two protocols:
- SAML 2.0 — enterprise IdPs (ADFS, Okta) for console/CLI SSO.
- OIDC — the modern token-based approach, ideal for CI/CD and workloads.
The killer use case is GitHub Actions → AWS with zero stored secrets. Instead of putting an AWS access key in GitHub, you register GitHub’s OIDC provider in IAM and create a role that trusts tokens from your specific repo:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com" },
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:ref:refs/heads/main"
}
}
}]
}
The sub condition is the security-critical line: it restricts assumption to workflows on main of exactly my-org/my-repo. Loosen it carelessly (e.g., repo:my-org/*) and any repo in your org — or a fork’s pull request — could assume the role. In the workflow:
permissions:
id-token: write # required to request the OIDC token
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubDeploy
aws-region: ap-southeast-1
No secret is stored anywhere; GitHub mints a short-lived OIDC token, AWS verifies it against the trust policy, and STS returns temporary credentials scoped to the role. The same pattern (AssumeRoleWithWebIdentity) powers EKS IRSA/Pod Identity and mobile app federation.
Least privilege and Access Analyzer
Least privilege means granting only the permissions a principal actually uses — no more. Getting there:
- Start narrow and widen on real Access Denied errors, rather than starting with
*and never tightening. - IAM Access Analyzer does the heavy lifting: it flags resources shared externally (public buckets, cross-account roles), and its policy generation feature reads CloudTrail history to produce a least-privilege policy from what an identity actually did over the last N days.
- Access Analyzer unused-access findings surface roles, users, and permissions that haven’t been used, so you can prune them.
- Use
aws iam simulate-principal-policyto test whether a policy allows an action before deploying it.
MFA and root account handling
- Root user: every account has an all-powerful root identity tied to the sign-up email. Lock it down: enable MFA (hardware key ideally), delete its access keys, never use it day-to-day. A few tasks genuinely require root (changing account settings, closing the account) — everything else uses IAM.
- MFA everywhere: require MFA for all human users and for sensitive actions. You can gate high-risk permissions behind an MFA condition:
"Condition": { "Bool": { "aws:MultiFactorAuthPresent": "true" } }. - Conditional MFA for AssumeRole: trust policies can require the caller to have authenticated with MFA before assuming a privileged role.
Best Practices
-
Retire long-lived credentials in favor of roles and STS. Temporary credentials expire automatically, so a leaked one is worthless within hours. Every human should log in through IAM Identity Center, and every workload (EC2, Lambda, ECS, EKS) should use an attached role — never an embedded access key.
-
Lock down the root user and never use it for daily work. Enable hardware MFA, delete its access keys, store its password in a physical safe, and set up alerts on any root activity. Root can bypass most controls, so any use of it should be a deliberate, logged, break-glass event.
-
Enforce MFA for all human access. Require MFA at sign-in and gate sensitive actions behind an
aws:MultiFactorAuthPresentcondition. MFA is the single highest-leverage control against credential theft. -
Grant least privilege and iterate toward it. Start with narrow permissions and widen only on demonstrated need. Use Access Analyzer’s policy generation to derive real-usage policies from CloudTrail, and its unused-access findings to prune what’s gone stale.
-
Prefer customer-managed policies over inline, and over broad AWS-managed ones. Managed policies are reusable, versioned, and roll-back-able; inline policies hide permissions inside identities. Treat
*FullAccessandPowerUserAccessas starting points to tighten, not as final answers. -
Use groups (or permission sets) for humans, roles for workloads. Never attach policies directly to individual users at scale — assign them via groups or Identity Center permission sets so access is consistent, auditable, and easy to revoke by moving someone out of a group.
-
Set permissions boundaries when delegating IAM. If developers can create roles, require every role they create to carry a boundary policy. This lets teams self-serve without any path to privilege escalation beyond the ceiling you set.
-
Use SCPs for org-wide guardrails. Enforce invariants centrally — restrict to approved regions, deny disabling CloudTrail/Config, block leaving the org, protect security resources from deletion. SCPs stop entire classes of mistakes regardless of what account admins do.
-
Scope conditions tightly, especially in trust policies. For GitHub OIDC, pin the
subclaim to an exact repo and branch; for cross-account roles, useExternalIdto defeat the confused-deputy problem. A too-broadPrincipalor condition is how one team’s trust becomes everyone’s backdoor. -
Adopt OIDC federation for CI/CD instead of stored keys. GitHub Actions, GitLab, and others can assume AWS roles via short-lived OIDC tokens. This removes the single most commonly leaked secret — a static AWS access key sitting in a CI settings page.
-
Require both sides to allow for cross-account access, and keep it explicit. Understand that a resource policy and the caller’s identity policy must both permit the action. Document every cross-account trust relationship; unexplained trust policies are how attackers pivot between accounts.
-
Rotate any remaining long-lived keys and alert on their use. Where access keys are unavoidable (legacy integrations), rotate them on a schedule, scope them minimally, and monitor with CloudTrail. Use Access Analyzer and credential reports to find keys that are old, unused, or over-privileged.
-
Enable IAM Access Analyzer in every account. It continuously flags resources exposed outside your account or org — public S3 buckets, over-shared KMS keys, wide cross-account roles — turning a manual audit into an always-on check.
-
Test policies before deploying them. Use
iam simulate-principal-policyand the console policy simulator to confirm a change grants exactly what you intend and nothing more. Explicit-deny interactions and boundary intersections are easy to get subtly wrong. -
Log and audit all IAM activity centrally. Send CloudTrail to a locked-down Log Archive account so every AssumeRole, policy change, and access-key use is captured immutably. This trail is your primary evidence during an incident and your proof of control during an audit.
-
Review IAM regularly and remove what’s unused. Generate credential reports, delete inactive users, prune unused roles and permissions, and re-verify that permission sets still match job functions. Access grants accrete silently; without deliberate pruning, least privilege decays into over-privilege.
References
- IAM User Guide
- IAM security best practices
- Policies and permissions in IAM
- IAM JSON policy elements reference
- How IAM policies are evaluated
- IAM roles
- AWS STS AssumeRole
- Cross-account access with IAM roles
- Permissions boundaries
- Service Control Policies (SCPs)
- IAM Identity Center User Guide
- IAM Access Analyzer
- Configuring OpenID Connect in AWS (GitHub Actions docs)
- aws-actions/configure-aws-credentials
- Root user best practices
- Using MFA in AWS