AWS IAM 역할 정책 설정, 처음부터 최소 권한이 정답일까 — 후행 좁히기 실전

목차

AWS IAM 역할 정책 설정을 처음부터 정밀하게 짜라는 게 모범 답안인 줄 알았다. 결과만 놓고 보면 정반대였다. 정밀 설계한 Role일수록 새벽 호출이 잦았고, 일단 넓게 주고 나중에 좁힌 Role은 사고가 거의 없었다. 같은 팀, 같은 워크로드에서 갈렸다.

Before / After — 정책 설계 순서를 뒤집고 나서

구간 Before (정밀 설계 우선) After (후행 좁히기)
신규 Role 배포 추정한 액션 목록만 허용 PowerUserAccess + Permission Boundary
첫 주 운영 AccessDenied 평균 3~5건/주 0~1건
정책 수정 빈도 격주로 패치 30일 후 1회
권한 상승 위험 Role 자체로 차단 Boundary로 차단
사고 대응 새벽 호출 → wildcard 땜빵 CloudWatch Alarm → 다음 영업일 처리

표가 단순해 보이지만 이 방식 차이가 운영 부담을 가른다. 정밀 설계 쪽은 "예측 실패"가 곧 장애였다. 후행 좁히기 쪽은 30일 동안 실제 호출을 다 받아본 뒤에 정책을 만들기 때문에 누락이 거의 없다. 처음 한 달의 "넓은 권한" 구간이 부담스럽게 들리지만, Boundary가 상한을 잡아주면 위험 자체는 통제 가능한 수준이다.

특히, 이 방식으로 바꾸고 나서 가장 좋았던 건 코드 리뷰 분위기였다. "이 액션이 빠지면 어쩌지" 같은 가정 토론이 사라졌다. 한 달 운영하고 로그를 보면 답이 나온다.

"처음부터 최소 권한"이라는 통념의 함정

IAM 가이드라인은 대부분 "처음부터 최소 권한"을 권한다. AWS Well-Architected Security Pillar(2025-12 업데이트 기준)도 같은 입장이다. 원칙으로는 맞는 말이다. 다만 그 원칙을 그대로 따랐을 때 현장에서 무슨 일이 벌어지는지가 자주 빠진다.

신규 서비스는 어떤 액션을 쓸지 정확히 모른다

게다가, boto3 한 줄이 내부적으로 부가 액션을 여러 개 호출한다. boto3.client("s3").upload_file() 하나만 봐도 s3:PutObject 외에 멀티파트면 s3:CreateMultipartUpload, s3:UploadPart, s3:CompleteMultipartUpload가 들어간다. KMS 암호화가 걸려 있으면 kms:GenerateDataKey도 필요하다. SDK 문서에 다 적혀 있지도 않다.

이걸 코드 정적 분석으로 잡아내려는 시도도 있지만, 런타임에 결정되는 호출이 많아서 완전하지 않다. 결국 운영하면서 막혀봐야 안다.

SDK 버전을 올리면 액션이 바뀐다

그런데, boto3 1.34에서 1.35로 올렸더니 SecretsManager 호출이 막힌 사례가 있다. 내부에서 새로운 메타데이터 API를 부르기 시작했기 때문이다. 코드는 그대로인데 권한만 부족해진다. 이런 변화를 정책 검토 사이클에 맞춰 추적하는 건 사실상 불가능하다.

결과는 wildcard 땜빵

권한 부족이 새벽에 터지면 정밀하게 좁힐 시간이 없다. s3:*, kms:*로 풀고 일단 살린다. 그리고 좁히는 작업은 미뤄진다. 두세 번 반복되면 처음에 정밀하게 짰던 정책이 실질적으로는 가장 넓은 상태가 된다.

CloudTrail 기반 후행 좁히기 — 실제 동작 방식

후행 좁히기는 발상이 단순하다. "실제 호출된 액션만 허용한다." 흐름은 이렇다.

