- Published on
GitHub Actions OIDC AWS 배포 AccessDenied 해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서론
GitHub Actions에서 AWS Access Key 없이 배포하려고 OIDC(OpenID Connect) 기반 AssumeRoleWithWebIdentity를 붙이면, 설정이 조금만 어긋나도 바로 AccessDenied를 만나게 됩니다. 문제는 AccessDenied가 원인이 매우 다양하다는 점입니다.
- OIDC Provider(issuer/클라이언트 ID) 불일치
- IAM Role의 신뢰 정책(Trust policy) 조건이 GitHub 토큰 클레임과 불일치
- Role은 잘 Assume 됐지만, 실제 배포 작업에 필요한 권한 정책(permissions) 부족
aud,sub,ref,environment등 조건을 너무 타이트하게 걸어 특정 브랜치/태그/환경만 실패
이 글은 “OIDC로 AssumeRole은 하려는데 AccessDenied가 난다”를 기준으로, 어디에서 거절되는지를 먼저 판별하고(핵심), 그 다음에 자주 틀리는 신뢰 정책/권한 정책을 실제 예시로 정리합니다. OIDC 기본 구성은 아래 글을 먼저 보고 오면 더 빠릅니다.
- GitHub Actions OIDC로 AWS 키 없이 배포하기
InvalidIdentityToken계열이면 이 글도 함께: GitHub Actions OIDC AWS 배포 InvalidIdentityToken 해결
1) AccessDenied를 “어느 단계에서” 맞는지부터 분리
OIDC 배포에서 AccessDenied는 크게 두 갈래입니다.
- AssumeRoleWithWebIdentity 자체가 거절됨 (신뢰 정책/조건/OIDC Provider 문제)
- AssumeRole은 성공했는데, AWS API 호출이 거절됨 (권한 정책 문제)
로그에서 다음 문구가 보이면 1번 가능성이 큽니다.
An error occurred (AccessDenied) when calling the AssumeRoleWithWebIdentity operation:
Not authorized to perform sts:AssumeRoleWithWebIdentity
반대로, 아래처럼 특정 서비스 액션이 거절되면 2번입니다.
An error occurred (AccessDenied) when calling the CreateInvalidation operation:
User: arn:aws:sts::123456789012:assumed-role/GHADeployRole/... is not authorized to perform: cloudfront:CreateInvalidation
이 분기만 제대로 하면 문제 해결 속도가 3배는 빨라집니다.
2) GitHub Actions 워크플로우 기본 체크 (권한/토큰)
OIDC 토큰을 발급받으려면 워크플로우에 id-token: write 권한이 필요합니다. 이게 없으면 보통 InvalidIdentityToken이나 토큰 발급 실패로 가지만, 상황에 따라 AccessDenied처럼 보일 때도 있어 먼저 확인합니다.
name: deploy
on:
push:
branches: ["main"]
permissions:
contents: read
id-token: write
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GHADeployRole
aws-region: ap-northeast-2
- run: aws sts get-caller-identity
aws sts get-caller-identity가 성공하면 AssumeRole은 된 것이고, 이후 실패는 권한 정책 쪽으로 보면 됩니다.
3) (1번 케이스) AssumeRoleWithWebIdentity가 AccessDenied인 경우
3-1) IAM Role의 Trust policy에서 가장 많이 틀리는 지점
OIDC는 “이 Role을 누가 Assume할 수 있는가”를 Trust policy로 제어합니다. GitHub Actions의 issuer는 token.actions.githubusercontent.com이며, 조건은 보통 aud와 sub를 씁니다.
아래는 가장 흔한 형태의 예시입니다.
{
"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:*"
}
}
}
]
}
여기서 AccessDenied로 이어지는 대표 실수:
Principal.FederatedARN이 다른 계정이거나 오타aud를sts.amazonaws.com이 아닌 값으로 제한sub를 너무 구체적으로 제한해서 특정 브랜치/태그만 매칭 실패
sub는 보통 다음 패턴입니다.
- 브랜치 push:
repo:{org}/{repo}:ref:refs/heads/{branch} - 태그:
repo:{org}/{repo}:ref:refs/tags/{tag} - environment 사용 시:
repo:{org}/{repo}:environment:{envName}
따라서 “main 브랜치만 허용”하고 싶으면 이렇게 씁니다.
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:ref:refs/heads/main"
}
태그 배포도 허용하려면 두 패턴을 OR로 풀어야 합니다(Statement를 두 개로 나누거나, 와일드카드 전략을 씁니다).
3-2) GitHub Environment를 쓰는데 sub 조건이 ref 기반이면 실패
예를 들어 워크플로우에 environment: production을 지정하면, GitHub가 발급하는 토큰의 sub가 environment: 형태로 나올 수 있습니다. 그런데 Trust policy가 ref:만 허용하면 Assume 단계에서 AccessDenied가 납니다.
해결은 Trust policy에 environment 패턴을 추가합니다.
{
"StringLike": {
"token.actions.githubusercontent.com:sub": [
"repo:my-org/my-repo:ref:refs/heads/main",
"repo:my-org/my-repo:environment:production"
]
}
}
3-3) OIDC Provider 설정(클라이언트 ID/aud)이 꼬인 경우
IAM의 OIDC Provider에서 Client ID(audience)가 sts.amazonaws.com로 등록되어 있어야 일반적인 구성과 맞습니다. Provider에 다른 값만 등록되어 있거나, Trust policy에서 aud를 강제했는데 실제 토큰 aud와 다르면 Assume이 거절됩니다.
실무에서는 aud 조건은 유지하되(보안상 의미 있음), Provider와 워크플로우 액션이 기본값(sts.amazonaws.com)을 쓰도록 맞추는 편이 안전합니다.
4) (2번 케이스) AssumeRole은 성공했는데 AWS API가 AccessDenied인 경우
get-caller-identity는 성공하는데, S3 업로드/CloudFront invalidation/ECR push/EKS 배포에서 AccessDenied가 뜬다면 Role에 붙은 권한 정책이 부족합니다.
4-1) 가장 흔한 S3 배포 AccessDenied: 버킷 정책 vs IAM 정책
정적 사이트 배포에서 자주 보는 실패:
An error occurred (AccessDenied) when calling the PutObject operation: Access Denied
이 경우 체크 순서:
- Role에
s3:PutObject권한이 있는가 - 대상 버킷 ARN/오브젝트 ARN이 정책 리소스와 일치하는가
- 버킷 정책에서 해당 Role을 명시적으로 거부(Deny)하거나, 다른 조건을 강제하는가
- KMS로 암호화된 버킷이면
kms:Encrypt/Decrypt가 필요한가
최소 예시(IAM permissions policy):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:ListBucket"],
"Resource": "arn:aws:s3:::my-deploy-bucket"
},
{
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:DeleteObject", "s3:GetObject"],
"Resource": "arn:aws:s3:::my-deploy-bucket/*"
}
]
}
리소스를 arn:aws:s3:::my-deploy-bucket만 넣고 /*를 빼면 PutObject는 계속 AccessDenied가 납니다.
4-2) CloudFront invalidation AccessDenied
배포 후 캐시 무효화에서 흔한 오류:
is not authorized to perform: cloudfront:CreateInvalidation on resource: arn:aws:cloudfront::123456789012:distribution/EDFDVBD6EXAMPLE
권한 추가:
{
"Effect": "Allow",
"Action": ["cloudfront:CreateInvalidation"],
"Resource": "arn:aws:cloudfront::123456789012:distribution/EDFDVBD6EXAMPLE"
}
CloudFront는 리소스 ARN이 계정/배포 ID까지 정확히 들어가야 합니다.
4-3) ECR push AccessDenied (토큰은 받았는데 push가 안 됨)
ECR은 ecr:GetAuthorizationToken(리소스 *)과 리포지토리 단위 권한이 함께 필요합니다.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["ecr:GetAuthorizationToken"],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"ecr:BatchCheckLayerAvailability",
"ecr:InitiateLayerUpload",
"ecr:UploadLayerPart",
"ecr:CompleteLayerUpload",
"ecr:PutImage"
],
"Resource": "arn:aws:ecr:ap-northeast-2:123456789012:repository/my-app"
}
]
}
여기서 GetAuthorizationToken을 리포지토리 ARN으로 제한하면 실패합니다(반드시 *).
5) AccessDenied를 “증거 기반”으로 좁히는 방법 (CloudTrail)
가장 빠른 디버깅은 CloudTrail에서 이벤트를 직접 보는 것입니다.
- Assume 단계 실패:
AssumeRoleWithWebIdentity이벤트가errorCode=AccessDenied로 남음 - API 호출 실패: 예)
PutObject,CreateInvalidation,eks:DescribeCluster등이AccessDenied로 남음
CloudTrail 이벤트에서 특히 유용한 필드:
eventSource(sts.amazonaws.com, s3.amazonaws.com 등)eventNameuserIdentity.arn(assumed-role인지, 어떤 세션인지)errorMessage(조건 불일치/명시적 Deny 등 힌트)
조직에서 EKS 배포까지 엮여 있다면, 권한 문제로 보이지만 실제로는 클러스터/서비스 구성이 꼬여 장애처럼 보이는 경우도 많습니다. EKS 운영 이슈 트러블슈팅은 아래 글도 참고할 만합니다.
6) 실전 체크리스트 (가장 많이 막히는 10분 코스)
- 워크플로우에
permissions: id-token: write있는가 aws sts get-caller-identity가 성공하는가- 실패면 Trust policy/OIDC Provider/조건 문제(3장)
- 성공이면 권한 정책 문제(4장)
- Trust policy의
sub가 실제 배포 트리거(브랜치/태그/environment)와 맞는가 - Trust policy의
aud가sts.amazonaws.com로 맞는가 - IAM permissions policy에서 리소스 ARN이 정확한가(S3는
/*, CloudFront는 distribution ARN) - 버킷 정책/KMS 키 정책/Organizations SCP에서 명시적 Deny가 없는가
- CloudTrail에서
eventName과errorMessage로 최종 거절 지점을 확인했는가
결론
GitHub Actions OIDC로 AWS 배포를 구성했을 때의 AccessDenied는 “OIDC가 안 된다”가 아니라, 대개 신뢰 정책 조건 불일치(Assume 단계) 또는 권한 정책 누락(API 단계) 중 하나입니다. 먼저 get-caller-identity로 경계를 나눈 뒤, Trust policy의 sub/aud와 실제 GitHub 토큰 클레임(브랜치/태그/environment)을 맞추고, 마지막으로 배포 대상 서비스(S3/CloudFront/ECR/EKS)에 필요한 최소 권한을 리소스 단위로 정확히 부여하면 대부분 깔끔하게 해결됩니다.