IAM: User, Role과 Policy 올바르게 이해하기
1. 개요
IAM (Identity and Access Management)는 AWS의 기반 서비스로, “누가 무엇을 어디에서 할 수 있는가”를 결정합니다. 많은 팀이 처음부터 잘못 사용합니다: 모든 것에 admin 권한을 가진 user를 만들거나, 빨리 처리하려고 *:* policy를 붙이거나, user와 role을 헷갈려 합니다. 이 글은 user, role, policy라는 세 가지 개념을 명확히 구분하고, JSON policy를 올바르게 작성하고, 최소 권한 원칙을 적용하고, AWS SDK for Java를 사용해 role assumption을 테스트하는 방법을 알려줍니다.
이 글을 읽고 나면, 간결하고 안전하며 모든 서비스에 “god-mode” 권한을 주지 않는 IAM 설계에 자신감을 가질 수 있을 것입니다.
2. 세 가지 핵심 요소
IAM은 세 가지 개념을 중심으로 돌아갑니다:
- User: 특정 사람 또는 애플리케이션의 정체성으로, 오래 지속되는 자격증명(password, access key)을 가집니다.
- Role: 임시 정체성으로 고정 자격증명이 없으며, user, service 또는 다른 계정에 의해 “assume”(가정)됩니다.
- Policy: user, role, group 또는 리소스에 붙는 JSON 텍스트로, 권한(allow/deny)을 설명합니다.
참고: Role은 “password 없는 user”가 아닙니다. Role은 임시 자격증명(시간 제한이 있고 STS를 통해 발급)을 생성하는 반면, user는 사용자가 회전(rotate)할 때까지 고정된 자격증명을 사용합니다.
3. User: 언제 사용하고 언제 사용하지 않을까요
User는 다음의 경우에 적합합니다:
- 사람이 Console에 로그인하여 수동으로 작업해야 할 때
- Role을 사용할 수 없는 불가피한 경우 (드물게, 예: AWS 외부의 레거시 시스템이 고정된 access key가 필요한 경우)
User는 다음의 경우에는 적합하지 않습니다:
- EC2, Lambda, ECS에서 실행되는 workload — 이런 경우는 role을 사용해야 합니다.
- 여러 사람이 하나의 user를 공유하는 경우 (CloudTrail에서 감사 추적이 불가능)
CLI로 user를 만드는 방법:
aws iam create-user --user-name order-service-deployer
aws iam create-login-profile \
--user-name order-service-deployer \
--password 'TempPass123!' \
--password-reset-required
첫 번째 줄이 user를 만듭니다. create-login-profile은 임시 password로 Console 로그인 권한을 부여하고 처음 로그인할 때 반드시 변경하도록 강제합니다 — 기본 password가 오래 유지되는 것을 방지합니다.
흔한 실수: user를 만들고 access key를 code에 hard-code하고 git에 commit합니다. access key를 꼭 사용해야 한다면 주기적인 회전을 활성화하고 Secrets Manager를 사용해 저장하세요.
4. Role: 올바른 곳의 임시 정체성
4.1. EC2 instance role
EC2 instance가 S3를 호출해야 할 때, access key를 .env에 embed하지 마세요. role을 만들고 instance profile에 붙이세요:
aws iam create-role \
--role-name OrderServiceEc2Role \
--assume-role-policy-document file://trust-policy.json
trust-policy.json 파일은 누가 이 role을 assume할 수 있는지를 정의합니다:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "Service": "ec2.amazonaws.com" },
"Action": "sts:AssumeRole"
}
]
}
Principal.Service는 EC2 service가 instance profile을 통해 이 role을 assume할 수 있음을 지정합니다. 이것을 trust policy라고 부르는데, 다음 섹션에서 설명할 permission policy와는 다릅니다.
4.2. Cross-account role
Role은 또한 다른 AWS 계정이 자격증명을 공유하지 않고도 귀하의 계정에 접근할 수 있게 해줍니다:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::111122223333:root" },
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": { "sts:ExternalId": "order-partner-2024" }
}
}
]
}
sts:ExternalId를 포함한 Condition은 confused deputy attack을 방지합니다 — 제3자가 비밀 ExternalId를 알아야만 role을 assume할 수 있으므로 제4자가 공개 ARN을 악용하는 상황을 피할 수 있습니다.
5. Policy: 구조 및 작성 방법
Policy JSON은 Version과 Statement로 구성됩니다 — 각 statement는 Effect, Action, Resource를 가지며, 선택적으로 Condition을 가집니다.
예: PaymentService가 특정 S3 bucket에 읽고 쓸 수 있지만 삭제는 못하도록 허용:
{
"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는 Allow GetObject와 PutObject 두 action을 payment-receipts-bucket/* 경로에만 허용합니다. 두 번째 statement는 삭제 작업을 명시적으로 Deny 합니다 — 나중에 누군가 s3:* policy를 추가하더라도 명시적 deny는 항상 우선입니다.
이 policy를 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: user/role/group에 붙이는 것으로, “이 정체성은 무엇을 할 수 있는가”를 답합니다.
- Resource-based policy: S3 bucket policy, SQS queue policy, KMS key policy처럼 리소스에 직접 붙는 것으로, “누가 이 리소스에 접근할 수 있는가”를 답합니다.
예: bucket policy (resource-based)로 특정 role만 접근 허용:
{
"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/*"
}
]
}
이 두 종류의 policy는 함께 작동할 수 있습니다 — 최종 권한은 identity-based와 resource-based 정책의 교집합입니다 (둘 다 명시적으로 allow해야 하고, 하나라도 deny하면 접근 불가).
6. 최소 권한 원칙
"Action": "s3:*"로 빨리 처리하지 마세요. 실제로 필요한 action을 나열하세요. Condition을 사용해 범위를 더욱 제한하세요.
예: 특정 VPC에서의 요청일 때만 object 읽기 허용:
{
"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는 글로벌 condition key로, 요청이 지정된 VPC의 VPC endpoint에서 왔을 때만 허용합니다 — 데이터가 내부 네트워크를 벗어나지 않도록 하려는 경우 유용합니다.
최소 권한을 달성하기 위한 실무적 접근:
- 빈 policy로 시작합니다 (기본 deny).
- workload를 실행하고 IAM Access Analyzer 또는 CloudTrail 로그에서 거부된 action을 확인합니다.
- 정확한 action만 policy에 추가합니다. 여분을 추가하지 않습니다.
- workload가 안정적으로 작동할 때까지 반복합니다.
팁:
aws iam generate-service-last-accessed-details를 사용해 user/role이 지난 90일 동안 실제로 사용한 action을 확인하세요 — 이를 통해 현재 policy에서 불필요한 action을 제거할 수 있습니다.
7. Policy 평가 로직 알아두기
AWS는 권한을 다음 순서대로 평가합니다. 권한 문제를 빠르게 디버그하려면 이 순서를 이해해야 합니다:
- 기본적으로 모든 것이 deny 됩니다 (implicit deny).
- 명시적
Denystatement가 일치하면 → 결과는 deny, 복구 불가능합니다. - 명시적 deny가 없으면, 일치하는
Allowstatement를 찾습니다 → allow. - 일치하는 allow가 없으면 → 여전히 deny (기본 implicit deny).
요청 도착 → 일치하는 명시적 Deny 있음?
├─ 있음 → DENY (종료)
└─ 없음 → 일치하는 Allow 있음?
├─ 있음 → ALLOW
└─ 없음 → DENY (implicit)
이 순서를 올바르게 이해하면 일반적인 버그를 피할 수 있습니다: user에게 AdministratorAccess를 붙였는데도 특정 action이 차단되는 경우, 조직 수준의 SCP (Service Control Policy)에서 명시적으로 그 action을 deny하는 경우가 있습니다.
8. AWS SDK for Java를 사용한 role assumption 테스트
SDK v2 dependency를 추가하세요:
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>sts</artifactId>
<version>2.28.11</version>
</dependency>
Service가 STS를 호출해 role을 assume하고 임시 자격증명을 받습니다:
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은 assume할 role을 지정하고, roleSessionName은 CloudTrail에서 추적하기 쉽도록 session 이름을 설정하고, durationSeconds는 임시 자격증명의 수명을 제한합니다 — 짧을수록 안전합니다.
JUnit 5와 Mockito를 사용해 테스트하여 AWS를 실제로 호출하지 않고 올바른 role ARN을 요청하는지 확인합니다:
@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");
}
이 테스트는 StsClient를 mock하고, 실제로 전송된 요청을 capture한 다음, roleArn이 예상대로인지 assert합니다. 이 방법으로 누군가 실수로 code의 hard-coded role ARN을 변경하는 경우를 조기에 발견할 수 있습니다.
assumePaymentServiceRole_shouldRequestCorrectRoleArn() PASSED
9. 피해야 할 흔한 실수
- 모든 service account에
AdministratorAccess붙이기 (나중에 최적화할 것이라고 생각하며). 실제로는 아무도 돌아와서 최적화하지 않으므로 위험이 영구적으로 존재합니다. - EC2/Lambda의 workload에 access key 사용 — role 대신. access key는 leak하기 쉽고 자동으로 회전되지 않으며 만료 기간이 없습니다.
- role의 trust policy를 검토하지 않기:
Principal이 너무 광범위합니다 (예:"AWS": "*"). 이렇게 하면 누구나 assume할 수 있습니다. - 민감한 작업(bucket 삭제, IAM policy 수정)에 MFA
Condition을 사용하지 않기."Bool": {"aws:MultiFactorAuthPresent": "true"}를 높은 위험 action에 추가하세요. - 많은 책임을 하나의 큰 policy에 섞기 (검토가 어려워집니다). resource/service별로 나누고 명확하게 이름을 붙이세요 (
PaymentBucketReadWrite,MyPolicy1같은 이름 피하기).
이런 경우 복잡한 IAM은 불필요: 개인 sandbox를 만들거나 새로운 서비스를 배우려고 할 때 처음부터 role/policy를 자세히 만드는 것은 학습 속도를 늦출 수 있습니다. 그럴 때는 기존 managed policy (
AmazonS3ReadOnlyAccess)를 가진 임시 user를 만드는 것으로도 충분합니다. 끝나면 삭제하세요.
10. 결론
User는 수동 로그인이 필요한 사람을 위한 것이고, role은 workload와 임시 접근을 위한 것이고, policy는 allow/deny를 결정하는 JSON 규칙입니다. 평가 순서를 이해하면 (명시적 deny는 항상 이기고 implicit deny가 기본) 권한이 “이상하게” 작동할 때 빠르게 디버그할 수 있습니다. 최소 권한을 적용하되 빈 상태에서 시작하고 실제 로그를 기반으로 차근차근 추가하고, code의 role assumption 로직을 항상 테스트하세요 — 이것이 AWS 시스템을 안전하게 유지하면서도 유지보수 부담을 줄이는 방법입니다.