← Cloud · AWS← Cloud · AWS
Cloud · AWSCloud · AWS19 Th7, 2026Jul 19, 202619 phút đọc14 min read

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

EntityNó là gìCó credentials?Dùng cho
UserMộ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àiCó (password và/hoặc access key)Legacy; tránh dùng cho con người — ưu tiên SSO
GroupMột tập hợp các user; policy gắn vào groupKhôngTổ chức user theo chức năng công việc
RoleMột danh tính được assume tạm thời; cấp ra credentials ngắn hạnKhông có creds cố định — được cấp khi assumeCách được ưu tiên để cấp quyền truy cập cho mọi thứ
PolicyMột tài liệu JSON liệt kê các quyềnN/AGắ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ạiGắn vàoMục đích
Identity-based policyUser, group, hoặc roleCấp quyền cho danh tính đó
Resource-based policyMộ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 policyCó thể tái sử dụng, có version, gắn vào nhiều danh tínhDo AWS quản lý (ví dụ AdministratorAccess) hoặc do khách hàng quản lý
Inline policyNhúng vào một danh tính, vòng đời 1:1Quyề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 accountTrần quyền toàn tổ chức; lọc bớt, không bao giờ cấp quyền
Session policyTruyền vào lúc AssumeRoleThu 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
VersionLuôn là 2012-10-17 (phiên bản ngôn ngữ policy hiện hành)
SidId statement tùy chọn, dễ đọc cho con người
EffectAllow hoặc Deny
ActionCá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ả
ConditionCá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:

  1. Mặc định = implicit deny. Không gì được phép cho tới khi có thứ gì đó cho phép.
  2. Một Deny tường minh ở bất cứ đâu luôn thắng — trong bất kỳ policy, SCP, hay boundary nào. Không Allow nào ghi đè được nó.
  3. Một request chỉ được phép nếu có một Allow áp dụng 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).
IAM policy evaluation
  1. ?Explicit Deny?
    yes
    DENY
  2. ?SCP allows?
    no
    DENY
  3. ?Boundary allows?
    no
    DENY
  4. ?Identity/resource Allow present?
    no
    DENY (implicit)
yes
ALLOW

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

  1. 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.
  2. 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ởiHiệu ứng
SCPOrg adminTrần quyền toàn tổ chức áp lên account
Permissions boundaryAdmin ủy quyềnMức tối đa một danh tính cá nhân có thể có
Identity policyNgười quản lý danh tính đóNhững gì nó được cấp
Session policyTruyền vào lúc AssumeRoleThu hẹp riêng session này
Resource policyChủ sở hữu tài nguyênCấ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ó một identity hoặc resource policy cấp nó 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.

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:

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 đó:

MFA và xử lý root account

Best Practices

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. Ư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 *FullAccessPowerUserAccess là điểm khởi đầu để siết chặt, không phải câu trả lời cuối cùng.

  6. 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.

  7. Đặ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.

  8. 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ì.

  9. Giới hạn condition thật chặt, đặc biệt trong trust policy. Với GitHub OIDC, ghim claim sub vào đúng một repo và nhánh; với cross-account role, dùng ExternalId để đánh bại vấn đề confused-deputy. Một Principal hoặ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.

  10. Á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.

  11. 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.

  12. 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.

  13. 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.

  14. Kiểm thử policy trước khi deploy chúng. Dùng iam simulate-principal-policy và 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.

  15. 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.

  16. 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

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

EntityWhat it isHas credentials?Use for
UserA named human or app identity with long-lived credentialsYes (password and/or access keys)Legacy; avoid for humans — prefer SSO
GroupA collection of users; policies attach to the groupNoOrganizing users by job function
RoleAn identity assumed temporarily; yields short-lived credentialsNo permanent creds — issued on assumeThe preferred way to grant access to anything
PolicyA JSON document listing permissionsN/AAttached 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

TypeAttached toPurpose
Identity-based policyUser, group, or roleGrants permissions to that identity
Resource-based policyA resource (S3 bucket, SQS queue, KMS key, Lambda)Grants permissions on that resource, and names the principal
Managed policyReusable, versioned, attach to many identitiesAWS-managed (e.g., AdministratorAccess) or customer-managed
Inline policyEmbedded in one identity, 1:1 lifecycleOne-off permissions tied to a single identity
Permissions boundarySets the maximum an identity can haveDelegation guardrail — caps effective permissions
SCP (Organizations)OU or accountOrg-wide ceiling; filters, never grants
Session policyPassed at AssumeRole timeFurther 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" }
      }
    }
  ]
}
ElementMeaning
VersionAlways 2012-10-17 (the current policy language version)
SidOptional human-readable statement id
EffectAllow or Deny
ActionThe API operations, e.g. s3:GetObject (service:Operation); supports wildcards
ResourceARN(s) the actions apply to; * means all
ConditionOptional 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:

  1. Default = implicit deny. Nothing is allowed until something allows it.
  2. An explicit Deny anywhere always wins — in any policy, SCP, or boundary. No Allow can override it.
  3. A request is allowed only if an Allow applies 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).
IAM policy evaluation
  1. ?Explicit Deny?
    yes
    DENY
  2. ?SCP allows?
    no
    DENY
  3. ?Boundary allows?
    no
    DENY
  4. ?Identity/resource Allow present?
    no
    DENY (implicit)
yes
ALLOW

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

  1. 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.
  2. 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:

LayerSet byEffect
SCPOrg adminOrg-wide ceiling on the account
Permissions boundaryDelegating adminMax an individual identity can have
Identity policyWhoever manages the identityWhat it’s granted
Session policyPassed at AssumeRoleNarrows this one session
Resource policyResource ownerGrants 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.

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:

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:

MFA and root account handling

Best Practices

  1. 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.

  2. 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.

  3. Enforce MFA for all human access. Require MFA at sign-in and gate sensitive actions behind an aws:MultiFactorAuthPresent condition. MFA is the single highest-leverage control against credential theft.

  4. 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.

  5. 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 *FullAccess and PowerUserAccess as starting points to tighten, not as final answers.

  6. 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.

  7. 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.

  8. 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.

  9. Scope conditions tightly, especially in trust policies. For GitHub OIDC, pin the sub claim to an exact repo and branch; for cross-account roles, use ExternalId to defeat the confused-deputy problem. A too-broad Principal or condition is how one team’s trust becomes everyone’s backdoor.

  10. 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.

  11. 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.

  12. 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.

  13. 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.

  14. Test policies before deploying them. Use iam simulate-principal-policy and 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.

  15. 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.

  16. 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