Published on

GitHub Actions OIDC에서 AWS AssumeRoleAccessDenied 해결

Authors

서버리스 배포든 EKS 배포든, GitHub Actions에서 AWS 자격 증명을 안전하게 다루려면 OIDC가 사실상 표준입니다. 장기 액세스 키를 저장하지 않고도 sts:AssumeRoleWithWebIdentity로 임시 자격 증명을 발급받을 수 있기 때문입니다.

하지만 설정을 조금만 잘못해도 워크플로는 아래와 같은 메시지로 멈춥니다.

  • AssumeRoleAccessDenied
  • Not authorized to perform sts:AssumeRoleWithWebIdentity
  • The requested role session name is invalid
  • No OpenIDConnect provider found in your account

이 글은 “왜 거부됐는지”를 AWS IAM 관점에서 역추적하고, 가장 흔한 실수(특히 sub/aud 조건 불일치, OIDC Provider 설정 누락, trust policy의 Principal/Action 오류)를 재현 가능한 형태로 정리합니다.

관련해서 GitHub Actions 인증/권한 이슈를 더 넓게 보고 싶다면 GitHub Actions GITHUB_TOKEN 403 권한오류 해결도 함께 참고하면 좋습니다.

문제의 본질: OIDC는 “권한”이 아니라 “신뢰”에서 막힌다

AssumeRoleAccessDenied는 대개 해당 Role이 OIDC 토큰을 신뢰하지 않거나, 신뢰는 하더라도 조건(Condition)이 토큰 클레임과 맞지 않아 거부되는 경우가 많습니다.

즉, 아래 3요소가 모두 맞아야 합니다.

  1. AWS 계정에 GitHub OIDC Provider가 존재
  2. Role의 Trust Policy에서 해당 Provider를 Principal로 신뢰
  3. Trust Policy의 Condition이 GitHub가 발급한 토큰 클레임(sub, aud, iss)과 일치

여기서 2, 3번이 가장 자주 틀립니다.

1) GitHub Actions 워크플로의 필수 권한: id-token: write

OIDC 토큰을 발급받으려면 워크플로에 반드시 아래 권한이 있어야 합니다. 없으면 configure-aws-credentials가 토큰을 못 받아오고, 결과적으로 AssumeRole 단계에서 실패합니다.

permissions:
  id-token: write
  contents: read

그리고 AWS 인증을 수행하는 액션은 보통 아래 조합을 씁니다.

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

여기서 role-to-assume가 올바른지(계정/Role 이름)부터 확인하세요. 의외로 “다른 계정 ARN”을 넣고 AccessDenied로 시간을 날리는 경우가 많습니다.

2) AWS에 GitHub OIDC Provider가 없거나 Thumbprint가 잘못된 경우

에러 예시:

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

AWS IAM에 OIDC Provider가 있어야 합니다.

  • Provider URL: https://token.actions.githubusercontent.com
  • Audience: 보통 sts.amazonaws.com

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

ClientIDListsts.amazonaws.com가 포함되어 있는지 확인하세요.

주의할 점은 Thumbprint인데, 최근에는 AWS 콘솔에서 GitHub OIDC Provider를 생성하면 일반적으로 문제 없이 들어갑니다. 수동 구성했다면 Thumbprint가 잘못되어 토큰 검증 단계에서 실패할 수 있습니다.

3) Trust Policy의 Action이 AssumeRole로 되어 있는 실수

OIDC는 sts:AssumeRoleWithWebIdentity를 사용합니다. 그런데 trust policy에 sts:AssumeRole만 있으면 거부됩니다.

잘못된 예:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

올바른 예(최소 구성):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity"
    }
  ]
}

4) 가장 흔한 원인: sub 조건이 GitHub 토큰과 불일치

GitHub OIDC 토큰의 핵심 클레임은 sub입니다. 일반적으로 아래 형태로 내려옵니다.

  • 브랜치 기준: repo:OWNER/REPO:ref:refs/heads/main
  • 태그 기준: repo:OWNER/REPO:ref:refs/tags/v1.2.3
  • PR 기준: repo:OWNER/REPO:pull_request
  • Environment 사용 시: repo:OWNER/REPO:environment:prod

Trust Policy에서 sub를 너무 빡빡하게 걸어두고 실제 실행 컨텍스트가 다르면 바로 AccessDenied가 납니다.

예를 들어, 아래처럼 main만 허용했는데 태그 릴리즈 워크플로에서 실행하면 실패합니다.

"Condition": {
  "StringEquals": {
    "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
    "token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:ref:refs/heads/main"
  }
}

해결 전략 A: 브랜치/태그를 모두 고려해 패턴 매칭

StringLike로 repo 범위를 고정하고 ref 패턴을 유연하게 허용합니다.

{
  "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",
            "repo:my-org/my-repo:ref:refs/tags/*"
          ]
        }
      }
    }
  ]
}

해결 전략 B: GitHub Environment를 쓰는 경우 sub가 바뀐다

environment 보호 규칙을 쓰면 subrepo:...:environment:prod 형태가 됩니다. 이때 브랜치 패턴만 허용하면 실패합니다.

