Published on

GitHub Actions OIDC로 AWS STS AssumeRole 실패 해결

Authors

서론

GitHub Actions에서 장기 AWS Access Key를 없애기 위해 OIDC(OpenID Connect)로 STS AssumeRoleWithWebIdentity를 사용하는 패턴이 빠르게 표준이 됐습니다. 하지만 막상 적용하면 Not authorized to perform sts:AssumeRoleWithWebIdentity, InvalidIdentityToken, AccessDenied 같은 에러로 배포 파이프라인이 멈추는 일이 흔합니다. 특히 “어제까지 되던 게 오늘 갑자기 실패”하거나, 특정 브랜치/PR에서만 실패하는 경우는 대개 OIDC 토큰 클레임(subject/audience)과 IAM Role 신뢰 정책(Trust Policy)의 조건 불일치에서 시작합니다.

이 글은 GitHub Actions OIDC로 AWS STS AssumeRole이 실패할 때의 원인을 유형별로 분류하고, 가장 많이 틀리는 신뢰 정책/워크플로 설정을 교정하는 방법을 제공합니다. 장애를 “한 번에” 끝내는 관점에서, 로그에서 무엇을 봐야 하고 어떤 값을 비교해야 하는지에 집중합니다. (장애 진단 흐름은 OAuth redirect_uri mismatch 즉시 해결 체크리스트처럼 불일치 지점을 빠르게 찾는 방식을 참고하면 도움이 됩니다.)

OIDC 기반 AssumeRole 동작 개요(실패 지점 지도)

GitHub Actions OIDC 흐름은 간단히 아래 순서로 동작합니다.

  1. 워크플로가 GitHub OIDC Provider에서 JWT 토큰을 발급받음
  2. AWS STS에 AssumeRoleWithWebIdentity 호출(토큰 + Role ARN)
  3. STS가 IAM Role의 Trust Policy를 평가
  4. 조건을 만족하면 임시 자격증명(AccessKeyId/SecretAccessKey/SessionToken) 발급

실패는 주로 2~3 단계에서 발생합니다.

  • 2단계 실패: 토큰 자체 문제(발급 불가, audience 불일치, 시간/서명 문제)
  • 3단계 실패: IAM Role 신뢰 정책 조건 불일치(sub, aud, iss) 또는 권한 부족

가장 흔한 에러 메시지별 원인 분류

1) AccessDenied: Not authorized to perform sts:AssumeRoleWithWebIdentity

대부분 Role Trust Policy가 토큰 클레임을 허용하지 않거나, GitHub OIDC Provider ARN/조건이 잘못된 경우입니다.

체크 포인트:

  • IAM Role의 Trust Policy에서 Principal이 Federated로 GitHub OIDC Provider를 가리키는가
  • Conditiontoken.actions.githubusercontent.com:sub, aud가 실제 토큰과 일치하는가
  • sub를 너무 빡빡하게 걸어 브랜치/태그/환경이 바뀌면 실패하는 구조는 아닌가

2) InvalidIdentityToken: audience is invalid

워크플로에서 요청한 audience와 Trust Policy에서 검사하는 aud가 불일치할 때 발생합니다.

  • aws-actions/configure-aws-credentials는 기본 audience가 sts.amazonaws.com입니다.
  • Trust Policy에 StringEqualsaud를 검사한다면 반드시 동일해야 합니다.

3) No OpenIDConnect provider found in your account

AWS 계정에 GitHub OIDC Provider가 없거나, Provider ARN이 다른 계정을 가리키는 경우입니다.

  • Provider URL: https://token.actions.githubusercontent.com
  • Client ID(=audience): 보통 sts.amazonaws.com

4) PR에서만 실패 / fork에서만 실패

보안상 GitHub는 fork PR에서 OIDC 토큰 발급이 제한되거나, sub 클레임이 기대와 다르게 나옵니다. 또한 pull_request 이벤트와 push 이벤트의 sub 패턴이 다릅니다.

필수 전제: GitHub Actions 권한 설정(id-token: write)

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

