IAM: User, Role và Policy đúng cách

1. Tổng quan

IAM (Identity and Access Management) là dịch vụ nền tảng của AWS, quyết định “ai được làm gì với cái gì”. Nhiều team dùng sai ngay từ đầu: tạo user có quyền admin cho mọi thứ, gắn policy *:* cho nhanh, hoặc nhầm lẫn giữa user và role. Bài này giúp bạn phân biệt rõ ba khái niệm user, role, policy, viết policy JSON đúng cú pháp, áp dụng nguyên tắc least privilege, và test việc assume role bằng AWS SDK cho Java.

Đọc xong, bạn sẽ tự tin thiết kế một bộ IAM gọn, an toàn, không phải “quyền god-mode” cho mọi service.

2. Ba thành phần cốt lõi

IAM xoay quanh ba khái niệm:

  • User: định danh cho một người hoặc một ứng dụng cụ thể, có credentials lâu dài (password, access key).
  • Role: định danh tạm thời, không có credentials cố định, được “assume” (giả lập) bởi user, service, hoặc tài khoản khác.
  • Policy: văn bản JSON mô tả quyền (allow/deny) được gắn vào user, role, group, hoặc resource.

Lưu ý: Role không phải là “user không có password”. Role sinh ra temporary credentials (có thời hạn, qua STS), còn user dùng credentials cố định cho đến khi bạn xoay (rotate) chúng.

3. User: khi nào dùng, khi nào không

User phù hợp cho:

  • Con người cần đăng nhập Console để làm việc thủ công.
  • Trường hợp bất khả kháng không dùng được role (hiếm, ví dụ hệ thống legacy ngoài AWS cần access key cố định).

User không phù hợp cho:

  • Workload chạy trên EC2, Lambda, ECS — những nơi này nên dùng role.
  • Chia sẻ một user cho nhiều người (mất khả năng audit theo CloudTrail).

Tạo user bằng CLI:

aws iam create-user --user-name order-service-deployer

aws iam create-login-profile \
  --user-name order-service-deployer \
  --password 'TempPass123!' \
  --password-reset-required

Dòng đầu tạo user. Dòng create-login-profile cấp quyền đăng nhập Console với password tạm, buộc đổi ngay lần đầu — tránh để password mặc định tồn tại lâu.

Lỗi hay gặp: tạo access key cho user rồi hard-code vào code, commit lên git. Nếu bắt buộc phải dùng access key, hãy bật rotation định kỳ và dùng Secrets Manager để lưu trữ.

4. Role: định danh tạm thời đúng chỗ

4.1. EC2 instance role

Khi một EC2 instance cần gọi S3, đừng nhúng access key vào .env. Hãy tạo role và gắn instance profile:

aws iam create-role \
  --role-name OrderServiceEc2Role \
  --assume-role-policy-document file://trust-policy.json

File trust-policy.json xác định ai được assume role này:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "Service": "ec2.amazonaws.com" },
      "Action": "sts:AssumeRole"
    }
  ]
}

Principal.Service chỉ định service EC2 được phép assume role này thông qua instance profile. Đây gọi là trust policy — khác với permission policy mô tả ở phần sau.

4.2. Cross-account role

Role cũng dùng để cho phép tài khoản AWS khác truy cập vào tài khoản của bạn mà không cần chia sẻ credentials:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::111122223333:root" },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": { "sts:ExternalId": "order-partner-2024" }
      }
    }
  ]
}

Condition với sts:ExternalId chống lại confused deputy attack — bên thứ ba phải biết đúng ExternalId bí mật mới assume được role, tránh trường hợp một bên thứ tư lợi dụng ARN công khai.

5. Policy: cấu trúc và cách viết

Một policy JSON gồm Version, Statement — mỗi statement có Effect, Action, Resource, và tùy chọn Condition.

Ví dụ: cho phép PaymentService đọc/viết vào một bucket S3 cụ thể, không được xóa:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowReadWritePaymentBucket",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject"
      ],
      "Resource": "arn:aws:s3:::payment-receipts-bucket/*"
    },
    {
      "Sid": "DenyDeleteObject",
      "Effect": "Deny",
      "Action": "s3:DeleteObject",
      "Resource": "arn:aws:s3:::payment-receipts-bucket/*"
    }
  ]
}

