- Published on
GitHub Actions OIDC로 AWS 배포 권한 오류 해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버리스든 EKS든, GitHub Actions에서 AWS로 배포할 때 가장 많이 겪는 장애는 결국 OIDC 기반 Role Assume 실패입니다. 기존처럼 액세스 키를 저장하지 않고도 배포할 수 있다는 장점 때문에 aws-actions/configure-aws-credentials + OIDC 조합이 표준이 됐지만, 설정이 조금만 어긋나도 AccessDenied나 InvalidIdentityToken 같은 모호한 오류가 터집니다.
이 글은 “왜 실패하는지”를 AWS IAM(신뢰 정책) ↔ GitHub OIDC 토큰(클레임) ↔ Actions 워크플로 권한의 3축으로 나눠, 재현 가능한 방식으로 해결하는 방법을 정리합니다.
> 참고: OIDC/STS 계열의 403은 EKS에서도 동일한 원리로 발생합니다. STS 서명/토큰 계열 이슈를 더 넓게 보고 싶다면 EKS Pod에서 STS 403 SignatureDoesNotMatch 해결도 함께 보면 원인 분리가 빨라집니다.
1) 증상별로 보는 대표 오류 메시지
GitHub Actions OIDC로 AWS에 접근할 때 오류는 대개 아래 중 하나로 수렴합니다.
1. Not authorized to perform sts:AssumeRoleWithWebIdentity
- 의미: 대상 Role의 Trust policy가 OIDC 토큰을 신뢰하지 않거나, 조건(Condition)이 불일치.
- 포인트: “권한 정책”이 아니라 “신뢰 정책” 문제인 경우가 많습니다.
2. InvalidIdentityToken: No OpenIDConnect provider found in your account
- 의미: IAM에 GitHub OIDC Provider(
token.actions.githubusercontent.com)가 없거나, ARN/URL이 틀림.
3. AccessDenied: Access denied for operation ... (예: ECR, S3, CloudFormation)
- 의미: Role Assume은 성공했지만, Permission policy(권한 정책) 가 부족.
4. Credentials could not be loaded / NoCredentialProviders
- 의미: Actions에서 OIDC 토큰을 발급받지 못했거나(
permissions: id-token: write누락), configure 단계가 실패.
2) OIDC 기반 배포의 동작 흐름(문제 지점 지도)
OIDC 배포는 대략 다음 순서로 진행됩니다.
- GitHub Actions가 OIDC 토큰(JWT)을 발급 받음
aws-actions/configure-aws-credentials가 해당 토큰으로sts:AssumeRoleWithWebIdentity호출- STS가 Role의 Trust policy 조건을 검증
- 임시 자격 증명 발급 → 이후 AWS API 호출(ECR/S3/EKS/CloudFormation 등)
따라서 문제는 1) 토큰 발급, 2) Trust policy, 3) Permission policy, 4) 리전/리소스 ARN 불일치 중 하나입니다.
3) GitHub Actions 워크플로 설정: 가장 먼저 확인할 2줄
OIDC 토큰 발급 자체가 안 되면 나머지는 전부 실패합니다. 워크플로에 아래가 반드시 있어야 합니다.
permissions: id-token: writepermissions: contents: read(대부분 필요)
예시: 최소 동작 워크플로
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/gha-deploy-role
aws-region: ap-northeast-2
- name: Verify caller identity
run: aws sts get-caller-identity
체크포인트
aws sts get-caller-identity가 성공하면 OIDC Assume은 성공입니다.- 이후 단계에서 실패하면 권한 정책 또는 리소스/리전 문제로 좁혀집니다.
4) IAM OIDC Provider 생성: URL/Thumbprint보다 중요한 것
AWS 계정에 GitHub OIDC Provider가 있어야 합니다.
- Provider URL:
https://token.actions.githubusercontent.com - Audience(client id): 보통
sts.amazonaws.com
CLI로 확인:
aws iam list-open-id-connect-providers
aws iam get-open-id-connect-provider \
--open-id-connect-provider-arn arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com
자주 하는 실수
- Provider URL에
https://를 빼거나, 끝에/를 붙여 ARN이 다르게 생성됨 - 다른 계정(또는 다른 조직)에서 만든 Role ARN을 참조
5) Trust policy(신뢰 정책) 설계: sub 조건이 90%의 원인
OIDC Assume 실패의 대부분은 Trust policy의 조건이 GitHub 토큰의 클레임과 맞지 않아서입니다.
GitHub OIDC 토큰에는 대표적으로 아래 클레임이 들어갑니다.
iss:https://token.actions.githubusercontent.comaud:sts.amazonaws.comsub: 저장소/브랜치/환경에 따라 달라지는 식별자
권장 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"
}
}
}
]
}
sub가 달라지는 케이스(환경/태그/PR)
- main 브랜치:
repo:ORG/REPO:ref:refs/heads/main - 태그:
repo:ORG/REPO:ref:refs/tags/v1.2.3 - PR:
repo:ORG/REPO:pull_request - Environments 사용 시:
repo:ORG/REPO:environment:prod
따라서 실제 운영에서는 다음 전략 중 하나를 선택합니다.
- 엄격하게 제한: main 브랜치만 허용(가장 안전)
- 태그 릴리즈 허용:
refs/tags/* - GitHub Environment 기반:
environment:prod만 허용
예: 태그도 허용
"StringLike": {
"token.actions.githubusercontent.com:sub": [
"repo:my-org/my-repo:ref:refs/heads/main",
"repo:my-org/my-repo:ref:refs/tags/*"
]
}
6) Permission policy(권한 정책): Assume 성공 후 AccessDenied를 끝내는 법
get-caller-identity가 되는데 배포가 실패한다면, 이제는 Role에 붙은 권한 정책을 봐야 합니다.
예를 들어 ECR 푸시를 한다면 최소 권한은 다음이 필요합니다.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ECRPushPull",
"Effect": "Allow",
"Action": [
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:CompleteLayerUpload",
"ecr:UploadLayerPart",
"ecr:InitiateLayerUpload",
"ecr:PutImage",
"ecr:BatchGetImage",
"ecr:GetDownloadUrlForLayer"
],
"Resource": "*"
}
]
}
S3 업로드가 필요하면 버킷/프리픽스 단위로 리소스를 제한하세요.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "UploadArtifacts",
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:AbortMultipartUpload"],
"Resource": "arn:aws:s3:::my-bucket/deploy/*"
},
{
"Sid": "ListBucket",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::my-bucket",
"Condition": {
"StringLike": {"s3:prefix": "deploy/*"}
}
}
]
}
디버깅 팁: CloudTrail로 “거부된 액션”을 확정
- CloudTrail Event history에서
errorCode=AccessDenied이벤트를 찾으면- 어떤 Principal(Role session)이
- 어떤 Action을
- 어떤 Resource에
- 어떤 조건 때문에 거부됐는지 를 바로 확인할 수 있습니다.
7) 실전에서 자주 터지는 함정 7가지
1) permissions 누락으로 OIDC 토큰 자체가 없음
- 증상:
NoCredentialProviders,Could not load credentials - 해결: 워크플로 최상단에
permissions: id-token: write
2) Trust policy에서 aud 조건 누락/오타
- 증상: AssumeRoleWithWebIdentity 거부
- 해결:
token.actions.githubusercontent.com:aud = sts.amazonaws.com
3) sub가 환경/태그/브랜치와 불일치
- 증상: main에서는 되는데 tag 릴리즈에서만 실패(또는 반대)
- 해결: 실제 배포 트리거에 맞춰
sub패턴을 설계
4) Organization/Repository 이름 대소문자, 포크 PR 등 이벤트 차이
- 증상: 내부 PR은 되는데 fork PR은 실패
- 설명: 보안상 fork PR에는 OIDC 토큰 권한이 제한될 수 있음
- 해결: 배포는
push/workflow_dispatch/protected environment로 제한
5) Role ARN 계정이 다름(멀티 계정 운영에서 흔함)
- 증상: 존재하는 Role인데도 Assume 실패/권한 꼬임
- 해결:
role-to-assume의 account id, provider arn을 동일 계정으로 맞춤
6) 세션 정책/Permission boundary/SCP(Organizations)로 인한 차단
- 증상: 정책상 허용인데도 AccessDenied
- 해결:
- Role에 Permission boundary가 있는지
- AWS Organizations SCP가 차단하는지
- configure-aws-credentials에 session policy를 넣었는지 확인
7) 캐시/빌드 이슈를 권한 문제로 오해
- 증상: 배포 단계가 아니라 빌드 산출물이 없어서 실패하는데 “AWS 문제”처럼 보임
- 해결: Actions 캐시/아티팩트 흐름을 점검. 관련해서 GitHub Actions 캐시 안 먹힘 원인 7가지를 함께 확인하면 헛다리 줄일 수 있습니다.
8) 디버깅을 빠르게 만드는 “토큰 클레임 확인” 방법
GitHub OIDC 토큰의 실제 sub를 알아야 Trust policy를 정확히 만들 수 있습니다. 가장 깔끔한 방법은 actions/github-script나 간단한 Node/Python으로 ACTIONS_ID_TOKEN_REQUEST_URL을 호출해 토큰을 받아 디코드하는 것입니다.
아래는 토큰을 받아 payload를 출력하는 예시입니다.
- name: Print OIDC token claims (debug)
shell: bash
env:
REQ_URL: ${{ env.ACTIONS_ID_TOKEN_REQUEST_URL }}
REQ_TOKEN: ${{ env.ACTIONS_ID_TOKEN_REQUEST_TOKEN }}
run: |
set -euo pipefail
echo "Requesting token..."
JWT=$(curl -sS -H "Authorization: Bearer $REQ_TOKEN" "$REQ_URL&audience=sts.amazonaws.com" | jq -r .value)
echo "$JWT" | awk -F. '{print $2}' | tr '_-' '/+' | base64 -d 2>/dev/null | jq .
- 출력된 JSON에서
sub,aud,iss를 Trust policy와 1:1로 맞추면 됩니다. - 단, 이 단계는 토큰 정보를 로그에 남기므로 일시적으로만 사용하고 제거하세요.
9) 권장 운영 패턴: 최소 권한 + 배포 경로 고정
실무에서 안정적으로 굴리려면 아래 조합이 가장 사고가 적습니다.
- Trust policy
aud = sts.amazonaws.comsub = repo:ORG/REPO:ref:refs/heads/main또는environment:prod
- Permission policy
- 필요한 서비스(ECR/S3/CloudFormation/EKS)에만 최소 권한
- 리소스 ARN을 가능한 좁게
- 워크플로
- 배포는
workflow_dispatch+ GitHub Environment 승인(선택) aws sts get-caller-identity로 Assume 성공을 먼저 검증
- 배포는
EKS로 배포하는 경우, 이후 단계에서 네트워크/인증이 섞여 더 복잡해질 수 있습니다. IRSA/OIDC 전반의 복구 관점은 EKS OIDC Provider 삭제로 IRSA 전부 실패했을 때 복구도 유사한 사고 대응에 도움이 됩니다.
10) 마무리 체크리스트(이대로만 보면 대부분 해결)
- 워크플로에
permissions: id-token: write가 있는가? - AWS 계정에 OIDC Provider(
token.actions.githubusercontent.com)가 존재하는가? - Role Trust policy에
aud=sts.amazonaws.com조건이 있는가? - Trust policy의
sub가 실제 트리거(브랜치/태그/환경)와 일치하는가? -
aws sts get-caller-identity가 성공하는가? - 성공 후 실패한다면 CloudTrail에서 거부된 Action/Resource를 확인했는가?
- Permission boundary/SCP 등 상위 제약이 있는가?
위 체크리스트를 순서대로 밟으면, “OIDC라서 어렵다”기보다 문제 지점이 어디인지 빠르게 특정할 수 있습니다. 특히 get-caller-identity를 경계로 Trust 문제와 Permission 문제를 나누면, 디버깅 시간이 체감상 절반 이하로 줄어듭니다.