Published on

GitHub Actions OIDC AssumeRole 실패 해결 가이드

Authors

서버리스나 컨테이너 배포 파이프라인에서 장기 액세스 키를 없애려면 GitHub Actions OIDC 연동이 사실상 표준입니다. 그런데 설정이 조금만 어긋나도 AssumeRole 단계에서 애매한 에러가 나고, 로그만 보고는 어디가 문제인지 감이 안 오는 경우가 많습니다.

이 글에서는 GitHub Actions에서 OIDC로 AWS AssumeRole이 실패하는 케이스를 증상별로 분류하고, 가장 빨리 원인 좁히는 디버깅 루틴Trust Policy/Workflow 수정 예시를 함께 제공합니다.

관련해서 AssumeRoleAccessDenied를 더 깊게 다룬 글도 함께 참고하면 좋습니다: GitHub Actions OIDC에서 AWS AssumeRoleAccessDenied 해결

실패를 빠르게 분류하는 체크리스트

OIDC AssumeRole 실패는 크게 다섯 갈래로 나뉩니다.

  1. GitHub 쪽 OIDC 토큰을 못 받음 (권한/설정 문제)
  2. AWS IAM Role Trust Policy의 조건이 안 맞음 (sub, aud, iss)
  3. Role ARN, 계정, 리전, STS 엔드포인트 등 기본 파라미터 문제
  4. STS AssumeRole은 성공했는데 이후 AWS API 호출이 실패 (권한 정책 문제)
  5. 간헐적/환경 의존 문제 (세션 길이, 시간 동기, 캐시된 자격증명)

아래부터는 실제로 많이 보는 에러 메시지 패턴과 해결법을 매칭합니다.

1) Could not load credentials 또는 id-token 관련 에러

대표 증상

  • aws-actions/configure-aws-credentials 실행 직후
  • 로그에 No OpenIDConnect provider found 또는 Unable to get OIDC token 뉘앙스
  • 혹은 그냥 Could not load credentials from any providers

원인

가장 흔한 원인은 Workflow 권한에 id-token: write가 없는 경우입니다. GitHub는 OIDC 토큰 발급을 id-token 권한으로 제어합니다.

해결

Workflow 최상단 또는 해당 Job에 다음을 추가합니다.

permissions:
  id-token: write
  contents: read

또한 pull_request 이벤트에서 포크 PR이면 보안상 OIDC 토큰이 제한될 수 있습니다. 이 경우는 pull_request_target로 바꾸거나, 포크 PR에서는 배포 Job을 건너뛰는 전략이 필요합니다.

2) Not authorized to perform sts:AssumeRoleWithWebIdentity

대표 증상

  • AccessDenied: Not authorized to perform sts:AssumeRoleWithWebIdentity
  • 혹은 AssumeRoleWithWebIdentity가 명시적으로 거부됨

원인

대부분 IAM Role의 Trust Policy가 GitHub OIDC 토큰의 클레임과 일치하지 않아서 발생합니다.

특히 다음이 자주 틀립니다.

  • sub 조건에 repo:owner/name:ref:refs/heads/main처럼 너무 구체적으로 박아두고 브랜치/태그/환경이 달라짐
  • aud 조건이 sts.amazonaws.com이 아닌 값으로 들어감
  • OIDC Provider ARN이 다른 계정/리전의 것으로 연결됨

해결: 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:my-org/my-repo:*"
        }
      }
    }
  ]
}

sub를 브랜치로 제한하고 싶다면

main 브랜치만 허용하려면 다음처럼 제한합니다.

  • repo:my-org/my-repo:ref:refs/heads/main

