Published on

GitHub Actions OIDC assume-role 실패 원인별 해결

Authors

서론

GitHub Actions에서 AWS 배포를 OIDC(OpenID Connect)로 전환하면 장기 액세스 키를 없앨 수 있어 보안과 운영이 크게 좋아집니다. 하지만 막상 aws-actions/configure-aws-credentials를 붙였는데 assume-role 단계에서 실패하면, 원인이 IAM Role 신뢰 정책(Trust Policy), OIDC 토큰 클레임 조건(sub/aud), 워크플로 권한(id-token), 조직/리포 정책, 세션 이름/조건 등으로 다양해 진단이 까다롭습니다.

이 글은 실패 로그를 기준으로 가장 자주 만나는 케이스를 “증상 → 원인 → 해결”로 정리합니다. OIDC 자체 개념/권한 설계는 아래 글도 함께 보면 전체 그림이 빨리 잡힙니다.

OIDC assume-role 흐름 한 장 요약

  1. GitHub Actions 러너가 OIDC 토큰(JWT)을 발급받음 (id-token: write 필요)
  2. sts:AssumeRoleWithWebIdentity 호출
  3. AWS IAM Role의 신뢰 정책이 토큰의 iss/aud/sub 등 조건을 만족하면 임시 자격 증명 발급
  4. 이후 AWS API 호출은 Role에 붙은 권한 정책(Permission Policy) 로 통제

즉, 실패는 크게 두 갈래입니다.

  • 신뢰 정책 불일치: STS가 Role을 “신뢰할 수 없음”으로 판단
  • 권한 정책 부족: Role은 assume 성공했지만 이후 API에서 AccessDenied

이번 글은 “assume-role 실패”에 집중합니다.

먼저 로그에서 확인할 3가지

1) 에러 메시지의 키워드

  • Not authorized to perform sts:AssumeRoleWithWebIdentity
  • No OpenIDConnect provider found in your account
  • The requested role is not assumable
  • InvalidIdentityToken / audience is invalid

2) 어떤 Action을 쓰는지

대부분 아래 액션을 씁니다.

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

3) 워크플로에 permissions가 있는지

OIDC 토큰 발급은 기본적으로 막혀 있는 경우가 많습니다.

permissions:
  id-token: write
  contents: read

이게 없으면 “assume-role 이전” 단계에서 토큰 자체를 못 받아 실패합니다.

케이스 1) id-token 권한 누락

증상

  • Error: Credentials could not be loaded, please check your action inputs: Could not load credentials from any providers
  • 또는 OIDC token 관련 메시지

원인

워크플로 권한에 id-token: write가 없어서 GitHub가 OIDC JWT를 발급해주지 않습니다.

해결

워크플로 최상단(또는 job 단위)에 명시합니다.

name: deploy
on:
  push:
    branches: ["main"]

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    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-deploy
          aws-region: ap-northeast-2

추가로, 조직/리포 설정에서 “Workflow permissions”가 Read-only로 강제되어 있으면 job에서 올려도 제한될 수 있습니다(Organization 정책 확인).

케이스 2) OIDC Provider(iam oidc provider) 미생성 또는 ARN 불일치

증상

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

원인

AWS 계정에 GitHub OIDC Provider가 없거나, Role 신뢰 정책에서 참조한 Provider ARN이 실제와 다릅니다.

해결

1) OIDC Provider 존재 확인

aws iam list-open-id-connect-providers

출력에 token.actions.githubusercontent.com가 없으면 생성해야 합니다.

2) OIDC Provider 생성(예시)

AWS 콘솔(IAM → Identity providers)에서 생성하거나 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 공식 가이드를 기준으로 확인하세요.

3) Role 신뢰 정책의 Principal이 정확한지 확인

신뢰 정책에서 Federated정확한 Provider ARN을 가리켜야 합니다.

케이스 3) Trust Policy 조건의 sub가 실제와 불일치

증상

  • Not authorized to perform sts:AssumeRoleWithWebIdentity
  • 액션은 정상인데 STS에서 거절

원인

가장 흔한 실수입니다. sub(subject) 조건을 너무 타이트하게 걸었거나, 브랜치/환경/리포 이름이 달라 토큰의 sub와 매칭이 안 됩니다.

GitHub OIDC의 sub는 보통 아래 형태 중 하나입니다.

  • repo:ORG/REPO:ref:refs/heads/main
  • repo:ORG/REPO:pull_request
  • repo:ORG/REPO:environment:prod

해결

1) 우선 “정확한 sub”를 확인

가장 확실한 방법은 토큰 클레임을 출력하는 것입니다. configure-aws-credentials는 내부적으로 토큰을 사용하지만, 디버깅 단계에서 ACTIONS_ID_TOKEN_REQUEST_URL로 직접 받아 decode할 수 있습니다.

- name: Dump OIDC token claims (debug)
  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=$(python -c "import json,sys; print(json.load(sys.stdin)['value'])" <<< "$TOKEN_JSON")

    # JWT payload decode (no signature verification; for debugging only)
    python - <<'PY'
import os,base64,json
jwt=os.environ['TOKEN']
parts=jwt.split('.')
payload=parts[1] + '==='  # pad
print(json.dumps(json.loads(base64.urlsafe_b64decode(payload)), indent=2))
PY
  env:
    TOKEN: ${{ steps.get_token.outputs.token }}

위 예시는 그대로 동작하지 않을 수 있어(steps 출력 등) 실무에선 더 단순한 방식으로 구성합니다. 핵심은 JWT payload에서 sub, aud, iss를 확인하는 것입니다.

2) Trust Policy를 실제 sub에 맞게 수정

가장 무난한 템플릿은 아래입니다.

