- Published on
Terraform apply 후 403? AWS IAM 권한추적 8단계
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
Terraform으로 인프라를 선언하고 terraform apply를 실행했는데 갑자기 403이 터지면, 대부분은 “권한이 없다”라는 한 문장으로 끝나지 않습니다. 같은 403이라도 누가(Principal), 어떤 API를, 어떤 리소스에, 어떤 조건에서 호출했는지에 따라 원인이 완전히 달라집니다.
특히 Terraform은 내부적으로 여러 AWS API를 연쇄 호출하고, assume role, pass role, 서비스 링크드 롤, KMS, 조직(Organizations) SCP, 세션 정책, 퍼미션 바운더리까지 얽히면 체감 난이도가 급상승합니다.
아래 8단계는 “감으로 IAM을 고치는” 대신, 증거 기반으로 403을 재현·추적·수정하는 실전 절차입니다.
참고로 CI에서 OIDC로 AssumeRole 하다가 403이 나는 케이스는 이 글과 겹치는 지점이 많습니다. 원인이 OIDC/신뢰 정책 쪽이라면 GitHub Actions OIDC 403·권한거부 원인 7가지도 같이 보시면 빠릅니다.
1단계: 403의 “정확한 에러 코드”부터 분해하기
Terraform 출력의 403은 AWS SDK 예외가 포장돼 보이는 경우가 많습니다. 먼저 로그에서 아래 중 무엇인지 분류하세요.
AccessDenied/AccessDeniedExceptionUnauthorizedOperationForbiddenExceptionInvalidClientTokenId(이건 인증/자격 증명 문제)SignatureDoesNotMatch(키/리전/시간 오차)
Terraform에서는 다음처럼 디버그 로그를 켜면, 어떤 API에서 깨졌는지 훨씬 선명해집니다.
export TF_LOG=DEBUG
export TF_LOG_PATH=./tf-debug.log
terraform apply
tf-debug.log에서 operation(예: CreateRole, PutBucketPolicy, CreateLoadBalancer)과 status code: 403 직전 문맥을 찾습니다. 여기서 **서비스(예: iam/s3/kms/ec2)**와 API 이름을 확정해야 다음 단계가 빨라집니다.
2단계: “누가 실행했는지” STS로 확정하기
권한 문제는 90%가 “내가 생각한 Principal”과 “실제 Principal”이 다를 때 발생합니다. 로컬이든 CI든, Terraform 실행 환경에서 아래를 먼저 찍어 현재 호출 주체를 확정하세요.
aws sts get-caller-identity
출력에서 확인할 것:
Account: 기대 계정이 맞는지Arn:user/인지role/인지, 또는assumed-role/인지UserId: 세션인지 장기 자격증명인지
여기서 assumed-role/이면, 신뢰 정책(Trust policy), 세션 정책(Session policy), 권한 바운더리(Permissions boundary), SCP까지 영향권에 들어옵니다.
3단계: CloudTrail에서 “거부 이벤트 1건”을 정확히 찾기
가장 빠른 정답지는 CloudTrail입니다. Terraform 로그가 길어도, CloudTrail은 “거부된 API 호출”을 한 줄로 요약해 줍니다.
CloudTrail Event History(콘솔)에서 다음 조건으로 검색하세요.
Event name: 1단계에서 찾은 APIError code:AccessDenied또는UnauthorizedOperationUser name또는Resource name
CLI로도 좁힐 수 있습니다.
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=CreateRole \
--max-results 20
이후 해당 이벤트의 CloudTrailEvent JSON을 열어 다음을 봅니다.
userIdentity.arn(실제 Principal)eventSource(예:iam.amazonaws.com)requestParameters(리소스 ARN, 정책 문서, KMS 키 등)errorMessage(조건 불일치, 명시적 거부 등 힌트)
Terraform이 여러 리소스를 만들다 실패해도, 첫 번째 AccessDenied 이벤트가 도미노의 시작점인 경우가 많습니다.
4단계: IAM Policy Simulator로 “정확히 어떤 문장이 막는지” 확인하기
CloudTrail로 API와 리소스까지 특정했다면, 이제는 IAM Policy Simulator(또는 simulate-principal-policy)로 정책 평가 결과를 재현합니다.
aws iam simulate-principal-policy \
--policy-source-arn arn:aws:iam::123456789012:role/your-role \
--action-names iam:CreateRole \
--resource-arns arn:aws:iam::123456789012:role/test-role
여기서 결과가 explicitDeny인지 implicitDeny인지가 핵심입니다.
implicitDeny: 허용이 없음(대개 정책 누락)explicitDeny: 어디선가 명시적 거부(대개 SCP, permission boundary, 정책의 Deny, 세션 정책)
explicitDeny면 “Allow를 더 붙여도” 계속 실패합니다. 반드시 Deny의 근원을 찾아 제거하거나 조건을 맞춰야 합니다.
5단계: AssumeRole/PassRole/서비스 링크드 롤 3종 세트 점검
Terraform에서 403이 자주 나는 대표 패턴이 아래 3개입니다.
5-1. sts:AssumeRole 신뢰 정책 문제
실행 주체가 역할을 Assume해야 하는데 신뢰 정책이 막는 경우입니다. 신뢰 정책에는 Principal과 Condition이 있고, OIDC라면 sub, aud 조건이 특히 흔한 실패 지점입니다.
5-2. iam:PassRole 누락
ECS, EKS 노드그룹, Lambda, Step Functions 등 “서비스가 역할을 사용”하는 리소스를 만들 때 Terraform 실행 주체에 iam:PassRole이 필요합니다.
예: ECS Task Execution Role을 넘기는 경우
{
"Effect": "Allow",
"Action": "iam:PassRole",
"Resource": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole"
}
PassRole은 흔히 Resource를 너무 넓게 열어두면 보안 감사에 걸리고, 너무 좁히면 Terraform이 생성하는 변형된 이름/경로 때문에 403이 납니다. CloudTrail의 requestParameters.roleArn을 기준으로 정확히 맞추세요.
5-3. Service-Linked Role 생성 권한
ELB, Auto Scaling, ECS 등은 서비스 링크드 롤이 필요할 수 있습니다. 이때 iam:CreateServiceLinkedRole이 없으면 403이 납니다.
6단계: Organizations SCP와 Permission Boundary로 “명시적 Deny” 찾기
권한이 충분해 보이는데도 계속 403이면, 상위 제약을 의심해야 합니다.
- AWS Organizations SCP: 계정/OU 단위로 API를 차단
- Permissions Boundary: 역할/사용자가 가질 수 있는 최대 권한을 제한
- Session Policy: AssumeRole 시 덧씌워진 추가 제한
특징은 다음과 같습니다.
- 정책 문서에 Allow가 있어도 막힘
- Policy Simulator에서
explicitDeny또는allowedByOrganizations같은 힌트가 보임
SCP는 특히 kms:*, s3:PutBucketPolicy, iam:* 같은 민감 API를 차단하는 경우가 많습니다. Terraform이 “정책을 붙이는 단계”에서 자주 터집니다.
7단계: KMS/리소스 기반 정책(S3, KMS, ECR 등)까지 확장해서 보기
IAM 권한만으로 끝나지 않는 403이 많습니다. 대표적으로 리소스 기반 정책이 있는 서비스는 다음을 같이 봐야 합니다.
- S3 Bucket Policy
- KMS Key Policy
- ECR Repository Policy
- SNS/SQS 정책
예를 들어 KMS는 IAM Allow가 있어도 Key Policy가 허용하지 않으면 실패합니다. Terraform에서 EBS 암호화, S3 SSE-KMS, Secrets Manager, RDS 암호화 설정을 건드릴 때 흔합니다.
체크 포인트:
- Key Policy에 실행 주체(또는 해당 역할)가 들어있는지
kms:ViaService,kms:CallerAccount조건으로 제한돼 있지 않은지- Terraform이 사용하는 리전과 키 리전이 일치하는지
8단계: Terraform 관점에서 재발 방지(최소권한·모듈화·검증 자동화)
원인을 찾았다면 “이번 한 번만” 땜질로 끝내지 말고, 다음을 적용하면 같은 403이 반복되는 빈도를 크게 줄일 수 있습니다.
8-1. 실행 역할을 목적별로 분리
하나의 거대한 Terraform 실행 역할에 모든 권한을 몰아주면, 보안팀과 충돌하거나 SCP/Boundary 설계가 꼬입니다.
- 네트워크 전용(예: VPC, TGW)
- 플랫폼 전용(예: EKS, ECS)
- 데이터 전용(예: RDS, OpenSearch)
처럼 스택을 나눠 역할도 분리하면, 403이 나도 범위가 좁아집니다.
8-2. terraform plan 단계에서 권한 사전 점검 스크립트 추가
실제 apply 전에 주요 API를 시뮬레이션해 “권한 부족”을 빨리 실패시키는 방식입니다.
set -euo pipefail
ROLE_ARN="arn:aws:iam::123456789012:role/terraform-exec"
aws iam simulate-principal-policy \
--policy-source-arn "$ROLE_ARN" \
--action-names \
ec2:CreateVpc \
iam:PassRole \
eks:CreateCluster \
kms:Encrypt \
--resource-arns "*" \
--query 'EvaluationResults[?EvalDecision!=`allowed`]' \
--output json
출력에 결과가 남으면 파이프라인을 중단하고, 어떤 액션이 allowed가 아닌지 즉시 확인합니다.
8-3. 에러 추적 습관을 “런북”으로 고정
장애 대응 글을 런북화해두면 팀 전체의 평균 복구 시간이 줄어듭니다. 예를 들어 쿠버네티스 장애도 원인 추적 절차가 중요하듯, IAM 403도 같은 방식으로 체크리스트가 효과적입니다. 장애 추적 흐름을 잡는 데는 K8s CrashLoopBackOff에서 OOMKilled 원인 추적 같은 글의 접근법도 참고할 만합니다.
자주 나오는 403 시나리오별 빠른 힌트
- S3 버킷 정책 붙일 때 403:
s3:PutBucketPolicy+ SCP Deny + 버킷 정책의 Principal/Condition 확인 - EKS 생성 중 403:
eks:CreateCluster외에iam:PassRole,ec2:Describe*,kms:*(암호화 사용 시) 확인 - ALB 생성 중 403:
elasticloadbalancing:*만이 아니라iam:CreateServiceLinkedRole필요 여부 확인 - “내 로컬은 되는데 CI는 안 됨”: CI의 AssumeRole 세션 정책, OIDC 조건, 다른 계정/리전 사용 여부 확인
결론
Terraform apply 후 403은 단순히 권한을 더 주는 문제가 아니라, **정책 평가 체인 전체(IAM 정책, 리소스 정책, SCP, Boundary, 세션 정책, 신뢰 정책)**를 증거로 좁혀가는 문제입니다.
정리하면 다음 순서가 가장 빠릅니다.
- Terraform 로그로 실패 API 확정
aws sts get-caller-identity로 Principal 확정- CloudTrail로 거부 이벤트 확정
- Policy Simulator로
implicitDenyvsexplicitDeny판별 - AssumeRole/PassRole/서비스 링크드 롤 점검
- SCP/Boundary/세션 정책 점검
- KMS/S3 등 리소스 기반 정책 점검
- 사전 시뮬레이션과 역할 분리로 재발 방지
이 8단계를 루틴으로 만들면, 403은 더 이상 “막막한 에러”가 아니라 “근거를 따라가면 반드시 풀리는 퍼즐”이 됩니다.