Published on

GitHub Actions OIDC로 AWS 배포 실패 해결 가이드

Authors

서버리스든 EC2든, GitHub Actions로 AWS에 배포할 때 가장 깔끔한 인증 방식은 OIDC(OpenID Connect)입니다. 장기 액세스 키를 저장하지 않아도 되고, 워크플로 실행 시점에만 STS 임시 자격 증명을 발급받아 사용하므로 보안과 운영 편의성이 모두 좋아집니다.

그런데 OIDC는 한 번 설정이 어긋나면 배포가 통째로 막히는 경우가 많습니다. 특히 AccessDenied, InvalidIdentityToken, Not authorized to perform sts:AssumeRoleWithWebIdentity 같은 에러는 원인이 다양해서 로그만 보고는 감이 안 올 때가 많습니다.

이 글에서는 GitHub Actions OIDC로 AWS 배포가 실패할 때, 어디부터 확인해야 하는지와 가장 흔한 실수 패턴을 “재현 가능한 설정”과 함께 정리합니다.

문제 유형 중 AccessDenied를 집중적으로 다룬 글은 아래 내부 링크도 함께 참고하면 좋습니다.


OIDC 동작 구조를 먼저 간단히 정리

OIDC 기반 배포 흐름은 대략 아래 순서로 진행됩니다.

  1. GitHub Actions 러너가 OIDC 토큰을 발급받음
  2. AWS IAM Role의 Trust Policy가 해당 토큰의 발급자와 클레임을 검증
  3. 검증되면 AWS STS가 임시 자격 증명(AccessKeyId, SecretAccessKey, SessionToken)을 발급
  4. 이후 AWS CLI나 SDK가 이 임시 자격 증명으로 배포 작업 수행

즉, 실패 지점은 크게 두 군데입니다.

  • STS AssumeRoleWithWebIdentity 단계에서 실패(신뢰 정책, OIDC Provider, 클레임 조건 문제)
  • AssumeRole은 성공했는데 실제 AWS API 호출 권한이 부족(권한 정책 문제)

로그에서 먼저 구분해야 합니다.

  • AssumeRoleWithWebIdentity 자체가 거부되면 Trust Policy나 OIDC Provider 문제
  • AssumeRole은 되었는데 s3:PutObject, cloudformation:*, ecr:* 등이 거부되면 Permission Policy 문제

GitHub Actions 워크플로 기본 예제(정상 동작 기준)

가장 먼저 워크플로가 OIDC 토큰을 받을 권한이 있는지 확인해야 합니다. 핵심은 permissions: id-token: write 입니다.

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: Who am I
        run: aws sts get-caller-identity

여기서 aws sts get-caller-identity가 실패하면, 배포 로직 이전에 인증/신뢰 설정부터 깨진 겁니다.


실패 1: id-token 권한 누락으로 OIDC 토큰 발급이 안 됨

증상

  • No OpenIDConnect provider found와는 다른 형태로, GitHub 쪽에서 토큰을 못 받는 로그가 보이거나
  • aws-actions/configure-aws-credentials 단계에서 토큰 관련 에러가 발생

해결

워크플로 상단 또는 job 단위에 아래 권한이 반드시 있어야 합니다.

permissions:
  id-token: write
  contents: read

조직 정책이나 리포지토리 설정에서 워크플로 권한이 제한되어 있으면, 리포지토리 Settings의 Actions 권한도 함께 확인하세요.


실패 2: IAM OIDC Provider 설정 불일치(발급자 URL, Audience)

증상

  • InvalidIdentityToken 또는 No OpenIDConnect provider found in your account for https://token.actions.githubusercontent.com

원인

AWS 계정에 GitHub OIDC Provider가 없거나, Provider의 URL이 다르거나, Audience 조건이 맞지 않으면 STS가 토큰을 신뢰하지 않습니다.

GitHub Actions의 표준 발급자(issuer)는 아래입니다.

  • https://token.actions.githubusercontent.com

해결: OIDC Provider 생성 확인

AWS 콘솔에서 IAM Identity providers에 OIDC Provider가 존재하는지 확인합니다.

CLI로도 확인 가능합니다.

aws iam list-open-id-connect-providers

Provider ARN을 확인한 뒤 상세를 봅니다.