{
  "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:YOURORG/YOURREPO:ref:refs/heads/main"
        }
      }
    }
  ]
}

브랜치 패턴을 허용하려면:

"StringLike": {
  "token.actions.githubusercontent.com:sub": "repo:YOURORG/YOURREPO:ref:refs/heads/*"
}

환경 보호(Environments)를 쓰면 environment:prod 형태로 바뀌므로 그에 맞춰야 합니다.

케이스 4) aud(audience) 불일치

증상

  • InvalidIdentityToken: audience is invalid
  • 또는 assume-role 거절

원인

Trust Policy에서 token.actions.githubusercontent.com:audsts.amazonaws.com로 강제했는데, 실제 토큰 aud가 다르게 발급되었습니다. 흔한 원인은 토큰 발급 시 audience 파라미터를 다른 값으로 준 경우입니다.

해결

  • 가장 표준은 aud = sts.amazonaws.com
  • Trust Policy도 이에 맞추고, 액션도 기본값을 따릅니다.

만약 의도적으로 커스텀 audience를 쓴다면 Trust Policy의 aud도 동일하게 맞추세요.

케이스 5) Role ARN/계정/이름 오타 또는 Path 포함

증상

  • The requested role is not assumable 유사 메시지
  • AccessDenied와 함께 role 찾기 실패처럼 보임

원인

role-to-assume에 적은 ARN이 실제 Role과 다릅니다. 특히 Role path(/service/gha-deploy)가 있는 경우 콘솔에서 보이는 이름만 복사해 붙이면 실패할 수 있습니다.

해결

CLI로 Role ARN을 정확히 확인합니다.

aws iam get-role --role-name gha-deploy

Path가 있다면 --role-name은 이름만이지만, 반환되는 Arn을 워크플로에 그대로 사용하세요.

케이스 6) GitHub 리포/브랜치/포크 이벤트 차이로 sub가 달라짐

증상

  • main 브랜치 push는 되는데 PR에서만 실패
  • 내부 PR은 되는데 fork PR에서만 실패

원인

이벤트 타입에 따라 sub가 달라집니다.

  • PR: repo:ORG/REPO:pull_request
  • fork PR: 보안상 OIDC 토큰 발급/권한이 제한될 수 있음(특히 pull_request vs pull_request_target 차이)

해결

  • 배포는 원칙적으로 push(protected branch)나 workflow_dispatch로 제한
  • PR에서 AWS 접근이 필요하면 최소권한 + 별도 Role + 조건을 분리

예: PR에서는 read-only Role만 허용하고, main 배포 Role은 ref:refs/heads/main만 허용.

케이스 7) Trust Policy는 맞는데, 세션 조건/태그 조건 때문에 거절

증상

  • Not authorized to perform sts:AssumeRoleWithWebIdentity
  • 또는 AccessDenied인데 Trust Policy가 맞아 보임

원인

Role 신뢰 정책에 sts:RoleSessionName, sts:TagSession, aws:RequestTag/* 같은 조건을 걸어둔 경우, 액션이 보내는 값과 불일치하면 assume 자체가 거절됩니다.

해결

  • 우선 신뢰 정책을 단순화하여 assume 성공을 확인
  • 이후 필요한 조건을 단계적으로 추가

configure-aws-credentialsrole-session-name을 지정할 수 있습니다.

- uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: arn:aws:iam::123456789012:role/gha-deploy
    role-session-name: gha-${{ github.run_id }}
    aws-region: ap-northeast-2

빠른 체크리스트(10분 진단)

  1. 워크플로에 permissions: id-token: write 있는가?
  2. AWS 계정에 OIDC Provider가 존재하는가?
  3. Role Trust Policy의 Principal.Federated가 올바른 Provider ARN인가?
  4. Trust Policy의 audsts.amazonaws.com과 일치하는가?
  5. Trust Policy의 sub가 실제 이벤트(push/PR/env)와 일치하는가?
  6. 리포 이름 대소문자/ORG 이름이 정확한가?
  7. role-to-assume ARN이 정확한가?(path 포함 여부)
  8. 조직 정책에서 OIDC/권한이 제한되어 있지 않은가?
  9. PR/fork 이벤트에서 토큰 발급 제한을 밟고 있지 않은가?
  10. 추가 조건(세션명/태그)이 있다면 일단 제거하고 최소 구성으로 성공시키는가?

권장하는 “안전한” Trust Policy 예시 2종

1) main 브랜치 배포 전용(가장 흔함)

{
  "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:YOURORG/YOURREPO:ref:refs/heads/main"
        }
      }
    }
  ]
}

2) 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",
          "token.actions.githubusercontent.com:sub": "repo:YOURORG/YOURREPO:environment:prod"
        }
      }
    }
  ]
}

Environment를 쓰면 GitHub의 보호 규칙(승인, 대기 등)과 함께 배포 권한을 더 안전하게 묶을 수 있습니다.

마무리: “신뢰 정책”과 “토큰 클레임”을 맞추는 게임

OIDC assume-role 실패는 대부분 AWS가 받은 토큰 클레임IAM Role 신뢰 정책 조건이 1:1로 맞지 않아서 발생합니다. 따라서 해결의 핵심은 (1) 워크플로에서 토큰 발급이 가능한지 확인하고, (2) aud/sub를 실제 값 기준으로 맞추며, (3) 이벤트 타입(PR/branch/environment)에 따라 Role을 분리하는 것입니다.

배포 이후 단계에서 AccessDenied가 이어진다면, 그건 assume-role이 아니라 Role 권한 정책 문제일 가능성이 큽니다. OIDC 전반의 권한 설계/오류 패턴은 아래 글에서 더 확장된 케이스로 다뤘습니다.