permissions:
  id-token: write
  contents: read

이게 빠지면 configure-aws-credentials가 내부적으로 OIDC 토큰을 못 받아 AssumeRole이 실패합니다.

정답에 가까운 구성: IAM OIDC Provider + Role Trust Policy

1) AWS에 GitHub OIDC Provider 생성

이미 있다면 스킵해도 됩니다. 콘솔에서 IAM → Identity providers → Add provider에서:

  • Provider type: OpenID Connect
  • Provider URL: https://token.actions.githubusercontent.com
  • Audience: sts.amazonaws.com

Terraform 예시:

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

  client_id_list = [
    "sts.amazonaws.com"
  ]

  thumbprint_list = [
    "6938fd4d98bab03faadb97b34396831e3780aea1"
  ]
}

> 참고: thumbprint는 AWS 문서/가이드에 따라 변경될 수 있으니, 조직 표준에 맞게 관리하세요.

2) IAM Role Trust Policy(핵심)

가장 많이 쓰는 패턴은 sub를 리포지토리 + 브랜치(또는 환경)로 제한하는 것입니다.

브랜치(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:OWNER/REPO:ref:refs/heads/main"
        }
      }
    }
  ]
}

GitHub Environments(예: prod) 기반으로 허용하는 예시

Environment를 쓰면 브랜치/태그 변화에 덜 흔들리고, 승인(Required reviewers)도 붙일 수 있어 운영 배포에 유리합니다.

{
  "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:OWNER/REPO:environment:prod"
        }
      }
    }
  ]
}

여기서 포인트는:

  • aud는 보통 sts.amazonaws.com으로 고정
  • sub는 너무 광범위하게 풀지 말되, 이벤트별 패턴 차이(브랜치/태그/환경)를 고려해 설계

GitHub Actions 워크플로 예시(검증 가능한 최소 구성)

아래 예시는 OIDC로 Role을 Assume한 뒤 sts get-caller-identity로 실제로 어떤 주체가 되었는지 검증합니다.

name: deploy

on:
  push:
    branches: [ "main" ]

permissions:
  id-token: write
  contents: read

jobs:
  aws-auth-check:
    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

이 단계에서 실패한다면, 애플리케이션 배포 로직 이전에 인증/신뢰 정책 문제로 범위를 좁힐 수 있습니다.

“내 토큰의 sub가 뭔지” 확인하기: 디버그 방법

신뢰 정책의 sub 조건이 맞는지 확인하려면, 실제 발급된 OIDC 토큰의 클레임을 봐야 합니다. GitHub는 OIDC 토큰을 직접 출력하지 않는 것을 권장하지만, 문제 해결을 위해 일시적으로 토큰의 payload만 디코딩해 확인하는 방식은 유용합니다(토큰 자체를 로그에 남기지 않도록 주의).

actions/github-script로 OIDC 요청 URL을 얻는 등의 방식도 있지만, 가장 현실적인 접근은 다음입니다.

  1. 먼저 Trust Policy에서 sub 조건을 임시로 완화(예: repo:OWNER/REPO:*)
  2. AssumeRole 성공 후 CloudTrail에서 AssumeRoleWithWebIdentity 이벤트를 확인
  3. 이벤트의 userIdentity/sessionContext/requestParameters에서 어떤 조건으로 매칭됐는지 확인
  4. 다시 sub를 최소 권한으로 조임

CloudTrail 기반으로 보는 이유는, 파이프라인 로그에 민감정보를 남기지 않으면서도 실제 STS 요청이 어떤 형태였는지 확인할 수 있기 때문입니다. 운영 장애를 빠르게 줄이는 방식은 systemd 서비스가 반복 재시작될 때 원인 추적법처럼 “관측 가능한 로그 소스(CloudTrail)로 원인을 좁히는” 접근과 유사합니다.

실전에서 많이 틀리는 7가지 포인트

1) Trust Policy에서 StringEqualssub를 고정해버림