Statement thứ nhất Allow hai action GetObject và PutObject chỉ trên path payment-receipts-bucket/*. Statement thứ hai Deny tường minh hành động xóa — dù sau này có ai gắn thêm policy s3:*, deny tường minh vẫn thắng.

Gắn policy này vào role:

aws iam put-role-policy \
  --role-name PaymentServiceRole \
  --policy-name PaymentBucketAccess \
  --policy-document file://payment-bucket-policy.json

5.1. Identity-based vs resource-based policy

  • Identity-based policy: gắn vào user/role/group, trả lời “identity này được làm gì”.
  • Resource-based policy: gắn trực tiếp vào resource (S3 bucket policy, SQS queue policy, KMS key policy), trả lời “ai được truy cập resource này”.

Ví dụ bucket policy (resource-based) chỉ cho phép role cụ thể truy cập:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::123456789012:role/PaymentServiceRole"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::payment-receipts-bucket/*"
    }
  ]
}

Hai loại policy này có thể kết hợp — quyền cuối cùng là giao của cả hai (cả identity-based và resource-based phải cho phép, nếu một trong hai không cho phép thì bị chặn, trừ trường hợp cross-account phải có cả hai allow).

6. Nguyên tắc least privilege

Thay vì viết "Action": "s3:*" cho nhanh, hãy liệt kê đúng action cần dùng. Dùng Condition để siết phạm vi hơn nữa.

Ví dụ: chỉ cho phép đọc object nếu request đến từ VPC cụ thể:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::order-exports-bucket/*",
      "Condition": {
        "StringEquals": {
          "aws:SourceVpc": "vpc-0abc1234"
        }
      }
    }
  ]
}

aws:SourceVpc là global condition key, chỉ allow nếu request xuất phát từ VPC endpoint của VPC chỉ định — hữu ích khi bạn không muốn dữ liệu rời khỏi network nội bộ.

Cách tiếp cận thực tế để đạt least privilege:

  1. Bắt đầu với policy rỗng (deny mặc định).
  2. Chạy workload, xem IAM Access Analyzer hoặc CloudTrail log action nào bị từ chối.
  3. Thêm đúng action đó vào policy, không thêm thừa.
  4. Lặp lại cho đến khi workload chạy ổn định mà không bị chặn.

Lưu ý: Dùng aws iam generate-service-last-accessed-details để xem user/role đã thực sự dùng những action nào trong 90 ngày — từ đó cắt bớt action dư thừa trong policy hiện có.

7. Policy evaluation logic cần nhớ

AWS đánh giá quyền theo thứ tự sau, bạn cần nhớ để debug nhanh:

  1. Mặc định tất cả bị deny (implicit deny).
  2. Có bất kỳ statement nào ghi Deny tường minh → kết quả cuối là deny, không gì cứu được.
  3. Nếu không có deny tường minh, tìm statement Allow phù hợp → allow.
  4. Nếu không có allow nào khớp → vẫn deny (implicit deny mặc định).
Request đến → Có Deny tường minh khớp? 
  ├─ Có → DENY (kết thúc)
  └─ Không → Có Allow khớp?
       ├─ Có → ALLOW
       └─ Không → DENY (implicit)

Hiểu đúng thứ tự này giúp bạn tránh bug kinh điển: gắn policy AdministratorAccess cho user nhưng vẫn bị chặn một action, vì có SCP (Service Control Policy) ở cấp Organization deny tường minh action đó.

8. Test assume role bằng AWS SDK cho Java

Thêm dependency SDK v2:

<dependency>
    <groupId>software.amazon.awssdk</groupId>
    <artifactId>sts</artifactId>
    <version>2.28.11</version>
</dependency>

Service gọi STS để assume role và lấy credentials tạm:

public class RoleAssumer {

    private final StsClient stsClient;

    public RoleAssumer(StsClient stsClient) {
        this.stsClient = stsClient;
    }

    public Credentials assumePaymentServiceRole() {
        AssumeRoleRequest request = AssumeRoleRequest.builder()
                .roleArn("arn:aws:iam::123456789012:role/PaymentServiceRole")
                .roleSessionName("payment-service-session")
                .durationSeconds(900)
                .build();

        AssumeRoleResponse response = stsClient.assumeRole(request);
        return response.credentials();
    }
}

roleArn chỉ định role cần assume, roleSessionName đặt tên session để dễ truy vết trong CloudTrail, durationSeconds giới hạn thời gian sống của credentials tạm — càng ngắn càng an toàn.

Test với JUnit 5 và Mockito để kiểm tra logic gọi đúng role ARN, không cần gọi AWS thật:

@Test
void assumePaymentServiceRole_shouldRequestCorrectRoleArn() {
    StsClient stsClient = mock(StsClient.class);
    Credentials fakeCredentials = Credentials.builder()
            .accessKeyId("AKIAFAKE")
            .secretAccessKey("secretFake")
            .sessionToken("tokenFake")
            .build();

    when(stsClient.assumeRole(any(AssumeRoleRequest.class)))
            .thenReturn(AssumeRoleResponse.builder()
                    .credentials(fakeCredentials)
                    .build());

    RoleAssumer assumer = new RoleAssumer(stsClient);
    Credentials result = assumer.assumePaymentServiceRole();

    ArgumentCaptor<AssumeRoleRequest> captor = ArgumentCaptor.forClass(AssumeRoleRequest.class);
    verify(stsClient).assumeRole(captor.capture());

    assertThat(captor.getValue().roleArn())
            .isEqualTo("arn:aws:iam::123456789012:role/PaymentServiceRole");
    assertThat(result.accessKeyId()).isEqualTo("AKIAFAKE");
}

Test này mock StsClient, capture request thực tế gửi đi, rồi assert roleArn đúng như mong đợi. Cách này giúp bạn phát hiện sớm nếu ai đó vô tình sửa hard-code sai ARN role trong code.

assumePaymentServiceRole_shouldRequestCorrectRoleArn() PASSED

9. Lỗi thường gặp cần tránh

  • Gắn AdministratorAccess cho mọi service account vì “để sau tối ưu”. Thực tế không ai quay lại tối ưu, rủi ro tồn tại vĩnh viễn.
  • Dùng access key cho workload trên EC2/Lambda thay vì role — access key dễ leak, không tự xoay, không có thời hạn.
  • Quên review trust policy của role: Principal quá rộng (ví dụ "AWS": "*") khiến bất kỳ ai cũng assume được.
  • Không dùng Condition với MFA cho hành động nhạy cảm (xóa bucket, sửa IAM policy). Thêm "Bool": {"aws:MultiFactorAuthPresent": "true"} cho các action rủi ro cao.
  • Trộn nhiều trách nhiệm vào một policy lớn khiến khó review. Hãy tách theo resource/service, đặt tên rõ (PaymentBucketReadWrite, không đặt MyPolicy1).

Khi nào KHÔNG cần IAM phức tạp: Nếu bạn đang làm sandbox cá nhân, học thử một dịch vụ mới, việc tạo role/policy chi tiết ngay từ đầu có thể làm chậm tốc độ học. Lúc đó dùng user tạm với policy managed có sẵn (AmazonS3ReadOnlyAccess) là đủ, miễn nhớ xóa sau khi xong.

10. Kết luận

User dành cho người cần đăng nhập thủ công, role dành cho workload và truy cập tạm thời, policy là bộ luật JSON quyết định allow/deny. Nắm chắc thứ tự đánh giá (deny tường minh luôn thắng, implicit deny là mặc định) giúp bạn debug nhanh khi quyền “kỳ lạ” xảy ra. Áp dụng least privilege bằng cách bắt đầu từ rỗng, thêm dần dựa trên log thực tế, và luôn viết test cho logic assume role trong code — đó là cách giữ hệ thống AWS của bạn an toàn mà không tốn công bảo trì về sau.

awsiamsecuritycloudjava