flowchart LR
    A[Day 0<br/>PowerUserAccess<br/>+ Boundary 배포] --> B[Day 1-30<br/>CloudTrail 수집]
    B --> C[Day 31<br/>Access Analyzer<br/>Policy Generation]
    C --> D[Day 32<br/>Customer Managed<br/>Policy 교체]
    D --> E[Day 33+<br/>AccessDenied<br/>CloudWatch Alarm]
    E -.이벤트 발생.-> C

결국, 핵심은 Permission Boundary다. Boundary는 Role이 도달할 수 있는 권한의 상한이다. PowerUserAccess가 붙어 있어도 Boundary가 IAM 액션을 차단하면 Role은 IAM을 못 만진다. 즉 "넓지만 권한 상승은 불가능한" 상태가 된다.

물론, Boundary 예시는 이렇다.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowAllExceptDangerousActions",
      "Effect": "Allow",
      "NotAction": [
        "iam:*",
        "organizations:*",
        "account:*",
        "kms:ScheduleKeyDeletion",
        "kms:DisableKey"
      ],
      "Resource": "*"
    }
  ]
}

NotAction은 "여기 적힌 액션을 제외한 모든 액션"을 의미한다. 결과적으로 IAM, Organizations, 일부 위험한 KMS 액션만 막힌다. 일반적인 데이터 작업(S3, DynamoDB, SQS, Lambda 호출 등)은 그대로 허용된다.

그런데, :::tip Boundary는 Role을 만들 때 --permissions-boundary 옵션으로 지정한다. 이후에 추가하거나 변경하는 건 가능하지만, 신규 Role 생성 시 Boundary를 강제하려면 SCP가 필요하다. 뒤에서 다룬다. :::

Access Analyzer Policy Generation을 써본 결과

이처럼, IAM Access Analyzer의 Policy Generation 기능은 v2 기준 2023년 말 GA됐다(출처: AWS Blog, 2023-11). CloudTrail 로그를 읽어 실제 호출된 액션만 추려서 정책으로 만들어 준다.

그런데, 써보면서 정리한 장단점.

좋았던 점

  • 90일치 CloudTrail이 있으면 거의 누락이 없다. 빈도 낮은 액션도 잘 잡는다.
  • IAM 콘솔에서 Role 단위로 바로 실행 가능. 별도 도구 안 깔아도 된다.
  • 결과 JSON을 그대로 Customer Managed Policy로 만들 수 있다.

아쉬운 점

  • Resource 수준까지는 좁혀주지 않는다. 액션은 정확해도 Resource는 여전히 "*"로 나오는 경우가 많다. 이건 수동으로 좁혀야 한다.
  • Cross-account 호출은 정확도가 떨어진다. 다른 계정 리소스를 부르는 Role은 결과를 한 번 더 검토해야 한다.
  • 90일 이상 데이터를 분석하려면 CloudTrail Lake가 필요하다. 기본 CloudTrail 보존 한계가 있어서다.

CLI로 돌리는 명령은 이렇다.

# 정책 생성 작업 시작
aws accessanalyzer start-policy-generation \
  --policy-generation-details \
    principalArn=arn:aws:iam::111122223333:role/MyServiceRole \
  --cloud-trail-details \
    "accessRole=arn:aws:iam::111122223333:role/AnalyzerAccessRole,\
trails=[{cloudTrailArn=arn:aws:cloudtrail:ap-northeast-2:111122223333:trail/MyTrail}],\
startTime=2026-04-01T00:00:00Z"

# 결과 조회 (job-id로)
aws accessanalyzer get-generated-policy \
  --job-id <job-id> \
  --include-resource-placeholders

include-resource-placeholders 옵션을 주면 Resource를 ${PlaceholderName} 형태로 내려준다. 수작업으로 정확한 ARN을 채우기 편하다.

Role 설계 — Inline과 Customer Managed가 갈리는 지점

