- Published on
GitHub Actions OIDC로 AWS 배포 403 해결 가이드
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버리스든(ECR/ECS/Lambda) 인프라든, GitHub Actions에서 OIDC(OpenID Connect) 로 AWS에 배포하도록 전환하면 장기 액세스 키 없이도 안전하게 배포할 수 있습니다. 하지만 설정이 조금만 어긋나도 배포 단계에서 갑자기 403(AccessDenied) 를 만나기 쉽습니다. 특히 AssumeRoleWithWebIdentity 단계에서 막히거나, AssumeRole은 성공했는데 S3/ECR/CloudFormation 호출에서 403이 터지는 케이스가 많습니다.
이 글은 “GitHub Actions OIDC로 AWS 배포 시 403이 나는 이유”를 딱 3층(연동/신뢰/권한) 으로 나눠서, 로그로 확인하고 바로 고칠 수 있게 정리합니다.
> 참고로, 런타임에서 자격 증명 자체가 안 잡히는 문제는 403이 아니라 NoCredentialProviders로 보이는 경우가 많습니다. 해당 유형은 EKS에서 AWS SDK NoCredentialProviders 해결 가이드도 함께 보면 원인 분리가 빨라집니다.
1) 403의 종류부터 구분하기: 어디서 막히는가?
OIDC 기반 배포에서 403은 크게 두 종류입니다.
1.1 AssumeRoleWithWebIdentity 자체가 403
대개 다음 중 하나입니다.
- IAM Role의 Trust Policy(신뢰 정책)에서 OIDC Provider/조건이 불일치
- GitHub 토큰의
aud,sub클레임 조건이 맞지 않음 - OIDC Provider가 잘못 등록(issuer URL/Thumbprint/ClientIdList)
이때 AWS CLI/SDK 에러는 보통 아래처럼 나옵니다.
An error occurred (AccessDenied) when calling the AssumeRoleWithWebIdentity operation:
Not authorized to perform sts:AssumeRoleWithWebIdentity
1.2 AssumeRole은 성공했는데 배포 API 호출이 403
즉, STS는 통과했지만 Role Permission Policy가 부족한 경우입니다.
- ECR push에서
ecr:PutImage/ecr:InitiateLayerUpload누락 - S3 업로드에서
s3:PutObject는 있는데 버킷 정책이 거부 - CloudFormation에서
cloudformation:CreateChangeSet등 누락 - KMS 암호화(S3 SSE-KMS, ECR KMS 등)로 인해
kms:Encrypt/Decrypt누락
이때는 대개 이런 식입니다.
AccessDeniedException: User: arn:aws:sts::123:assumed-role/GitHubActionsRole/... is not authorized to perform: ecr:PutImage
2) GitHub Actions OIDC 동작 원리(디버깅 포인트)
OIDC 플로우는 간단합니다.
- GitHub Actions Runner가 GitHub OIDC에서 ID Token(JWT) 을 발급받음
- AWS STS가 그 토큰을 검증하고, Role의 Trust Policy 조건을 만족하면
- 임시 자격 증명(AccessKey/SecretKey/SessionToken)을 발급
여기서 403이 가장 많이 나는 지점은 Trust Policy 조건입니다. 특히 sub(subject) 조건이 Repository/Branch/Environment와 정확히 일치해야 합니다.
3) 가장 흔한 원인 1: Trust Policy의 sub 조건 불일치
GitHub OIDC의 sub는 상황에 따라 형태가 달라집니다.
- 브랜치 기반:
repo:ORG/REPO:ref:refs/heads/main - 태그 기반:
repo:ORG/REPO:ref:refs/tags/v1.2.3 - 환경(Environment) 기반:
repo:ORG/REPO:environment:prod
즉, Trust Policy에서 sub를 main으로 고정해두고 태그로 배포하면 403이 납니다.
3.1 권장 Trust Policy 예시(브랜치 + 환경 허용)
아래는 가장 많이 쓰는 형태입니다.
{
"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:ref:refs/heads/main",
"repo:my-org/my-repo:environment:prod"
]
}
}
}
]
}
체크 포인트
aud는 대부분sts.amazonaws.com이어야 합니다.StringLike를 써서 브랜치/태그/환경을 여러 개 허용할 수 있습니다.- 오타(대소문자 포함) 하나만 있어도 STS 단계에서 403이 납니다.
4) 가장 흔한 원인 2: Workflow에 id-token: write 권한 누락
GitHub Actions에서 OIDC 토큰을 발급받으려면 workflow 권한이 필요합니다.
permissions:
id-token: write
contents: read
이게 없으면 configure-aws-credentials가 토큰을 못 받아서 다른 형태의 실패가 나거나, 결과적으로 STS 호출이 실패합니다(로그를 보면 OIDC 토큰 관련 메시지가 보입니다).
5) 가장 흔한 원인 3: OIDC Provider 등록/ClientIdList 문제
AWS IAM에 OIDC Provider가 다음 값으로 등록되어 있어야 합니다.
- Provider URL:
https://token.actions.githubusercontent.com - Audience(Client ID): 보통
sts.amazonaws.com
콘솔에서 OIDC Provider의 ClientIdList에 sts.amazonaws.com이 빠져 있으면, aud 조건과 맞지 않아 403이 발생할 수 있습니다.
6) 토큰 클레임을 직접 확인해서 403을 끝내기
가장 확실한 방법은 실제 발급된 JWT의 클레임을 보고 Trust Policy 조건과 비교하는 것입니다.
6.1 GitHub Actions에서 OIDC 토큰 받아 디코딩하기
아래는 디버깅용 예시입니다(운영에서는 토큰 출력/로그 노출 주의).
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- name: Print OIDC token claims (debug)
shell: bash
run: |
set -euo pipefail
echo "Requesting OIDC token..."
TOKEN_JSON=$(curl -sSf -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=sts.amazonaws.com")
TOKEN=$(echo "$TOKEN_JSON" | jq -r '.value')
echo "$TOKEN" | awk -F. '{print $2}' | tr '_-' '/+' | base64 -d 2>/dev/null | jq
여기서 특히 확인할 값:
subaudrepository,ref,environment(있다면)
이 값을 Trust Policy의 StringLike/StringEquals와 맞추면 STS 403은 거의 끝납니다.
7) AssumeRole은 되는데 403: 권한 정책(permissions policy) 점검
STS가 성공했다면 이제는 “배포에 필요한 API 권한이 충분한가” 문제입니다.
7.1 ECR 푸시 403에서 자주 빠지는 권한
최소 권한 예시(리포지토리 ARN은 환경에 맞게 제한 권장):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:InitiateLayerUpload",
"ecr:UploadLayerPart",
"ecr:CompleteLayerUpload",
"ecr:PutImage"
],
"Resource": "*"
}
]
}
ecr:GetAuthorizationToken은 종종Resource: *가 필요합니다.- 리포지토리 정책(Resource policy)로도 거부될 수 있으니, 조직/계정 간 접근이면 리포지토리 정책도 같이 확인하세요.
7.2 S3 업로드 403: IAM은 허용인데 버킷 정책이 거부
S3는 IAM 정책 + 버킷 정책 + (있다면) VPC Endpoint 정책이 합쳐져 최종 결정됩니다.
- IAM에
s3:PutObject가 있어도 - 버킷 정책에
Deny가 있거나 - 특정 조건(예:
aws:PrincipalArn,s3:x-amz-server-side-encryption)을 강제하면
403이 납니다.
특히 EKS/프라이빗 네트워크에서 VPC Endpoint를 쓰고 있다면, 엔드포인트 정책 때문에 “DNS는 되는데 S3만 이상” 같은 현상이 나기도 합니다. 이 케이스는 EKS Pod DNS는 되는데 S3만 503? 엔드포인트 정책과 함께 보면, 정책 계층을 놓치지 않고 추적할 수 있습니다.
7.3 KMS가 숨어있는 403
S3 SSE-KMS, ECR 암호화, Parameter Store SecureString 등을 쓰면 KMS 권한이 필요합니다.
kms:Encrypt,kms:Decrypt,kms:GenerateDataKey- 그리고 KMS Key policy에서 Role을 신뢰해야 함(IAM만으로는 부족할 수 있음)
증상은 대개:
AccessDeniedException: not authorized to perform kms:Decrypt on key ...
8) GitHub Actions 배포 워크플로 예시(정상 구성)
아래는 OIDC로 Role을 받아 ECR 로그인 후 이미지를 푸시하는 흐름 예시입니다.
name: deploy
on:
push:
branches: ["main"]
permissions:
id-token: write
contents: read
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
aws-region: ap-northeast-2
- name: Login to ECR
uses: aws-actions/amazon-ecr-login@v2
- name: Build and push
env:
ECR_REGISTRY: 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com
ECR_REPOSITORY: my-app
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
이 구성이 403을 피하는 핵심은:
permissions.id-token: write- Trust Policy에서
aud=sub조건이 실제 토큰과 일치 - ECR에 필요한 액션이 Role policy에 포함
9) 빠른 체크리스트: 403을 10분 안에 좁히는 순서
9.1 STS 단계 403이면
- Workflow에
id-token: write가 있는지 - IAM OIDC Provider가
token.actions.githubusercontent.com인지 - Provider ClientIdList에
sts.amazonaws.com가 있는지 - Role Trust Policy의
- Federated ARN이 올바른지
aud가sts.amazonaws.com인지sub가 실제 토큰과 일치하는지(브랜치/태그/환경)
9.2 API 호출 단계 403이면
- 에러 메시지에 찍힌
Action(예:ecr:PutImage)을 Role policy에 추가 - 리소스 정책(S3 버킷 정책, ECR repo policy, KMS key policy)의
Deny확인 - 조건 강제(암호화 헤더,
aws:PrincipalArn,aws:SourceVpce)로 인한 거부 확인
네트워크/STS 엔드포인트 접근 문제는 보통 403이 아니라 타임아웃/5xx로 보이지만, 환경에 따라 혼동되기도 합니다. STS 호출이 비정상적으로 실패한다면 EKS Pod STS AssumeRole 타임아웃 - NAT·PrivateLink·DNS처럼 연결 계층도 함께 점검해 두면 좋습니다.
10) 결론: 403은 “신뢰(Trust) vs 권한(Permission)”으로 쪼개면 끝난다
GitHub Actions OIDC에서 403을 해결하는 가장 좋은 방법은, 문제를 감으로 고치지 않고 아래처럼 분리하는 것입니다.
AssumeRoleWithWebIdentity가 403이면: Trust Policy/Provider/클레임(sub, aud) 문제- AssumeRole 이후가 403이면: Role policy + 리소스 정책(S3/ECR/KMS) + 조건 문제
특히 sub는 배포 트리거(브랜치/태그/환경)에 따라 달라지므로, 한 번은 토큰 클레임을 직접 디코딩해서 “내가 허용한 조건과 실제 값이 같은지”를 확인하는 게 가장 빠른 정답입니다.