태그 릴리스만 허용하려면:

  • repo:my-org/my-repo:ref:refs/tags/*

환경 보호 규칙을 쓰는 경우에는 environment 클레임을 활용하는 패턴도 있지만, 먼저 sub부터 정확히 맞추는 게 우선입니다.

3) InvalidIdentityToken 또는 No OpenIDConnect provider found

대표 증상

  • InvalidIdentityToken: No OpenIDConnect provider found in your account for https://token.actions.githubusercontent.com
  • 혹은 OIDC Provider가 없다는 메시지

원인

AWS 계정에 GitHub OIDC Provider가 생성되어 있지 않거나, Thumbprint/URL이 잘못되었거나, 다른 계정의 Role을 가리키고 있을 수 있습니다.

해결

IAM에서 OIDC Provider가 다음 URL로 등록되어 있는지 확인합니다.

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

그리고 Role Trust Policy의 Principal.Federated가 정확히 같은 계정의 OIDC Provider ARN을 가리키는지 확인합니다.

4) The security token included in the request is invalid

대표 증상

  • AssumeRole은 된 것처럼 보이는데 이후 AWS API 호출에서 실패
  • 또는 STS 단계부터 실패하는 경우도 있음

원인

  • 이미 설정된 다른 자격증명(예: AWS_ACCESS_KEY_ID)이 섞여 들어와 충돌
  • 캐시된 자격증명 사용
  • 리전/STS 엔드포인트 혼선

해결

  1. Workflow에서 장기 키를 완전히 제거합니다. secrets.AWS_ACCESS_KEY_ID 같은 변수가 남아 있으면 OIDC 대신 그걸 집어드는 액션/SDK가 생깁니다.

  2. configure-aws-credentials 이후에 aws sts get-caller-identity로 즉시 검증합니다.

- 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
  1. 셀프 호스티드 러너라면 이전 Job의 환경변수/자격증명이 남지 않도록 러너 격리(에페메럴) 또는 워크스페이스 정리도 점검합니다.

5) AccessDenied는 뜨는데 AssumeRole은 성공한 경우

대표 증상

  • aws sts get-caller-identity는 성공
  • 이후 ecr:PutImage, eks:DescribeCluster, s3:PutObject 같은 실제 작업에서 AccessDenied

원인

이건 OIDC 문제가 아니라 Role에 붙은 IAM Permission Policy 문제입니다. Trust Policy는 "누가 Role을 쓸 수 있나"이고, Permission Policy는 "Role이 무엇을 할 수 있나"입니다.

해결

aws sts get-caller-identity로 Role ARN이 기대한 값인지 확인한 뒤, 필요한 권한을 최소 범위로 추가합니다.

예를 들어 ECR 푸시가 목적이면 대략 다음 권한들이 필요합니다(환경에 따라 조정).

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

EKS 배포가 섞이면 eks:DescribeCluster와 함께, 쿠버네티스 RBAC까지 이어질 수 있습니다. 배포가 진행 중 멈추는 류의 디버깅은 이 글도 도움이 됩니다: GitHub Actions Kubernetes 배포 stuck in Progress 디버깅

6) sub 클레임이 예상과 다를 때: 토큰 클레임을 직접 확인하기

가장 답답한 지점이 "내가 Trust Policy에 넣은 sub가 실제 토큰의 sub와 다르다"입니다. 이럴 때는 OIDC 토큰을 받아서 클레임을 확인하면 한 번에 끝납니다.

GitHub Actions에서는 OIDC 토큰을 요청할 수 있습니다. 아래 예시는 토큰을 받아 페이로드를 디코딩해 sub, aud, iss 등을 확인하는 방법입니다.

주의: 토큰을 로그에 그대로 찍으면 보안 사고입니다. 아래는 디버깅 시 일시적으로 사용하고, 확인 후 제거하세요.

- name: Dump OIDC claims (temporary)
  shell: bash
  run: |
    set -euo pipefail

    # GitHub가 제공하는 OIDC 요청 URL/토큰으로 JWT를 발급받는다
    jwt=$(curl -sS -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
      "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=sts.amazonaws.com" \
      | python -c "import sys,json; print(json.load(sys.stdin)['value'])")

    # JWT의 payload 부분만 base64url 디코딩
    payload=$(python - <<'PY'
import os,sys,base64,json
jwt=os.environ['JWT']
parts=jwt.split('.')
pad='='*(-len(parts[1])%4)
data=base64.urlsafe_b64decode(parts[1]+pad)
print(data.decode())
PY
    )

    echo "$payload" | python -m json.tool
  env:
    JWT: ${{ steps.getjwt.outputs.jwt }}

위 예시는 러프한 형태라 그대로 쓰기보다는, 실제로는 curl 결과를 step output으로 넘기거나 한 스텝에서 처리하도록 단순화하세요. 핵심은 audience=sts.amazonaws.com로 요청하고, payload에서 sub를 확인해 Trust Policy의 StringLike 패턴을 맞추는 것입니다.

7) 실전에서 가장 안전한 Workflow 템플릿

OIDC AssumeRole 실패를 줄이려면, 다음 원칙을 지키는 템플릿이 좋습니다.

  • Job permissionsid-token: write 명시
  • configure-aws-credentials 직후 get-caller-identity로 검증
  • 리전 명시
  • 불필요한 장기 키 환경변수 제거
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

      - name: Deploy (example)
        run: |
          echo "do deploy..."

8) 자주 놓치는 디테일들

aud 불일치

Trust Policy에서 audsts.amazonaws.com로 강제했는데, 토큰 요청 시 audience를 다른 값으로 받으면 무조건 실패합니다. configure-aws-credentials는 기본적으로 STS audience를 사용하지만, 커스텀 구현을 섞었다면 확인하세요.

브랜치/태그/PR 이벤트에 따른 sub 변화

sub는 이벤트 유형과 ref에 따라 형태가 달라집니다. 예를 들어 태그 푸시 릴리스로 바꾸면 refs/heads/main이 아니라 refs/tags/v1.2.3이 됩니다. 배포 트리거를 바꾼 직후 AssumeRole이 깨지는 이유 1순위입니다.

조직/리포 이름 변경

리포를 rename하면 subrepo:org/repo가 바뀝니다. Trust Policy가 하드코딩이면 즉시 실패합니다.

세션 시간(role-duration-seconds)과 장시간 Job

빌드/배포가 길면 세션이 만료되기도 합니다. 이 경우 configure-aws-credentialsrole-duration-seconds를 늘리되, AWS Role의 MaxSessionDuration도 함께 올려야 합니다.

- uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: arn:aws:iam::123456789012:role/gha-deploy-role
    aws-region: ap-northeast-2
    role-duration-seconds: 3600

결론: “Trust Policy부터 좁히지 말고, 먼저 맞춘 뒤 조인다”

OIDC AssumeRole 실패는 대부분 Trust Policy의 조건 불일치에서 시작합니다. 실전에서는 다음 순서가 가장 빠릅니다.

  1. Workflow에 permissions: id-token: write 확인
  2. configure-aws-credentials 직후 aws sts get-caller-identity로 성공 여부 확인
  3. 실패하면 Trust Policy에서 audsub를 가장 완화된 형태로 맞춘 뒤
  4. 브랜치/태그/환경에 맞춰 sub를 점진적으로 조이기

배포 파이프라인은 한 번 안정화해두면 이후 장애 비용이 크게 줄어듭니다. 특히 EKS나 ALB 같은 구성요소가 붙으면 원인 추적 범위가 급격히 넓어지므로, 인증 단계(OIDC AssumeRole)를 가장 먼저 "확실하게" 고정시키는 것이 좋습니다.

추가로 AWS 인프라 쪽에서 502/504 같은 증상까지 같이 겪고 있다면, 인증과 무관한 런타임 장애를 분리해서 보려는 관점에서 이 체크리스트도 도움이 됩니다: AWS ALB 502·504 난사 - 원인별 해결 체크리스트