"StringLike": {
  "token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:environment:prod"
}

브랜치 기반 배포와 환경 기반 배포를 섞는다면 sub 허용 규칙을 분리하거나, Role을 환경별로 나누는 편이 운영상 안전합니다.

5) aud 조건 누락/불일치: sts.amazonaws.com이 아닌 값을 기대하는 경우

GitHub OIDC에서 AWS 액션은 기본적으로 audience를 sts.amazonaws.com으로 사용합니다. Trust Policy에 aud 조건을 걸어두는 것은 권장되는 보안 패턴이지만, 값이 다르면 바로 실패합니다.

권장 설정:

"StringEquals": {
  "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
}

만약 액션 설정에서 audience를 커스텀했다면 워크플로 쪽도 함께 확인해야 합니다.

with:
  audience: sts.amazonaws.com

6) Role ARN은 맞는데도 AccessDenied: “권한 정책”이 아니라 “세션 정책/Permission boundary”를 의심

OIDC AssumeRole이 성공하려면 Trust Policy만 맞으면 됩니다. 그런데 현실에서는 다음이 추가로 발목을 잡습니다.

  • Permission Boundary가 붙어 있고, 그 경계가 sts:AssumeRoleWithWebIdentity 이후 작업 권한을 제한
  • 조직 SCP(Service Control Policy)로 특정 리전/서비스가 차단
  • Role 자체에 정책이 없어서 AssumeRole은 되지만 이후 단계에서 AccessDenied가 연쇄 발생

이 경우 증상은 “AssumeRoleAccessDenied”가 아니라, AssumeRole은 성공하고 다음 AWS API 호출에서 AccessDenied가 납니다. 로그를 구분해서 보세요.

예:

  • sts:AssumeRoleWithWebIdentity 성공
  • ecr:GetAuthorizationToken 또는 eks:DescribeCluster 등에서 AccessDenied

7) 디버깅: GitHub OIDC 토큰 클레임을 눈으로 확인하는 방법

가장 빠른 방법은 실제 워크플로에서 OIDC 토큰을 받아서(민감정보 주의) 클레임을 확인하는 것입니다.

아래는 토큰을 직접 출력하지 않고, 페이로드만 디코딩해 필요한 클레임을 확인하는 예시입니다.

- name: Dump OIDC claims (safe)
  shell: bash
  run: |
    set -euo pipefail
    TOKEN_URL="${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=sts.amazonaws.com"
    JWT=$(curl -sSf -H "Authorization: Bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" "$TOKEN_URL" | jq -r '.value')

    # JWT payload(base64url) decode
    PAYLOAD=$(echo "$JWT" | cut -d'.' -f2 | tr '_-' '/+' | awk '{print $0"=="}' | base64 -d 2>/dev/null || true)
    echo "$PAYLOAD" | jq '{iss, aud, sub, repository, ref, sha, actor, workflow, job_workflow_ref}'

여기서 출력되는 sub를 그대로 Trust Policy에 맞추면 됩니다. 많은 경우 “내가 생각한 sub”와 “실제 sub”가 다릅니다.

8) 안전한 기준 설정: 최소 권한 Trust Policy 템플릿

운영에서 추천하는 형태는 다음입니다.

  • audsts.amazonaws.com로 고정
  • sub는 반드시 repo:OWNER/REPO: prefix로 제한
  • 브랜치/태그/환경 중 실제 사용하는 컨텍스트만 허용

예시(메인 브랜치 배포만 허용):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "GitHubActionsOidc",
      "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:my-org/my-repo:ref:refs/heads/main"
        }
      }
    }
  ]
}

예시(태그 릴리즈만 허용):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "GitHubActionsOidcTagRelease",
      "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/tags/*"
        }
      }
    }
  ]
}

9) 체크리스트: 3분 안에 원인 좁히기

  1. 워크플로에 permissions: id-token: write가 있는가
  2. AWS 계정에 OIDC Provider가 존재하는가(token.actions.githubusercontent.com)
  3. Role trust policy의 Actionsts:AssumeRoleWithWebIdentity인가
  4. trust policy의 Principal이 올바른 Provider ARN인가
  5. audsts.amazonaws.com으로 일치하는가
  6. sub가 실제 실행 컨텍스트(브랜치/태그/환경)와 일치하는가
  7. AssumeRole 이후 단계에서 권한이 터진 것인지(예: ECR/EKS) 로그를 구분했는가

마무리

AssumeRoleAccessDenied는 대부분 “IAM 권한 정책이 부족해서”가 아니라, OIDC 토큰을 신뢰하는 규칙이 실제 토큰 클레임과 불일치해서 발생합니다. 특히 sub 조건은 워크플로 트리거(브랜치, 태그, PR, environment)에 따라 형태가 달라지므로, 먼저 실제 클레임을 확인하고 그에 맞춰 trust policy를 조정하는 것이 가장 빠릅니다.

OIDC 기반 설정을 더 깊게 다룬 글이 필요하다면 GitHub Actions OIDC로 AWS STS AssumeRole 실패 해결도 함께 보면, 에러 메시지별로 원인을 더 촘촘히 매핑할 수 있습니다.