반면, Inline Policy와 Customer Managed Policy를 어디에 써야 할지 헷갈리는 경우가 많다. 비교는 단순하다.

항목 Inline Policy Customer Managed Policy
재사용 불가 여러 Role에 attach
버전 관리 Role과 함께 독립 (최대 5개 버전)
Role 삭제 시 같이 삭제 남음
IaC 관리 Role 정의 안에 포함 별도 리소스
변경 추적 Role 변경 이력으로만 정책 단위로 가능
적합한 경우 정말 1회성, Role 종속적 운영 환경 대부분

그런데, 운영 환경에서는 Customer Managed가 거의 항상 낫다. Terraform이나 CDK로 모듈화하기 쉽고, 정책 변경 이력을 별도로 추적할 수 있다. Inline은 EventBridge가 자동 생성하는 Role처럼 Role과 생명주기가 같이 가야 하는 경우에만 쓴다.

반면, 한 가지 빠지기 쉬운 함정. Inline Policy는 Role 정의 안에 박혀 있어서 IaC diff에서 잘 안 보인다. Terraform aws_iam_roleinline_policy 블록을 수정하면 Role 전체가 변경된 것처럼 표시된다. 추적성이 떨어진다.

실수 방지 패턴 — Permission Boundary와 SCP

그런데, Boundary 외에 한 층 더 있다. Service Control Policy(SCP)다. AWS Organizations 단위로 거는 가드레일이고, 계정 안의 모든 Principal에 적용된다.

그래서, 자주 쓰는 SCP 예시.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyRegionsOutsideKorea",
      "Effect": "Deny",
      "NotAction": [
        "iam:*",
        "organizations:*",
        "support:*",
        "cloudfront:*",
        "route53:*"
      ],
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "aws:RequestedRegion": ["ap-northeast-2"]
        }
      }
    },
    {
      "Sid": "RequireBoundaryOnNewRoles",
      "Effect": "Deny",
      "Action": [
        "iam:CreateRole",
        "iam:PutRolePermissionsBoundary"
      ],
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "iam:PermissionsBoundary":
            "arn:aws:iam::111122223333:policy/StandardBoundary"
        }
      }
    }
  ]
}

결국, 첫 Statement는 서울 리전 외에는 어떤 액션도 못 하게 막는다. 실수로 us-east-1에 리소스 만들고 청구서 보고 놀라는 사고를 예방한다. (개인적으로 이게 효과가 가장 컸다.)

그러나, 둘째 Statement는 신규 Role 생성 시 StandardBoundary를 반드시 붙이도록 강제한다. 누군가 Boundary 없이 Role을 만들려고 하면 SCP가 차단한다.

반면, Boundary와 SCP, Role 정책이 동시에 작동하면 권한 평가 순서가 복잡해진다. 평가 순서는 이렇다.

1. SCP가 허용하는가?
2. Resource-based Policy가 허용하는가?
3. Identity-based Policy(Role의 정책)가 허용하는가?
4. Permission Boundary가 허용하는가?
5. Session Policy가 허용하는가? (AssumeRole 시)

그러나, 다섯 단계 중 하나라도 막으면 끝이다. 그래서 AccessDenied가 떴을 때 정확히 어디서 막혔는지 찾는 게 별도의 기술이 된다.

권한 오류 디버깅 — AccessDenied 메시지에서 살아남기

이처럼, AccessDenied 메시지는 2024년 후반부터 많이 친절해졌다(출처: AWS re:Inforce 2024 keynote). 이전에는 "User is not authorized to perform" 한 줄로 끝났는데, 이제는 막힌 이유가 메시지에 들어간다.

User: arn:aws:sts::111122223333:assumed-role/MyServiceRole/i-0abc
is not authorized to perform: s3:GetObject
on resource: "arn:aws:s3:::my-bucket/key.txt"
because no identity-based policy allows the s3:GetObject action