aws iam get-open-id-connect-provider \
  --open-id-connect-provider-arn arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com

여기서 Urltoken.actions.githubusercontent.com인지, ClientIDListsts.amazonaws.com가 포함되어 있는지 확인하세요. 일반적으로 GitHub Actions OIDC의 audience는 sts.amazonaws.com로 두는 구성이 가장 흔합니다.


실패 3: Trust Policy 조건 미스매치(특히 sub)

OIDC에서 가장 많이 틀리는 부분이 Role의 Trust Policy 조건입니다. 특히 sub 클레임을 너무 빡세게 걸어두면 브랜치나 환경이 바뀌는 순간 바로 실패합니다.

대표 증상

  • AccessDenied: Not authorized to perform sts:AssumeRoleWithWebIdentity
  • The role ... cannot be assumed 류 메시지

sub 클레임 형태 이해

GitHub OIDC 토큰의 sub는 보통 아래 패턴 중 하나입니다.

  • 브랜치 기반: repo:ORG/REPO:ref:refs/heads/main
  • 태그 기반: repo:ORG/REPO:ref:refs/tags/v1.2.3
  • 환경 기반: repo:ORG/REPO:environment:prod

즉, Trust Policy에서 sub를 브랜치로 묶으면, 태그 배포나 환경 배포로 바꾸는 순간 AssumeRole이 실패합니다.

권장 Trust Policy 예시

아래는 “특정 리포지토리에서만” Role을 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"
        },
        "StringLike": {
          "token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:*"
        }
      }
    }
  ]
}
  • aud는 정확히 일치시키는 편이 안전합니다.
  • sub는 최소한 repo:ORG/REPO:* 수준으로 시작한 뒤, 요구 보안 수준에 맞춰 ref 또는 environment로 점진적으로 좁히는 전략이 운영에서 덜 흔들립니다.

실패 4: 브랜치 보호, PR 이벤트에서 sub가 달라짐

증상

  • push에서는 되는데 pull_request에서만 AssumeRole 실패
  • 또는 main 브랜치에서는 되는데 릴리즈 브랜치에서만 실패

원인

이벤트 타입에 따라 토큰 클레임이 달라집니다. 예를 들어 PR 기반 워크플로는 ref가 기대와 다르게 들어가거나, 보안상 제한된 토큰이 내려올 수 있습니다.

해결 전략

  1. 배포 워크플로는 가급적 push 또는 workflow_dispatch로만 실행
  2. pull_request에서는 배포 대신 빌드/테스트만 수행
  3. Trust Policy의 sub 조건을 이벤트별로 분기하거나, 배포 이벤트만 허용하도록 설계

예를 들어 배포는 수동 실행만 허용하고, 환경을 명시하는 패턴도 자주 씁니다.

on:
  workflow_dispatch:
    inputs:
      env:
        required: true
        type: choice
        options: ["dev", "prod"]

그리고 Trust Policy는 environment:prod만 허용하도록 좁힐 수 있습니다.


실패 5: AssumeRole은 성공했는데 배포 API 권한이 부족함

증상

  • aws sts get-caller-identity는 성공
  • 하지만 이후 단계에서 AccessDenied가 발생
  • 예: ECR 푸시, S3 업로드, CloudFormation 배포, ECS 업데이트 등

원인

Role의 Permission Policy에 필요한 액션이 빠졌습니다. OIDC 문제라기보다 IAM 권한 설계 문제입니다.

해결: 최소 권한으로 필요한 액션을 역추적

CloudTrail의 Event nameError code를 보면 어떤 액션이 거부됐는지 빠르게 찾을 수 있습니다.

예를 들어 ECR 푸시에는 최소한 아래가 자주 필요합니다.

  • ecr:GetAuthorizationToken
  • ecr:BatchCheckLayerAvailability
  • ecr:InitiateLayerUpload
  • ecr:UploadLayerPart
  • ecr:CompleteLayerUpload
  • ecr:PutImage

S3 업로드라면 보통 아래가 필요합니다.

  • s3:PutObject
  • s3:PutObjectAcl(필요한 경우에만)
  • s3:ListBucket(동기화나 존재 확인 시)

