Published on

AWS IAM AccessDenied 403, 정책 시뮬레이터로 추적

Authors

서버리스, EKS, CI/CD 등 어떤 환경이든 AWS에서 AccessDenied 혹은 403은 자주 마주칩니다. 문제는 에러 메시지 자체가 원인을 충분히 말해주지 않는다는 점입니다. 같은 403이라도 명시적 거부(Explicit Deny), 조건(Condition) 불일치, 리소스 ARN 불일치, 권한 경계(Permissions boundary), SCP(Service Control Policy), 세션 정책(Session policy), 리소스 기반 정책(Resource-based policy) 등 원인이 다양합니다.

이 글에서는 IAM Policy Simulator(정책 시뮬레이터)를 중심으로, CloudTrail과 함께 “어떤 정책의 어떤 문장 때문에 거부되었는지”를 빠르게 특정하는 실전 절차를 정리합니다.

403의 종류부터 구분하기

AWS에서 흔히 보는 형태는 아래 두 가지입니다.

  • An error occurred (AccessDenied) when calling the ... operation: User is not authorized to perform: ... on resource: ...
  • 403 Forbidden (특히 S3, CloudFront, API Gateway, STS 등에서 HTTP 코드로 보일 때)

중요한 건 “누가(Principal)”, “무엇을(Action)”, “어떤 리소스(Resource)에서”, “어떤 컨텍스트(Context: region, VPC, tags, MFA, source IP 등)로” 요청했는지를 정확히 잡아내는 것입니다. 이 4가지가 정리되면 정책 시뮬레이터로 재현이 가능합니다.

1단계: CloudTrail로 실제 요청을 먼저 고정하기

정책 시뮬레이터는 입력값이 정확할수록 강력합니다. 운영에서 터진 403은 먼저 CloudTrail 이벤트로 “실제 호출”을 확정하세요.

CloudTrail에서 확인할 핵심 필드

  • eventSource, eventName : 어떤 서비스/액션인지
  • userIdentity : 호출 주체(역할, 사용자, 세션)
  • requestParameters : 어떤 리소스/파라미터로 요청했는지
  • resources : CloudTrail이 인식한 리소스
  • errorCode, errorMessage
  • additionalEventData : 서비스별 추가 정보

CLI로 AccessDenied 이벤트 조회 예시

아래는 최근 1시간 내 AccessDenied를 CloudTrail에서 찾는 예시입니다.

aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRole \
  --max-results 50 \
  --query 'Events[?contains(CloudTrailEvent, `AccessDenied`)].CloudTrailEvent' \
  --output text

AssumeRole이 아니라 S3 작업이라면 GetObject, PutObject, ListBucket 등으로 바꿔 조회합니다.

포인트

  • “어떤 액션이 거부되었는지”를 추측하지 말고 CloudTrail에서 확정하세요.
  • 특히 STS(AssumeRoleWithWebIdentity, AssumeRole)나 KMS(Decrypt)는 2차 호출에서 터지는 경우가 많아, 연쇄 이벤트를 같이 봐야 합니다.

2단계: IAM Policy Simulator로 ‘정책 레벨’ 원인 좁히기

IAM Policy Simulator는 특정 Principal에 대해 특정 Action이 특정 Resource에서 허용되는지 시뮬레이션합니다. 여기서 중요한 점은 “허용”이 아니라 “왜 거부되었는지”를 보는 것입니다.

콘솔에서 빠르게 하는 방법

  1. IAM 콘솔에서 Policy Simulator로 이동
  2. 사용자/역할(Role)을 선택
  3. Actions에서 CloudTrail의 eventName에 해당하는 액션 선택
  4. Resource ARN을 CloudTrail 기준으로 입력
  5. 결과가 Denied면 상세(Statements)를 열어 어떤 정책 문장에 의해 거부되는지 확인

CLI로 재현하기: simulate-principal-policy

콘솔보다 자동화·재현에 유리한 CLI 예시입니다.

aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::123456789012:role/MyAppRole \
  --action-names s3:GetObject \
  --resource-arns arn:aws:s3:::my-bucket/path/to/object.txt

