Published on

GitHub Actions OIDC로 AWS 배포 권한 오류 해결

Authors

서버리스든 EKS든, GitHub Actions에서 AWS로 배포할 때 가장 많이 겪는 장애는 결국 OIDC 기반 Role Assume 실패입니다. 기존처럼 액세스 키를 저장하지 않고도 배포할 수 있다는 장점 때문에 aws-actions/configure-aws-credentials + OIDC 조합이 표준이 됐지만, 설정이 조금만 어긋나도 AccessDeniedInvalidIdentityToken 같은 모호한 오류가 터집니다.

이 글은 “왜 실패하는지”를 AWS IAM(신뢰 정책) ↔ GitHub OIDC 토큰(클레임) ↔ Actions 워크플로 권한의 3축으로 나눠, 재현 가능한 방식으로 해결하는 방법을 정리합니다.

> 참고: OIDC/STS 계열의 403은 EKS에서도 동일한 원리로 발생합니다. STS 서명/토큰 계열 이슈를 더 넓게 보고 싶다면 EKS Pod에서 STS 403 SignatureDoesNotMatch 해결도 함께 보면 원인 분리가 빨라집니다.

1) 증상별로 보는 대표 오류 메시지

GitHub Actions OIDC로 AWS에 접근할 때 오류는 대개 아래 중 하나로 수렴합니다.

1. Not authorized to perform sts:AssumeRoleWithWebIdentity

  • 의미: 대상 Role의 Trust policy가 OIDC 토큰을 신뢰하지 않거나, 조건(Condition)이 불일치.
  • 포인트: “권한 정책”이 아니라 “신뢰 정책” 문제인 경우가 많습니다.

2. InvalidIdentityToken: No OpenIDConnect provider found in your account

  • 의미: IAM에 GitHub OIDC Provider(token.actions.githubusercontent.com)가 없거나, ARN/URL이 틀림.

3. AccessDenied: Access denied for operation ... (예: ECR, S3, CloudFormation)

  • 의미: Role Assume은 성공했지만, Permission policy(권한 정책) 가 부족.

4. Credentials could not be loaded / NoCredentialProviders

  • 의미: Actions에서 OIDC 토큰을 발급받지 못했거나(permissions: id-token: write 누락), configure 단계가 실패.

2) OIDC 기반 배포의 동작 흐름(문제 지점 지도)

OIDC 배포는 대략 다음 순서로 진행됩니다.

  1. GitHub Actions가 OIDC 토큰(JWT)을 발급 받음
  2. aws-actions/configure-aws-credentials가 해당 토큰으로 sts:AssumeRoleWithWebIdentity 호출
  3. STS가 Role의 Trust policy 조건을 검증
  4. 임시 자격 증명 발급 → 이후 AWS API 호출(ECR/S3/EKS/CloudFormation 등)

따라서 문제는 1) 토큰 발급, 2) Trust policy, 3) Permission policy, 4) 리전/리소스 ARN 불일치 중 하나입니다.

3) GitHub Actions 워크플로 설정: 가장 먼저 확인할 2줄

OIDC 토큰 발급 자체가 안 되면 나머지는 전부 실패합니다. 워크플로에 아래가 반드시 있어야 합니다.

  • permissions: id-token: write
  • permissions: contents: read (대부분 필요)

예시: 최소 동작 워크플로

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

체크포인트

  • aws sts get-caller-identity가 성공하면 OIDC Assume은 성공입니다.
  • 이후 단계에서 실패하면 권한 정책 또는 리소스/리전 문제로 좁혀집니다.

4) IAM OIDC Provider 생성: URL/Thumbprint보다 중요한 것

AWS 계정에 GitHub OIDC Provider가 있어야 합니다.

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

CLI로 확인:

aws iam list-open-id-connect-providers
aws iam get-open-id-connect-provider \
  --open-id-connect-provider-arn arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com

자주 하는 실수

  • Provider URL에 https://를 빼거나, 끝에 /를 붙여 ARN이 다르게 생성됨
  • 다른 계정(또는 다른 조직)에서 만든 Role ARN을 참조

5) Trust policy(신뢰 정책) 설계: sub 조건이 90%의 원인

OIDC Assume 실패의 대부분은 Trust policy의 조건이 GitHub 토큰의 클레임과 맞지 않아서입니다.

GitHub OIDC 토큰에는 대표적으로 아래 클레임이 들어갑니다.

  • iss: https://token.actions.githubusercontent.com
  • aud: sts.amazonaws.com
  • sub: 저장소/브랜치/환경에 따라 달라지는 식별자

권장 Trust policy 예시(브랜치 제한)

{
  "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"
        }
      }
    }
  ]
}

sub가 달라지는 케이스(환경/태그/PR)

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

