Published on

GitHub Actions OIDC로 AWS 권한 오류 해결하기

Authors

서론

GitHub Actions로 AWS에 배포 파이프라인을 만들 때, 장기 Access Key를 저장하지 않고 OIDC(OpenID Connect) 로 STS 임시 자격 증명을 발급받는 구성이 사실상 표준이 됐습니다. 그런데 막상 적용하면 AccessDenied, Not authorized to perform sts:AssumeRoleWithWebIdentity, InvalidIdentityToken, No OpenIDConnect provider found 같은 권한 오류를 자주 만나게 됩니다.

이 글은 “OIDC로 바꿨는데 권한 오류가 난다” 상황에서, 무엇을 어디서 확인해야 하는지(신뢰 정책/권한 정책/GitHub 워크플로우 permissions/조건식/audience) 를 재현 가능한 형태로 정리한 트러블슈팅 가이드입니다. STS 토큰 관련 403 케이스는 AWS STS 토큰 만료로 403? IRSA·AssumeRole 점검 도 함께 보면 디버깅 속도가 빨라집니다.

OIDC 흐름을 먼저 이해하기 (어디서 막히는지 분리)

GitHub Actions OIDC는 대략 다음 순서로 동작합니다.

  1. 워크플로우가 실행되면 GitHub가 OIDC ID 토큰(JWT)을 발급
  2. aws-actions/configure-aws-credentials가 그 토큰을 AWS STS에 전달
  3. STS가 IAM Role의 Trust Policy(신뢰 정책) 를 검사
  4. 통과하면 임시 자격 증명(AccessKeyId/SecretAccessKey/SessionToken)을 반환
  5. 이후 AWS API 호출은 그 Role의 권한 정책(permissions policy) 로 통제

따라서 오류는 크게 두 부류입니다.

  • AssumeRole 단계에서 실패: Trust Policy, OIDC Provider, audience/sub 조건, GitHub permissions 문제
  • AssumeRole은 성공했지만 AWS API 호출이 실패: Role 권한 정책 부족(ecr:PutImage, ecs:UpdateService 등)

이 글은 첫 번째(AssumeRole/권한오류) 중심으로 다룹니다.

가장 흔한 증상과 메시지별 원인 맵

1) No OpenIDConnect provider found in your account

원인: AWS 계정에 GitHub OIDC Provider가 아직 없거나, ARN이 다른 계정/리전에 생성됨.

해결: IAM > Identity providers에 token.actions.githubusercontent.com Provider 생성.

2) Not authorized to perform sts:AssumeRoleWithWebIdentity

원인: Role Trust Policy가 OIDC Principal/Condition을 허용하지 않음.

해결: Trust Policy에 Federated Principal(Provider ARN) + sts:AssumeRoleWithWebIdentity + 조건(sub/aud)을 올바르게 추가.

3) InvalidIdentityToken: audience is invalid

원인: GitHub OIDC 토큰의 aud(audience)와 Trust Policy 조건이 불일치.

해결: 보통 audsts.amazonaws.com을 사용. 워크플로우에서 커스텀 audience를 썼다면 Trust Policy와 맞춰야 함.

4) AccessDenied: User is not authorized to perform: ecr:PutImage ...

원인: AssumeRole은 성공했으나 Role permission policy에 해당 액션 권한이 없음.

해결: 배포 대상 서비스(ECR/ECS/EKS/S3/CloudFront 등)에 맞는 권한을 Role에 부여.

필수 전제: GitHub Actions permissions 설정

OIDC 토큰을 발급받으려면 워크플로우에 다음 권한이 필요합니다.

  • permissions: id-token: write
  • permissions: contents: read (대부분의 체크아웃/빌드에 필요)

이게 빠지면 configure-aws-credentials가 토큰을 못 받아서 실패합니다. 캐시/권한이 꼬이는 케이스는 GitHub Actions 캐시가 안 먹을 때 - key·restore-keys·권한 도 같이 점검하면 좋습니다.