sub는 이벤트/컨텍스트에 따라 바뀝니다.

  • push는 보통 ref:refs/heads/<branch>
  • tagref:refs/tags/<tag>
  • environment를 쓰면 environment:<name>

브랜치 패턴을 허용하려면 StringLike + 와일드카드를 고려하세요.

2) 리포지토리 대소문자/OWNER 오타

repo:OWNER/REPO:...는 문자열 매칭입니다. 조직명 변경, 리포 rename, 대소문자 차이로도 매칭 실패가 납니다.

3) Audience 불일치

  • 워크플로에서 audience를 커스텀으로 바꾸면 Trust Policy도 함께 바뀌어야 합니다.
  • 기본값을 유지한다면 Trust Policy는 sts.amazonaws.com으로 두는 게 안전합니다.

4) 다른 AWS 계정의 OIDC Provider를 참조

멀티계정 환경에서 Role은 계정 A에 있는데 OIDC Provider는 계정 B에 만들어둔 경우가 있습니다. Trust Policy의 Principal.Federated ARN이 정확한지 확인하세요.

5) Role 권한 정책(Permission Policy)과 혼동

OIDC 실패는 대부분 Trust Policy 문제입니다. 하지만 AssumeRole이 성공한 뒤에도 AWS API 호출이 실패하면 그때는 Permission Policy(예: S3, ECR, ECS, CloudFormation 권한)를 봐야 합니다.

6) PR(fork)에서 배포를 시도

fork PR은 의도적으로 제한되는 경우가 많습니다. 배포 워크플로는 push/workflow_dispatch/environment 승인 기반으로 분리하는 것을 권장합니다.

7) 세션 이름/태그 정책으로 거부

조직에서 SCP나 IAM 정책으로 sts:TagSession, sts:RoleSessionName 제한을 걸어둔 경우도 있습니다. 이 경우 AccessDenied가 다르게 보일 수 있으니, Organizations SCP까지 포함해 확인해야 합니다.

권장하는 최소 권한 설계(보안과 운영의 균형)

  • Trust Policy는 리포지토리 단위로 고정하고, 배포 단위는 environment:prod 같은 형태로 제한
  • audsts.amazonaws.com으로 고정
  • 브랜치 기반 제한이 필요하면 main 정도만 허용(릴리즈 브랜치 전략이면 패턴 허용)
  • 배포 워크플로는 workflow_dispatch + environment 승인으로 운영자 통제를 강화

이렇게 하면 “브랜치 이름 바뀌어서 배포 깨짐” 같은 운영 이슈가 줄고, 공격 표면도 작아집니다.

트러블슈팅 체크리스트(10분 컷)

  1. 워크플로에 permissions: id-token: write가 있는가
  2. AWS 계정에 OIDC Provider(token.actions.githubusercontent.com)가 존재하는가
  3. Role Trust Policy의 Principal.Federated가 올바른 Provider ARN인가
  4. Trust Policy에서 aud == sts.amazonaws.com 조건이 맞는가
  5. Trust Policy의 sub 패턴이 현재 이벤트(push/pr/tag/environment)에 맞는가
  6. (성공 후 API 실패 시) Role Permission Policy에 필요한 AWS 권한이 있는가
  7. Organizations SCP/Permission Boundary로 STS가 막히지 않았는가

마무리

GitHub Actions OIDC로 STS AssumeRole이 실패할 때는 “AWS 자격증명이 없어서”가 아니라, 거의 항상 OIDC 토큰 클레임과 IAM Trust Policy 조건의 불일치 문제입니다. 가장 빠른 해결책은 (1) aud를 표준값으로 고정하고, (2) sub를 이벤트에 맞게 설계하며, (3) CloudTrail로 실제 STS 호출을 관측해 조건을 조여가는 방식입니다.

배포 파이프라인이 멈출 때 원인 분류/관측을 먼저 정리하면 해결 속도가 압도적으로 빨라집니다. 비슷한 결의 “진단 루틴”은 Jenkins 빌드가 멈출 때 - 에이전트 오프라인 진단에서도 그대로 응용할 수 있습니다.