따라서 실제 운영에서는 다음 전략 중 하나를 선택합니다.

  1. 엄격하게 제한: main 브랜치만 허용(가장 안전)
  2. 태그 릴리즈 허용: refs/tags/*
  3. GitHub Environment 기반: environment:prod만 허용

예: 태그도 허용

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

6) Permission policy(권한 정책): Assume 성공 후 AccessDenied를 끝내는 법

get-caller-identity가 되는데 배포가 실패한다면, 이제는 Role에 붙은 권한 정책을 봐야 합니다.

예를 들어 ECR 푸시를 한다면 최소 권한은 다음이 필요합니다.

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

S3 업로드가 필요하면 버킷/프리픽스 단위로 리소스를 제한하세요.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "UploadArtifacts",
      "Effect": "Allow",
      "Action": ["s3:PutObject", "s3:AbortMultipartUpload"],
      "Resource": "arn:aws:s3:::my-bucket/deploy/*"
    },
    {
      "Sid": "ListBucket",
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::my-bucket",
      "Condition": {
        "StringLike": {"s3:prefix": "deploy/*"}
      }
    }
  ]
}

디버깅 팁: CloudTrail로 “거부된 액션”을 확정

  • CloudTrail Event history에서 errorCode=AccessDenied 이벤트를 찾으면
    • 어떤 Principal(Role session)이
    • 어떤 Action을
    • 어떤 Resource에
    • 어떤 조건 때문에 거부됐는지 를 바로 확인할 수 있습니다.

7) 실전에서 자주 터지는 함정 7가지

1) permissions 누락으로 OIDC 토큰 자체가 없음

  • 증상: NoCredentialProviders, Could not load credentials
  • 해결: 워크플로 최상단에 permissions: id-token: write

2) Trust policy에서 aud 조건 누락/오타

  • 증상: AssumeRoleWithWebIdentity 거부
  • 해결: token.actions.githubusercontent.com:aud = sts.amazonaws.com

3) sub가 환경/태그/브랜치와 불일치

  • 증상: main에서는 되는데 tag 릴리즈에서만 실패(또는 반대)
  • 해결: 실제 배포 트리거에 맞춰 sub 패턴을 설계

4) Organization/Repository 이름 대소문자, 포크 PR 등 이벤트 차이

  • 증상: 내부 PR은 되는데 fork PR은 실패
  • 설명: 보안상 fork PR에는 OIDC 토큰 권한이 제한될 수 있음
  • 해결: 배포는 push/workflow_dispatch/protected environment로 제한

5) Role ARN 계정이 다름(멀티 계정 운영에서 흔함)

  • 증상: 존재하는 Role인데도 Assume 실패/권한 꼬임
  • 해결: role-to-assume의 account id, provider arn을 동일 계정으로 맞춤

6) 세션 정책/Permission boundary/SCP(Organizations)로 인한 차단

  • 증상: 정책상 허용인데도 AccessDenied
  • 해결:
    • Role에 Permission boundary가 있는지
    • AWS Organizations SCP가 차단하는지
    • configure-aws-credentials에 session policy를 넣었는지 확인

7) 캐시/빌드 이슈를 권한 문제로 오해

  • 증상: 배포 단계가 아니라 빌드 산출물이 없어서 실패하는데 “AWS 문제”처럼 보임
  • 해결: Actions 캐시/아티팩트 흐름을 점검. 관련해서 GitHub Actions 캐시 안 먹힘 원인 7가지를 함께 확인하면 헛다리 줄일 수 있습니다.

8) 디버깅을 빠르게 만드는 “토큰 클레임 확인” 방법

GitHub OIDC 토큰의 실제 sub를 알아야 Trust policy를 정확히 만들 수 있습니다. 가장 깔끔한 방법은 actions/github-script나 간단한 Node/Python으로 ACTIONS_ID_TOKEN_REQUEST_URL을 호출해 토큰을 받아 디코드하는 것입니다.

아래는 토큰을 받아 payload를 출력하는 예시입니다.

- name: Print OIDC token claims (debug)
  shell: bash
  env:
    REQ_URL: ${{ env.ACTIONS_ID_TOKEN_REQUEST_URL }}
    REQ_TOKEN: ${{ env.ACTIONS_ID_TOKEN_REQUEST_TOKEN }}
  run: |
    set -euo pipefail
    echo "Requesting token..."
    JWT=$(curl -sS -H "Authorization: Bearer $REQ_TOKEN" "$REQ_URL&audience=sts.amazonaws.com" | jq -r .value)
    echo "$JWT" | awk -F. '{print $2}' | tr '_-' '/+' | base64 -d 2>/dev/null | jq .
  • 출력된 JSON에서 sub, aud, iss를 Trust policy와 1:1로 맞추면 됩니다.
  • 단, 이 단계는 토큰 정보를 로그에 남기므로 일시적으로만 사용하고 제거하세요.

9) 권장 운영 패턴: 최소 권한 + 배포 경로 고정

실무에서 안정적으로 굴리려면 아래 조합이 가장 사고가 적습니다.

  • Trust policy
    • aud = sts.amazonaws.com
    • sub = repo:ORG/REPO:ref:refs/heads/main 또는 environment:prod
  • Permission policy
    • 필요한 서비스(ECR/S3/CloudFormation/EKS)에만 최소 권한
    • 리소스 ARN을 가능한 좁게
  • 워크플로
    • 배포는 workflow_dispatch + GitHub Environment 승인(선택)
    • aws sts get-caller-identity로 Assume 성공을 먼저 검증

EKS로 배포하는 경우, 이후 단계에서 네트워크/인증이 섞여 더 복잡해질 수 있습니다. IRSA/OIDC 전반의 복구 관점은 EKS OIDC Provider 삭제로 IRSA 전부 실패했을 때 복구도 유사한 사고 대응에 도움이 됩니다.

10) 마무리 체크리스트(이대로만 보면 대부분 해결)

  • 워크플로에 permissions: id-token: write가 있는가?
  • AWS 계정에 OIDC Provider(token.actions.githubusercontent.com)가 존재하는가?
  • Role Trust policy에 aud=sts.amazonaws.com 조건이 있는가?
  • Trust policy의 sub가 실제 트리거(브랜치/태그/환경)와 일치하는가?
  • aws sts get-caller-identity가 성공하는가?
  • 성공 후 실패한다면 CloudTrail에서 거부된 Action/Resource를 확인했는가?
  • Permission boundary/SCP 등 상위 제약이 있는가?

위 체크리스트를 순서대로 밟으면, “OIDC라서 어렵다”기보다 문제 지점이 어디인지 빠르게 특정할 수 있습니다. 특히 get-caller-identity를 경계로 Trust 문제와 Permission 문제를 나누면, 디버깅 시간이 체감상 절반 이하로 줄어듭니다.