출력에서 EvalDecisionallowed인지 explicitDeny인지 implicitDeny인지가 핵심입니다.

  • implicitDeny : “허용하는 문장이 없음” 또는 “조건 불일치로 허용이 성립하지 않음”
  • explicitDeny : 어디선가 Deny가 걸려서 무조건 거부

3단계: 가장 흔한 6가지 원인 패턴과 시뮬레이터 해석법

1) Resource ARN이 틀려서 implicitDeny

S3는 특히 자주 헷갈립니다.

  • s3:ListBucket은 버킷 ARN이 arn:aws:s3:::my-bucket
  • s3:GetObject는 오브젝트 ARN이 arn:aws:s3:::my-bucket/*

정책이 아래처럼 되어 있는데 GetObjectarn:aws:s3:::my-bucket에 시뮬레이션하면 당연히 거부됩니다.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject"],
      "Resource": "arn:aws:s3:::my-bucket/*"
    }
  ]
}

시뮬레이터에서 Resource를 정확히 바꾸면 allowed로 바뀝니다.

2) Condition 불일치로 implicitDeny

예를 들어 특정 VPC 엔드포인트에서만 S3 접근을 허용하는 정책은, 그 조건이 만족되지 않으면 허용이 성립하지 않습니다.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::my-bucket",
        "arn:aws:s3:::my-bucket/*"
      ],
      "Condition": {
        "StringEquals": {
          "aws:sourceVpce": "vpce-0abc1234def567890"
        }
      }
    }
  ]
}

이 경우 시뮬레이터에서 Context entriesaws:sourceVpce를 넣지 않으면 Denied가 나올 수 있습니다. 즉 “정책은 맞는데 컨텍스트가 달라서” 거부되는 전형적인 케이스입니다.

3) 명시적 Deny가 어디엔가 숨어있음: explicitDeny

explicitDeny가 뜨면 게임이 거의 끝납니다. 어느 정책이든 Deny가 하나라도 매칭되면 무조건 거부입니다.

  • IAM 사용자/역할에 직접 붙은 정책
  • Permission boundary
  • 조직의 SCP
  • 리소스 정책(S3 bucket policy, KMS key policy 등)

시뮬레이터 결과 상세에서 MatchedStatements 혹은 OrganizationsDecisionDetail 같은 단서를 확인하세요.

4) Permissions boundary 때문에 허용이 “상한선”에 걸림

권한 경계는 “이 역할이 가질 수 있는 최대 권한”입니다. 역할 정책에 Allow가 있어도 boundary에서 막히면 최종 Deny가 됩니다.

점검 포인트:

  • 역할(Role)에 boundary가 설정되어 있는지
  • boundary 정책에 해당 Action/Resource가 포함되는지

이 케이스는 팀 내 공통 역할 템플릿에서 boundary를 강제하는 환경에서 특히 자주 발생합니다.

5) 조직 SCP로 막힘

AWS Organizations를 쓰면 SCP가 계정 전체의 상한선을 결정합니다. 개발 계정에서는 되는데 운영 계정에서만 403이 나는 대표 원인입니다.

SCP는 IAM Policy Simulator만으로는 완벽히 재현이 어려울 수 있습니다. 이때는:

  • CloudTrail의 errorMessage에 SCP 관련 문구가 있는지
  • AWS Organizations 콘솔에서 해당 OU/계정에 연결된 SCP 확인
  • 필요 시 aws organizations describe-effective-policy로 확인

6) STS AssumeRole 계열: 신뢰 정책(Trust policy) 문제

AssumeRole/AssumeRoleWithWebIdentityAccessDenied라면, 대부분 “권한 정책”이 아니라 “신뢰 정책” 또는 “OIDC 조건” 문제입니다.

예: OIDC에서 sub/aud 조건이 맞지 않음.

신뢰 정책 예시(일부):

{
  "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:*"
        }
      }
    }
  ]
}

이 영역은 케이스가 많아 별도 체크리스트가 유용합니다. 위에서 언급한 GitHub Actions OIDC 403·권한거부 원인 7가지를 같이 보면서 sub/aud, provider ARN, 워크플로 권한, 세션 지속시간 등을 함께 점검하세요.

4단계: S3 403을 정책 시뮬레이터로 끝내는 실전 흐름

S3는 403이 “인증 실패”처럼 보여도 실제로는 권한/정책/암호화(KMS) 등 복합 원인이 많습니다. 아래 순서가 가장 빠릅니다.

체크리스트

  1. CloudTrail에서 s3:GetObject/PutObject 이벤트 확인
  2. 동일 요청에 kms:Decrypt/kms:Encrypt가 뒤따르는지 확인(서버사이드 암호화가 KMS면 거의 항상 필요)
  3. IAM 시뮬레이터로 s3:GetObject를 정확한 오브젝트 ARN으로 시뮬레이션
  4. KMS를 쓴다면 kms:Decrypt도 같은 Principal로 시뮬레이션
  5. 버킷 정책에 Deny가 있는지 확인(특히 aws:SecureTransport, aws:PrincipalArn, aws:sourceVpce, aws:SourceIp)

KMS까지 포함한 시뮬레이션 예시

aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::123456789012:role/MyAppRole \
  --action-names s3:GetObject kms:Decrypt \
  --resource-arns \
    arn:aws:s3:::my-bucket/path/to/object.txt \
    arn:aws:kms:ap-northeast-2:123456789012:key/11112222-3333-4444-5555-666677778888

여기서 s3:GetObjectallowed인데 kms:DecryptDenied면, 체감상 “S3 403”처럼 보여도 실제 원인은 KMS 권한입니다.

5단계: “정책은 Allow인데도 403”일 때 보는 것들

정책 문장에 Allow가 있는데도 거부되는 경우는 대개 아래 중 하나입니다.

  • Allow는 맞지만 Resource가 다름(ARN 패턴, 와일드카드 범위)
  • Allow는 맞지만 Condition이 달라서 매칭이 안 됨
  • 다른 곳의 Deny(SCP, boundary, bucket policy, key policy)가 최종적으로 이김
  • 호출 주체가 생각한 Principal이 아님(예: EC2 인스턴스 프로파일 역할이 아니라 다른 역할로 실행)

이때는 CloudTrail의 userIdentity.arn을 기준으로 “정말 그 역할로 호출했는지”부터 재검증하세요.

6단계: 재발 방지 팁(운영 관점)

1) 정책 변경 시 최소 단위로 검증

  • 새 정책을 붙이기 전, 시뮬레이터로 핵심 액션들(List, Get, Put, Decrypt, AssumeRole)만 먼저 돌려보면 사고가 크게 줄어듭니다.

2) 태그 기반 접근(ABAC)은 테스트 컨텍스트를 표준화

aws:PrincipalTag, aws:ResourceTag를 쓰면 강력하지만, 시뮬레이터에서도 태그 컨텍스트를 넣어야 재현됩니다. 팀 내에서 “필수 태그 키”와 “세션 태그 전달 방식”을 문서화하세요.

3) CI/CD는 OIDC 조건을 보수적으로 시작

처음부터 sub를 너무 넓게 열기보다, 좁게 열고 필요 시 확장하는 편이 안전합니다. OIDC는 403이 나면 원인 범위가 넓어지기 때문에, GitHub Actions OIDC로 AWS 키 없이 배포하기처럼 단계적으로 구성하는 접근이 좋습니다.

마무리: 403을 “감”이 아니라 “증거”로 끝내기

AccessDenied 403을 빨리 끝내는 핵심은 다음 한 줄로 요약됩니다.

  • CloudTrail로 “실제 호출(Principal, Action, Resource, Context)”을 확정하고
  • IAM Policy Simulator로 “어떤 문장이 왜 매칭/비매칭되는지”를 확인한다

이 흐름이 익숙해지면, 단순히 정책을 늘리는 방식(과도한 Action: *)이 아니라 정확한 원인 제거로 해결할 수 있고, 보안 품질도 함께 올라갑니다.

필요하면 다음 정보를 주시면, 케이스에 맞춰 시뮬레이터 입력값(액션/리소스/컨텍스트)까지 구체적으로 잡아드릴 수 있습니다.

  • CloudTrail 이벤트의 eventName, userIdentity.arn, resources 일부
  • 에러가 난 서비스(S3, STS, KMS, ECR, DynamoDB 등)
  • 사용 중인 제약(SCP 사용 여부, permissions boundary 여부, VPC endpoint 강제 여부)