아래는 최소 예시입니다.

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

aws sts get-caller-identity가 통과하면 OIDC AssumeRole은 성공한 것입니다. 이후 오류는 대부분 “Role 권한 정책” 문제로 좁혀집니다.

AWS IAM OIDC Provider 생성 (정석)

IAM 콘솔에서 생성해도 되지만, IaC(Terraform/CloudFormation)로 관리하는 게 안전합니다. 핵심 값은 다음입니다.

  • URL: https://token.actions.githubusercontent.com
  • Audience: sts.amazonaws.com
  • Thumbprint: GitHub 문서/가이드에 맞는 값(현재는 AWS 콘솔이 자동 처리하는 경우가 많음)

Terraform 예시는 다음과 같습니다.

resource "aws_iam_openid_connect_provider" "github" {
  url = "https://token.actions.githubusercontent.com"

  client_id_list = [
    "sts.amazonaws.com"
  ]

  # thumbprint_list는 환경에 따라 필요/불필요할 수 있습니다.
  # thumbprint_list = ["..."]
}

이 Provider ARN이 Trust Policy의 Principal.Federated에 들어갑니다.

Trust Policy(신뢰 정책)에서 가장 많이 틀리는 부분

OIDC는 “누가 이 Role을 Assume할 수 있는가”를 Trust Policy로 엄격히 제한해야 합니다. 특히 GitHub는 sub 클레임이 강력한 제어 포인트입니다.

권장 Trust Policy 예시 (repo + branch 제한)

아래 예시는 my-org/my-repomain 브랜치에서만 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:ref:refs/heads/main"
        }
      }
    }
  ]
}

체크포인트

  • Principal.Federated정확한 Provider ARN인지
  • Actionsts:AssumeRoleWithWebIdentity인지
  • aud 조건이 실제 토큰과 일치하는지(대부분 sts.amazonaws.com)
  • sub 조건이 너무 넓거나(보안 위험) 너무 좁아서(권한 오류) 매칭이 안 되는지

PR / 태그 / 환경별로 sub가 달라지는 함정

GitHub OIDC의 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

따라서 “main 브랜치만 허용”으로 해놓고 태그 배포를 하면 AssumeRole이 거부될 수 있습니다. 태그 배포를 허용하려면 예를 들어 다음처럼 확장합니다.

"StringLike": {
  "token.actions.githubusercontent.com:sub": [
    "repo:my-org/my-repo:ref:refs/heads/main",
    "repo:my-org/my-repo:ref:refs/tags/v*"
  ]
}

audience(aud) 불일치로 인한 InvalidIdentityToken 해결

configure-aws-credentials는 기본적으로 audience=sts.amazonaws.com을 사용합니다. 하지만 조직 정책이나 멀티 클라우드 연동 등으로 audience를 커스텀하는 경우가 있습니다.

  • 워크플로우에서 audience를 바꿨다
  • Trust Policy는 여전히 sts.amazonaws.com만 허용한다

이러면 InvalidIdentityToken이 납니다.

워크플로우에서 커스텀 audience를 쓰는 예:

- name: Configure AWS credentials (custom audience)
  uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: arn:aws:iam::123456789012:role/gha-deploy-role
    aws-region: ap-northeast-2
    audience: my-custom-audience

이 경우 Trust Policy도 동일하게 맞춰야 합니다.

"StringEquals": {
  "token.actions.githubusercontent.com:aud": "my-custom-audience"
}

“AssumeRole은 되는데 배포가 안 됨” = Role 권한 정책 문제

aws sts get-caller-identity가 성공했는데도 ECR/ECS/S3에서 AccessDenied가 나면, 이제는 OIDC 문제가 아니라 Role permission policy를 봐야 합니다.

