- Published on
GitHub Actions OIDC로 AWS 배포 권한 오류 해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
GitHub Actions로 AWS 배포 파이프라인을 만들 때, 가장 흔한 장애는 결국 “권한”입니다. 특히 OIDC(OpenID Connect)로 AssumeRoleWithWebIdentity를 쓰면 장기 액세스 키를 없앨 수 있어 보안적으로는 훨씬 낫지만, 설정이 조금만 어긋나도 AccessDenied나 InvalidIdentityToken 같은 오류가 바로 발생합니다.
이 글에서는 GitHub Actions OIDC 기반 배포에서 권한 오류가 나는 대표 패턴을 로그 기준으로 분류하고, AWS IAM의 신뢰 정책(trust policy)과 권한 정책(permission policy)을 어떻게 고쳐야 하는지 실전 형태로 정리합니다.
참고로 CI 최적화까지 함께 고민 중이라면 GitHub Actions 매트릭스 빌드로 CI 50% 줄이기도 같이 보면 파이프라인 설계에 도움이 됩니다.
OIDC 흐름을 먼저 한 장으로 이해하기
OIDC 방식은 요약하면 다음 순서입니다.
- GitHub Actions 러너가 GitHub OIDC Provider에서 ID 토큰을 발급받음
- AWS STS가 그 토큰을 검증하고, IAM Role의 신뢰 정책 조건을 만족하면 Assume을 허용
- 임시 자격 증명(AccessKeyId, SecretAccessKey, SessionToken)이 발급됨
- 이후 AWS CLI/SDK는 그 임시 자격 증명으로 배포 작업 수행
즉, 오류는 크게 두 군데에서 납니다.
- STS가 토큰을 “검증”하지 못함:
InvalidIdentityToken, OIDC provider 설정 문제 - STS는 토큰을 검증했지만 Role Assume이 “거부”됨: trust policy 조건 불일치
- Role Assume은 됐지만 실제 AWS API 호출이 “거부”됨: permission policy 부족
오류 메시지별 원인 맵
1) No OpenIDConnect provider found in your account
- AWS 계정에 GitHub OIDC Provider가 등록되지 않았거나, ARN이 다른 계정/리전에 있음
- Provider URL이 정확히
token.actions.githubusercontent.com이 아닌 경우
2) InvalidIdentityToken: audience is invalid
- GitHub OIDC 토큰의
aud클레임과 AWS에서 기대하는aud가 불일치 - 보통
aws-actions/configure-aws-credentials에서audience를 바꿨거나, trust policy에서aud조건을 잘못 걸었을 때 발생
3) AccessDenied: Not authorized to perform sts:AssumeRoleWithWebIdentity
- trust policy가
sts:AssumeRoleWithWebIdentity를 허용하지 않음 sub조건이 브랜치/환경/리포지토리와 불일치
4) AccessDeniedException 또는 AccessDenied (ECR, S3, CloudFormation 등)
- Role Assume은 성공했으나, 실제 배포 대상 서비스 권한이 부족
- 예: ECR push 권한에서
ecr:GetAuthorizationToken누락, S3 업로드에서s3:PutObject누락
AWS에서 GitHub OIDC Provider 만들기
이미 구성되어 있다면 건너뛰어도 됩니다.
콘솔에서 생성
- IAM → Identity providers → Add provider
- Provider type:
OpenID Connect - Provider URL:
https://token.actions.githubusercontent.com - Audience: 보통
sts.amazonaws.com
CLI로 확인
아래 명령으로 provider가 있는지 확인합니다.
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`
MDX 렌더링 환경에서 부등호가 그대로 노출되면 빌드 에러가 날 수 있으므로, ARN의 꺾쇠나 제네릭 표기 같은 문자를 본문에 직접 쓰지 말고 항상 코드 블록 또는 인라인 코드로 감싸는 습관이 안전합니다.
핵심 1: IAM Role 신뢰 정책(trust policy) 제대로 만들기
신뢰 정책은 “누가 이 Role을 Assume할 수 있는가”를 정의합니다. GitHub OIDC에서는 Principal이 OIDC provider이고, Action은 sts:AssumeRoleWithWebIdentity입니다.
가장 많이 쓰는 신뢰 정책 예시
아래는 특정 리포지토리의 main 브랜치에서만 Assume을 허용하는 예시입니다.
{
"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:my-org/my-repo:ref:refs/heads/main"
}
}
}
]
}
여기서 자주 틀리는 포인트
sub값이 실제와 다름- 브랜치가
main이 아닌데main으로 고정 - 태그 배포인데
refs/tags/...를 고려하지 않음
- 브랜치가
- 환경 보호 규칙을 쓰는 경우
environment기반sub패턴을 써야 하는데 브랜치 패턴으로 걸어둠
브랜치가 아니라 GitHub Environment로 제한하기
GitHub Environment(예: prod)로 배포를 통제한다면 sub가 다음 형태가 됩니다.
repo:ORG/REPO:environment:prod
예시:
{
"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:environment:prod"
}
}
}
]
}
StringEquals 대신 StringLike를 쓰는 이유는, 팀에서 점진적으로 조건을 확장할 때 와일드카드가 필요해지는 경우가 많기 때문입니다. 다만 보안상 과도한 와일드카드는 피해야 합니다.
핵심 2: GitHub Actions 워크플로에서 OIDC 권한 선언하기
OIDC 토큰 발급 자체가 GitHub Actions 권한에 의해 막히는 경우가 있습니다. 워크플로에 permissions가 없거나 id-token: write가 빠지면 configure-aws-credentials가 토큰을 못 받아 실패합니다.
최소 권한 워크플로 예시
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
여기서 Who am I 단계는 디버깅에 매우 유용합니다.
- Role Assume이 성공했는지 즉시 확인 가능
- 계정이 맞는지(멀티 계정 환경에서 특히) 확인 가능
핵심 3: Assume은 성공했는데 배포가 실패한다면 권한 정책을 의심
sts get-caller-identity는 통과하지만, ECR push나 S3 업로드에서 AccessDenied가 난다면 그건 trust policy 문제가 아니라 Role에 붙은 permission policy 문제입니다.
예: ECR에 이미지 푸시 권한 최소 세트
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ecr:GetAuthorizationToken"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"ecr:BatchCheckLayerAvailability",
"ecr:CompleteLayerUpload",
"ecr:InitiateLayerUpload",
"ecr:PutImage",
"ecr:UploadLayerPart",
"ecr:DescribeRepositories"
],
"Resource": "arn:aws:ecr:ap-northeast-2:123456789012:repository/my-repo"
}
]
}
자주 빠지는 권한
ecr:GetAuthorizationToken은Resource가*여야 하는 경우가 대부분입니다.kms:Decrypt가 필요한데 누락되는 경우도 많습니다(특히 ECR 또는 S3가 KMS로 암호화된 경우).
예: S3 정적 배포(업로드) 권한
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:DeleteObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::my-deploy-bucket",
"arn:aws:s3:::my-deploy-bucket/*"
]
}
]
}
S3는 ListBucket은 버킷 ARN, PutObject는 오브젝트 ARN이 필요해서 Resource를 둘 다 넣는 패턴이 흔합니다.
디버깅 체크리스트 (10분 컷)
1) GitHub에서 실제 OIDC 클레임 확인
워크플로에서 토큰을 직접 다루는 것은 권장되지 않지만, 디버깅 시에는 sub가 무엇으로 찍히는지 확인이 필요할 때가 있습니다. 안전한 방법은 aws-actions/configure-aws-credentials의 로그와, AWS CloudTrail의 STS 이벤트를 함께 보는 것입니다.
- CloudTrail에서 이벤트 이름
AssumeRoleWithWebIdentity를 검색 userIdentity.sessionContext.sessionIssuer.arn이 기대 Role인지 확인requestParameters.roleArn및 실패 사유 확인
2) trust policy의 sub 조건을 “너무 빡세게” 걸지 않았는지 확인
처음에는 아래처럼 리포지토리 단위로만 제한하고, 이후 브랜치/환경 조건을 좁혀가는 접근이 운영에서 덜 흔들립니다.
token.actions.githubusercontent.com:sub를repo:my-org/my-repo:*로 시작- 정상 동작 확인 후
ref또는environment로 구체화
단, 이 완화는 배포 Role에만 적용하고, 강한 승인(환경 보호, 리뷰어 승인)을 병행하는 것이 좋습니다.
3) aud는 기본값인 sts.amazonaws.com으로 맞추기
특별한 이유가 없다면 aud는 sts.amazonaws.com으로 고정하는 것이 시행착오가 적습니다. configure-aws-credentials에서 audience를 커스터마이징했다면 trust policy도 동일하게 맞춰야 합니다.
4) 멀티 계정/멀티 Role에서 계정 혼동 제거
aws sts get-caller-identity 결과의 Account가 기대 값인지 확인하세요. 파이프라인이 여러 계정에 배포한다면, job마다 role-to-assume와 aws-region을 명시적으로 분리하는 편이 안전합니다.
5) 배포 이후 장애까지 함께 본다면
배포는 성공했는데 런타임에서 터지는 장애(예: 이미지 풀 실패, 크래시 루프)는 권한 문제와 별개로 운영 이슈로 이어집니다. 배포 파이프라인을 다듬는 김에 운영 진단 가이드도 같이 준비해두면 좋습니다.
보안 모범 사례: “성공”보다 “안전한 성공”
OIDC의 장점은 장기 키를 없애는 것뿐 아니라, 조건 기반으로 권한을 강하게 제한할 수 있다는 점입니다.
- trust policy에서
sub를 리포지토리 단위로 제한 - 가능하면 브랜치가 아니라 GitHub Environment로 제한하고, Environment 보호 규칙(승인자, 대기 타이머)을 적용
- permission policy는 배포에 필요한 최소 권한만 부여
- CloudTrail로
AssumeRoleWithWebIdentity이벤트를 상시 추적
마무리
GitHub Actions OIDC 기반 AWS 배포에서 권한 오류를 해결하는 핵심은 “어디서 막혔는지”를 먼저 분리하는 것입니다.
- STS 토큰 검증 단계 문제인지
- Role Assume(trust policy) 단계 문제인지
- 실제 AWS API 권한(permission policy) 문제인지
이 3단계로 나누어 보면, 대부분의 AccessDenied는 신뢰 정책의 sub 불일치 또는 배포 서비스 권한 누락으로 빠르게 수렴합니다. 위 예시 템플릿을 기준으로 trust policy를 정확히 맞추고, aws sts get-caller-identity로 단계별 확인을 넣으면 재발도 크게 줄일 수 있습니다.