- Published on
GitHub Actions OIDC로 AWS 배포 - AssumeRole 403 해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버리스든 ECS/EKS든, GitHub Actions에서 AWS로 배포할 때 장기 Access Key를 없애기 위해 OIDC(웹 아이덴티티) 로 AssumeRoleWithWebIdentity를 붙이는 구성이 사실상 표준이 됐습니다. 문제는 설정이 조금만 어긋나도 배포 단계에서 바로 403 AccessDenied가 터진다는 점입니다. 특히 에러 메시지가 짧게 나오면(예: Not authorized to perform sts:AssumeRoleWithWebIdentity) 어디가 틀렸는지 감이 안 옵니다.
이 글에서는 GitHub Actions OIDC로 AWS 배포 시 발생하는 AssumeRole 403을 원인별로 분해하고, 가장 재현률 높은 실수(OIDC Provider/신뢰 정책 조건/권한 정책/리포지토리·브랜치 매칭/대상 Role ARN 오타)를 체크리스트 형태로 해결합니다.
> 참고로, “403”이라는 증상은 AWS뿐 아니라 경계면(인증/인가/프록시/WAF)에서 자주 반복됩니다. EKS에서 403을 로그로 푸는 접근은 EKS ALB Ingress WAF 403 차단 로그로 푸는 법도 같이 보면 디버깅 사고방식이 도움이 됩니다.
1) 전제: GitHub Actions OIDC 흐름 요약
GitHub Actions는 실행 중에 OIDC 토큰(JWT)을 발급받을 수 있고, AWS STS는 이 토큰을 검증해 임시 자격 증명(AccessKeyId/SecretAccessKey/SessionToken) 을 발급합니다.
흐름은 다음과 같습니다.
- Workflow가
id-token: write권한으로 OIDC 토큰을 요청 - AWS IAM에 등록된 OIDC Provider(
token.actions.githubusercontent.com)가 토큰을 검증 - Role의 Trust policy가 토큰 클레임(
sub,aud등)을 조건으로 허용 - STS가
AssumeRoleWithWebIdentity를 수행하고 임시 크레덴셜 발급 - 이후 AWS API 호출은 해당 Role의 Permissions policy로 결정
403이 나는 지점은 크게 2가지입니다.
- STS AssumeRole 단계에서 403: Trust policy/OIDC Provider/클레임 조건/Role ARN 문제
- Assume 성공 후 AWS API 호출에서 403: Role 권한 정책(permissions) 부족
로그에서 에러가 sts:AssumeRoleWithWebIdentity인지, s3:PutObject 같은 서비스 API인지 먼저 구분해야 합니다.
2) 가장 흔한 403 유형 3가지
(A) Not authorized to perform sts:AssumeRoleWithWebIdentity
거의 항상 Trust policy 또는 OIDC Provider 설정 문제입니다.
- OIDC Provider가 아예 없거나 Thumbprint/URL/Audience가 불일치
- Trust policy에서
Principal.FederatedARN이 잘못됨 Condition의sub가 현재 실행된 ref(브랜치/태그/환경)와 불일치aud조건을 걸어놓고 실제 토큰 aud가 다름(대개sts.amazonaws.com)
(B) InvalidIdentityToken / No OpenIDConnect provider found
OIDC Provider 리소스가 없거나, Provider ARN을 잘못 참조했을 때 자주 발생합니다.
- AWS 계정에
token.actions.githubusercontent.comProvider 미생성 - 다른 리전에 있는 리소스가 아니라 계정 전역(IAM) 이라서, 단순히 계정이 다름
(C) AssumeRole은 성공했는데 배포가 403
이 경우는 STS는 통과했고, Role 권한 정책이 부족한 것입니다.
예)
- S3 업로드:
s3:PutObject/s3:ListBucket누락 - ECR 푸시:
ecr:InitiateLayerUpload등 누락 - CloudFormation/SSM/Secrets Manager 호출 권한 누락
S3 403의 원인이 다양하듯(버킷 정책, KMS, VPC 엔드포인트 등) AssumeRole 이후의 403도 “권한 정책”만이 원인이 아닐 수 있습니다. S3라면 EKS Pod에서 S3 403 AccessDenied 원인 10가지처럼 리소스 정책/암호화/조건절까지 같이 확인해야 합니다.
3) 필수: GitHub Actions Workflow 권한 설정
OIDC 토큰을 받으려면 Workflow에 권한을 명시해야 합니다.
name: deploy
on:
push:
branches: ["main"]
permissions:
id-token: write
contents: read
jobs:
deploy:
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/github-actions-deploy
aws-region: ap-northeast-2
- name: Who am I?
run: aws sts get-caller-identity
체크 포인트:
permissions.id-token: write가 없으면 OIDC 토큰 발급 자체가 안 됩니다.role-to-assumeARN 오타가 생각보다 흔합니다(계정 번호/Role 이름/경로).
4) AWS IAM OIDC Provider 생성(정석 구성)
AWS 콘솔에서 IAM → Identity providers → Add provider
- Provider type: OpenID Connect
- Provider URL:
https://token.actions.githubusercontent.com - Audience:
sts.amazonaws.com
CLI로도 만들 수 있습니다(이미 있다면 생략).
aws iam create-open-id-connect-provider \
--url https://token.actions.githubusercontent.com \
--client-id-list sts.amazonaws.com \
--thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1
주의:
- Thumbprint는 AWS 문서/콘솔에서 자동 처리되는 편이지만, 수동 입력 시 값이 바뀌는 경우가 있어 콘솔 생성이 안전합니다.
- Provider는 계정 단위입니다. 다른 AWS 계정에 만들고 현재 계정 Role에서 참조하면 403/에러가 납니다.
5) 핵심: Trust policy(신뢰 정책) 올바르게 쓰기
AssumeRole 403의 80%는 여기서 납니다. 아래는 “main 브랜치 push”만 허용하는 최소 예시입니다.
{
"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",
"token.actions.githubusercontent.com:sub": "repo:YOUR_ORG/YOUR_REPO:ref:refs/heads/main"
}
}
}
]
}
5.1 sub 조건에서 가장 많이 틀리는 포인트
sub는 실행 컨텍스트에 따라 달라집니다.
- 브랜치 push:
repo:org/repo:ref:refs/heads/main - 태그 push:
repo:org/repo:ref:refs/tags/v1.2.3 - PR:
repo:org/repo:pull_request - Environments 사용 시:
repo:org/repo:environment:prod형태가 섞일 수 있음
따라서 운영 배포를 environment: production으로 강제하고 싶다면 sub를 environment 기준으로 제한하는 편이 더 안전합니다.
예: GitHub Environment prod에서만 허용
{
"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",
"token.actions.githubusercontent.com:sub": "repo:YOUR_ORG/YOUR_REPO:environment:prod"
}
}
}
5.2 브랜치/태그를 여러 개 허용해야 한다면
StringLike로 패턴 매칭을 쓰는 방법이 실무에서 많이 쓰입니다.
{
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": [
"repo:YOUR_ORG/YOUR_REPO:ref:refs/heads/main",
"repo:YOUR_ORG/YOUR_REPO:ref:refs/tags/v*"
]
}
}
}
주의: StringLike를 너무 넓게 풀면(예: repo:org/repo:*) 사실상 누구나 해당 리포에서 Role을 Assume할 수 있게 되어 위험합니다.
6) 디버깅: 실제 OIDC 클레임 확인하기
403이 나면 “내가 생각한 sub/aud가 맞나?”부터 확인해야 합니다. GitHub Actions에서는 OIDC 토큰을 직접 받아서(주의: 로그 노출 위험) 헤더/페이로드를 디코딩해 볼 수 있습니다.
아래는 디버깅 전용 예시입니다. 토큰을 그대로 출력하지 말고 payload만 최소로 확인하세요.
- name: Debug OIDC claims (DO NOT use in production logs)
shell: bash
run: |
set -euo pipefail
echo "Requesting OIDC token..."
TOKEN_JSON=$(curl -sS -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=sts.amazonaws.com")
TOKEN=$(echo "$TOKEN_JSON" | jq -r '.value')
# Decode JWT payload
PAYLOAD=$(echo "$TOKEN" | cut -d '.' -f2 | tr '_-' '/+' | base64 -d 2>/dev/null || true)
echo "$PAYLOAD" | jq '{sub, aud, iss, repository, ref, actor, workflow, job_workflow_ref}'
여기서 출력된 sub 값이 Trust policy의 token.actions.githubusercontent.com:sub와 1글자라도 다르면 STS 단계에서 403이 납니다.
7) AssumeRole 성공/실패를 CloudTrail로 확정하기
로컬 추측으로 시간을 쓰기보다, CloudTrail 이벤트로 STS 호출 결과를 먼저 확정하는 게 빠릅니다.
- 이벤트 소스:
sts.amazonaws.com - 이벤트 이름:
AssumeRoleWithWebIdentity - 에러 코드:
AccessDenied인지, 성공인지 - requestParameters/responseElements에 어떤 Role ARN으로 요청했는지
CloudTrail에서 성공이 확인되면, 이후 403은 서비스 권한/리소스 정책 문제로 범위를 좁힐 수 있습니다.
8) “AssumeRole은 되는데 배포가 403”일 때 권한 정책 예시
예를 들어 S3에 빌드 아티팩트를 업로드하는 배포라면 최소 권한은 보통 아래 정도가 필요합니다.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "UploadArtifacts",
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:AbortMultipartUpload",
"s3:ListBucket",
"s3:GetBucketLocation"
],
"Resource": [
"arn:aws:s3:::my-deploy-bucket",
"arn:aws:s3:::my-deploy-bucket/*"
]
}
]
}
여기서도 403이 계속 난다면 다음을 추가로 봐야 합니다.
- 버킷 정책에서 해당 Role을 명시적으로 거부(Deny)하고 있지 않은지
- KMS로 암호화된 버킷이라면
kms:Encrypt권한 및 KMS 키 정책이 맞는지 - 조직 SCP(Service Control Policy)로 STS나 특정 서비스 액션이 막히지 않았는지
9) 실전 체크리스트(10분 컷)
아래 순서대로 보면 대개 빠르게 해결됩니다.
- 에러가 STS인지 서비스 API인지 로그로 구분
- Workflow에
permissions: id-token: write존재 확인 role-to-assumeARN이 정확한지(계정/이름/경로)- IAM에 OIDC Provider
token.actions.githubusercontent.com존재 확인 - Trust policy의
Principal.Federated가 Provider ARN과 일치하는지 - Trust policy의
aud가sts.amazonaws.com인지 - Trust policy의
sub가 실제 실행 컨텍스트와 일치하는지 - CloudTrail에서
AssumeRoleWithWebIdentity이벤트로 성공/실패 확정 - Assume 성공이면 Role permissions + 리소스 정책(S3/ECR/KMS 등) 점검
- 마지막으로 조직 SCP/Permission boundary 여부 확인
10) 결론: 403을 “조건 불일치”로 생각하면 빨라진다
GitHub Actions OIDC의 AssumeRole 403은 대부분 OIDC 토큰 클레임(sub/aud) 과 IAM Trust policy 조건이 맞지 않아 발생합니다. 먼저 STS 단계에서 막히는지부터 분리하고, 실제 토큰 클레임을 확인한 뒤 Trust policy를 최소 권한으로 조정하면 해결 속도가 확 올라갑니다.
배포 파이프라인에서 403은 종종 네트워크/보안 계층까지 얽혀 증상이 비슷하게 보이기도 합니다. EKS/ALB/WAF 등 다른 계층의 403 디버깅 패턴이 궁금하다면 EKS AWS Load Balancer Controller 설치 후 403 해결도 함께 참고하면 좋습니다.