정확한 액션은 “실패한 API 호출” 기준으로 추가하는 방식이 가장 빠릅니다.


실패 6: 리전 또는 STS 엔드포인트 혼선

증상

  • 특정 리전에서만 실패
  • 또는 로컬에서는 되는데 Actions에서만 실패

체크 포인트

  • 워크플로에서 aws-region이 실제 리소스 리전과 일치하는지
  • 조직에서 STS를 특정 리전에만 허용하는 정책이 있는지

가능하면 워크플로 초기에 아래를 찍어서 환경을 고정합니다.

- name: Print AWS region
  run: |
    aws configure list
    echo "AWS_REGION=$AWS_REGION"
    echo "AWS_DEFAULT_REGION=$AWS_DEFAULT_REGION"

실패 7: 디버깅을 위한 최소 재현 파이프라인 만들기

배포 파이프라인이 길어질수록 원인 파악이 어려워집니다. OIDC 문제를 확인할 때는 아래 2단계만 남긴 “최소 재현” 워크플로로 줄여서 확인하는 게 좋습니다.

  1. OIDC로 Role Assume
  2. get-caller-identity 호출
name: oidc-smoke-test
on:
  workflow_dispatch:

permissions:
  id-token: write
  contents: read

jobs:
  smoke:
    runs-on: ubuntu-latest
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/gha-deploy-role
          aws-region: ap-northeast-2

      - run: aws sts get-caller-identity

이게 통과하면 “OIDC 신뢰 체인”은 정상입니다. 이후는 순수하게 권한 정책 또는 배포 도구 설정 문제로 범위를 좁히면 됩니다.


운영 팁: 보안과 안정성을 함께 잡는 권장 패턴

1) Role을 배포 대상별로 쪼개기

하나의 Role에 모든 권한을 몰아주면 편하지만, 나중에 사고가 납니다.

  • gha-deploy-s3-role
  • gha-deploy-ecr-role
  • gha-deploy-ecs-role

처럼 분리하고, 워크플로에서 필요한 Role만 Assume하도록 설계하면 권한 최소화가 쉬워집니다.

2) sub를 너무 타이트하게 시작하지 않기

초기에 ref:refs/heads/main까지 박아두면, 릴리즈 태그 배포나 핫픽스 브랜치가 생겼을 때 운영이 자주 멈춥니다. 처음엔 repo:ORG/REPO:*로 안정화하고, 이후 환경 기반으로 좁히는 방법이 실무에서 덜 고통스럽습니다.

3) 장애 추적 접근법을 표준화하기

OIDC든 어떤 장애든, “로그를 어디서부터 좁힐지”가 중요합니다. 원인 추적 사고방식 자체는 다른 시스템 장애에도 그대로 적용됩니다. 예를 들어 서비스가 계속 재시작될 때 원인을 좁히는 방식은 아래 글과 결이 비슷합니다.


체크리스트: OIDC 배포 실패 시 5분 진단 순서

  1. 워크플로에 permissions: id-token: write가 있는가
  2. aws-actions/configure-aws-credentials 다음에 aws sts get-caller-identity가 성공하는가
  3. IAM에 OIDC Provider가 있고 URL이 token.actions.githubusercontent.com인가
  4. Role Trust Policy에서 audsts.amazonaws.com로 맞는가
  5. Trust Policy의 sub 조건이 실제 배포 이벤트의 sub와 맞는가
  6. AssumeRole은 되는데 API가 막히면 Permission Policy를 CloudTrail로 역추적했는가

마무리

GitHub Actions OIDC는 한 번 제대로 잡아두면 장기 키 관리에서 해방되고, 조직 보안 감사에도 강해집니다. 반대로 초기 설정에서 aud, sub, Provider URL, 워크플로 권한 중 하나만 어긋나도 배포가 바로 멈춥니다.

가장 빠른 해결법은 “OIDC 신뢰 문제인지, AWS 권한 문제인지”를 먼저 분리하고, get-caller-identity를 기준 테스트로 삼아 범위를 좁히는 것입니다. 그 다음 Trust Policy의 sub를 이벤트 특성에 맞게 조정하고, 실제 배포 API 권한은 CloudTrail 기반으로 최소 권한을 쌓아가면 재발이 크게 줄어듭니다.