게다가, 마지막 줄이 단서다.

  • no identity-based policy allows → Role 정책에 액션이 없다
  • explicit deny in a permissions boundary → Boundary가 차단
  • explicit deny in an organization policy → SCP가 차단
  • explicit deny in a resource-based policy → 리소스 정책이 차단

특히, 이 단서가 없으면 IAM 정책만 들여다보다가 한참을 헤맨다. SCP가 막은 걸 Role 문제로 오해해서 한 시간 날린 적이 있다. 메시지에 SCP라고 적혀 있는데 처음엔 그 단어가 눈에 안 들어왔다. 시행착오 한 번에 시간이 꽤 나간다.

결국, 진단 도구로는 IAM Policy Simulator가 있다. Role과 액션, 리소스를 넣으면 평가 결과를 보여준다.

aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::111122223333:role/MyServiceRole \
  --action-names s3:GetObject kms:Decrypt \
  --resource-arns arn:aws:s3:::my-bucket/key.txt \
                  arn:aws:kms:ap-northeast-2:111122223333:key/abcd-1234

즉, 결과 JSON의 EvalDecision이 핵심이다.

  • allowed → 통과
  • explicitDeny → 명시적 차단(Deny 문이 있음)
  • implicitDeny → 정책이 아예 없음

Simulator의 한계도 있다. SCP는 일부만 반영된다. 정확히 말하면 Simulator는 SCP를 직접 평가하지 않고, "이 Principal에 적용되는 SCP가 있다"는 사실 정도만 알려준다. 완전한 시뮬레이션이 아니다.

자주 막히는 패턴

예를 들어, 운영하면서 반복적으로 본 막힘 패턴 몇 가지.

KMS 누락. S3 버킷이 SSE-KMS로 암호화되어 있는데 Role에 kms:Decrypt가 없는 경우. s3:GetObject는 통과하지만 객체를 못 읽는다.

STS 누락. AssumeRole 체인 중간에 sts:GetCallerIdentitysts:GetSessionToken이 빠진 경우. SDK 초기화에서 막힌다.

리전 불일치. 정책 Resource ARN의 리전과 실제 호출 리전이 다른 경우. 막힌 이유가 "no identity-based policy"로 나와서 정책 누락처럼 보이는데 사실은 리전 문제다.

이런 패턴은 후행 좁히기로 정책을 만들면 거의 안 생긴다. 실제 호출 기반이라 누락이 없다.

한계와 단서

이 방식이 만능은 아니다. 일단 30일은 넓은 권한으로 운영해야 한다. 보안팀이 강한 조직에서는 이 30일을 못 견딘다. Boundary가 상한을 잡아도 "PowerUser는 일단 위험하다"는 인식이 있다. 이건 기술이 아니라 합의의 문제다.

또한, 또 CloudTrail이 켜져 있어야 한다. 켜져 있더라도 데이터 이벤트(S3 객체 단위, Lambda 호출 단위)는 기본값이 꺼져 있다. 그 영역까지 정확히 잡으려면 데이터 이벤트를 활성화해야 하고, 비용이 늘어난다. 100GB 단위로 청구가 붙는 구조라 무시할 수 없다.

당장 시도해볼 만한 액션 세 가지.

  • 운영 중인 Role 한두 개에 Access Analyzer Policy Generation을 돌려본다. 결과만 받아보고 적용은 나중에 해도 된다. 부담 없다.
  • 모든 IAM Role 생성 시 Permission Boundary를 강제하는 SCP를 켠다. 신규 Role의 권한 상한이 자동으로 잡혀서 사고 가능성이 줄어든다.
  • CloudWatch Logs Insights로 AccessDenied가 몇 건 쌓이는지부터 확인한다. 막힌 호출이 어디서 오는지 보이면 정책 개선 방향이 잡힌다.

이처럼, Resource 수준의 정밀 좁히기는 여전히 수작업이 필요하다. 자동화는 더 지켜봐야 한다.

관련 글