예를 들어 ECR에 푸시하는 파이프라인이면 최소한 다음 권한이 필요합니다(상황에 따라 더 필요).

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ecr:GetAuthorizationToken",
        "ecr:BatchCheckLayerAvailability",
        "ecr:InitiateLayerUpload",
        "ecr:UploadLayerPart",
        "ecr:CompleteLayerUpload",
        "ecr:PutImage"
      ],
      "Resource": "*"
    }
  ]
}

EKS로 이어지는 배포라면 ECR Pull/Push와 더불어 클러스터 접근(aws-auth, kubectl 인증)까지 얽힐 수 있습니다. 이미지 풀 단계에서 막히는 경우는 권한 외에도 레이트리밋/네트워크 요인이 있어, EKS에서 ECR ImagePullBackOff 429 해결법 도 참고할 만합니다.

빠르게 원인 좁히는 디버깅 체크리스트

1) OIDC 토큰 발급 권한 확인

  • 워크플로우 최상단 또는 job 단위에 permissions: id-token: write가 있는가?
  • Organization/Repository 설정에서 Actions 권한이 제한되어 있지 않은가?

2) AssumeRole 성공 여부를 단일 커맨드로 확인

- run: |
    aws sts get-caller-identity
    aws configure list
  • 여기서 실패하면 Trust Policy/OIDC Provider/permissions 문제
  • 성공하면 이후 단계의 AWS API 권한 문제

3) Trust Policy의 sub 조건이 실제 실행 컨텍스트와 맞는가?

  • main push인지, tag인지, PR인지
  • mono-repo에서 경로 조건을 쓰려는 경우(경로는 sub로 직접 제한이 어려움 → 별도 전략 필요)

4) 계정/Role ARN 혼동

  • role-to-assume가 올바른 AWS Account의 Role인가?
  • Provider ARN도 같은 계정에 존재하는가?

5) 세션 시간/토큰 만료

장시간 빌드/배포에서 STS 토큰 만료로 403이 나는 경우도 있습니다. 이 경우는 OIDC 자체보다는 세션 지속 시간, 재발급 타이밍, AssumeRole 체인 등을 점검해야 하며, 관련 포인트는 AWS STS 토큰 만료로 403? IRSA·AssumeRole 점검 에 정리해 두었습니다.

실전 예시: ECR 푸시 + ECS 배포 워크플로우

아래는 OIDC로 Role을 Assume한 뒤 ECR 로그인/푸시를 수행하는 예시입니다.

name: build-and-push

on:
  push:
    branches: ["main"]

permissions:
  id-token: write
  contents: read

jobs:
  ecr:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/gha-ecr-push-role
          aws-region: ap-northeast-2

      - run: aws sts get-caller-identity

      - name: Login to ECR
        run: |
          aws ecr get-login-password --region ap-northeast-2 \
            | docker login --username AWS --password-stdin 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com

      - name: Build & Push
        run: |
          docker build -t myapp:${{ github.sha }} .
          docker tag myapp:${{ github.sha }} 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/myapp:${{ github.sha }}
          docker push 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/myapp:${{ github.sha }}

이 워크플로우에서 흔한 실패 지점은 다음 두 가지입니다.

  • get-caller-identity 실패 → Trust Policy/OIDC Provider/permissions 문제
  • docker push에서 AccessDeniedException → ECR 권한 정책 부족

결론

GitHub Actions OIDC로 AWS 권한 오류를 해결하는 핵심은 “OIDC AssumeRole 단계”와 “AWS API 권한 단계”를 분리해서 보는 것입니다.

  • AssumeRole 단계 오류는 대부분 워크플로우 permissions(id-token: write), OIDC Provider 존재 여부, Trust Policy의 aud/sub 조건 불일치에서 발생합니다.
  • AssumeRole 성공 후의 오류는 거의 항상 Role permission policy 부족입니다.

먼저 aws sts get-caller-identity로 경계를 명확히 하고, Trust Policy의 sub를 실행 컨텍스트(브랜치/태그/PR)에 맞게 조정하면, 대부분의 OIDC 권한 오류는 빠르게 정리됩니다.