Published on

GitHub Actions OIDC로 AWS 배포 - AssumeRole 실패 해결

Authors

서버리스든 ECS/EKS든, GitHub Actions에서 AWS로 배포할 때 가장 깔끔한 인증 방식은 OIDC(OpenID Connect) 기반의 AssumeRoleWithWebIdentity입니다. 장기 액세스 키를 저장하지 않아도 되고, 토큰은 워크플로 실행 시점에만 발급되며, 레포/브랜치/환경 단위로 강하게 스코프를 제한할 수 있습니다.

그런데 막상 적용하면 다음과 같은 에러를 자주 만납니다.

  • Not authorized to perform sts:AssumeRoleWithWebIdentity
  • InvalidIdentityToken: No OpenIDConnect provider found in your account
  • AccessDenied: ... is not authorized to perform: sts:AssumeRole on resource ...
  • The security token included in the request is invalid

이 글은 “왜 실패하는지”를 AWS IAM 신뢰 정책, OIDC Provider 설정, GitHub Actions 권한/워크플로, 그리고 디버깅 순서로 쪼개서 재현 가능하게 해결하는 가이드입니다.

관련해서 GitHub Actions 자체 디버깅 관점은 GitHub Actions 캐시 미스 - 키·경로 디버깅 실전도 함께 참고하면, 워크플로를 관찰/검증하는 습관을 잡는 데 도움이 됩니다.

OIDC 흐름을 한 장으로 이해하기

GitHub Actions OIDC의 핵심은 다음 3단계입니다.

  1. 워크플로가 id-token: write 권한을 가지고 GitHub OIDC 토큰(JWT)을 발급받음
  2. AWS STS가 해당 JWT를 검증하고, IAM Role의 신뢰 정책이 허용하면 AssumeRoleWithWebIdentity 성공
  3. STS가 임시 자격 증명(AccessKeyId/SecretAccessKey/SessionToken)을 발급하고, 이후 AWS API 호출은 이 임시 자격 증명으로 수행

실패의 90%는 2번에서 발생합니다. 즉 “토큰은 발급됐는데, AWS가 신뢰하지 않는다” 혹은 “신뢰는 했는데, 실제 권한 정책이 부족하다”입니다.

가장 흔한 실패 원인 1: OIDC Provider가 없거나 값이 틀림

에러 예:

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

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

  • Provider URL: https://token.actions.githubusercontent.com
  • Audience(클라이언트 ID): 보통 sts.amazonaws.com

CLI로 확인:

aws iam list-open-id-connect-providers

# 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가 없으면, Role trust policy에서 audsts.amazonaws.com로 요구하는 순간 매칭이 깨져 AssumeRole이 실패합니다.

Terraform 예시: OIDC Provider

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

  client_id_list = [
    "sts.amazonaws.com",
  ]

  # thumbprint는 AWS 문서/권장 값을 사용하거나,
  # 관리 정책에 맞춰 최신값을 검증해 반영하세요.
  thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
}

가장 흔한 실패 원인 2: GitHub Actions에 id-token: write 권한이 없음

에러는 보통 다음처럼 보입니다.

  • Could not load credentials from any providers
  • No OIDC token 류의 메시지

워크플로 최상단 또는 해당 job에 다음이 필요합니다.

permissions:
  id-token: write
  contents: read

contents: read는 체크아웃 등 기본 동작에 필요하고, 핵심은 id-token: write입니다. 이게 없으면 GitHub가 OIDC JWT를 발급해주지 않아서, aws-actions/configure-aws-credentials가 STS 호출 자체를 못합니다.

가장 흔한 실패 원인 3: IAM Role 신뢰 정책(trust policy)의 조건이 GitHub 클레임과 불일치

OIDC AssumeRole에서 “신뢰 정책”은 권한 정책과 완전히 별개입니다.

  • 신뢰 정책: “누가 이 Role을 맡을 수 있는가”
  • 권한 정책: “이 Role을 맡은 뒤 무엇을 할 수 있는가”

AssumeRole 단계에서 막히면 대부분 신뢰 정책 문제입니다.

정답에 가까운 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:ORG/REPO:ref:refs/heads/main"
        }
      }
    }
  ]
}

여기서 자주 틀리는 지점:

  • subrepo:ORG/REPO:*로 열어두거나, 반대로 실제 값과 다르게 너무 빡빡하게 잠가서 실패
  • ref:refs/heads/main인데 실제는 ref:refs/tags/v1.2.3 또는 pull_request 이벤트여서 mismatch
  • audsts.amazonaws.com가 아닌 다른 값으로 들어오는데 StringEquals로 고정해둠

sub는 이벤트에 따라 달라진다

  • push on branch: repo:ORG/REPO:ref:refs/heads/브랜치명
  • tag push: repo:ORG/REPO:ref:refs/tags/태그명
  • pull_request: repo:ORG/REPO:pull_request

따라서 배포 워크플로가 tag 기반이면 sub 조건도 태그 패턴으로 맞춰야 합니다.

예: 모든 태그 v 프리픽스만 허용

"StringLike": {
  "token.actions.githubusercontent.com:sub": "repo:ORG/REPO:ref:refs/tags/v*"
}

GitHub Environment를 쓰면 environment 클레임도 고려

Environment 보호 규칙(승인 등)을 쓰는 경우, 토큰에 환경 정보가 들어옵니다. 이때는 더 강하게 잠글 수 있습니다.

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

조직 정책상 “프로덕션 Role은 production environment에서만” 같은 규칙이 필요하면 강력한 안전장치가 됩니다.

가장 흔한 실패 원인 4: Role은 AssumeRole 됐는데, 배포 권한이 부족함

AssumeRole 실패 메시지와 혼동하기 쉬운 케이스입니다.

  • AssumeRole은 성공
  • 이후 ecr:PutImage, ecs:UpdateService, cloudformation:UpdateStack 등에서 AccessDenied

이건 trust policy가 아니라 “권한 정책(permissions policy)” 문제입니다.

예: ECR push + ECS 서비스 업데이트 최소 예시(개념용)

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ecr:GetAuthorizationToken"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "ecr:BatchCheckLayerAvailability",
        "ecr:CompleteLayerUpload",
        "ecr:InitiateLayerUpload",
        "ecr:PutImage",
        "ecr:UploadLayerPart"
      ],
      "Resource": "arn:aws:ecr:ap-northeast-2:123456789012:repository/my-app"
    },
    {
      "Effect": "Allow",
      "Action": [
        "ecs:UpdateService",
        "ecs:DescribeServices"
      ],
      "Resource": "arn:aws:ecs:ap-northeast-2:123456789012:service/my-cluster/my-service"
    }
  ]
}

EKS로 배포하면서 ECR을 건드린다면, 이미지 pull/push와 IAM 연동에서 추가 이슈가 생길 수 있습니다. 쿠버네티스 쪽 인증/권한까지 포함해 막히는 경우는 Kubernetes ImagePullBackOff 401 - ECR·IRSA·imagePullSecrets도 같이 보면 원인 분리가 빨라집니다.

재현 가능한 디버깅 절차: JWT 클레임을 직접 확인하기

“내가 trust policy에 적은 sub/aud가 실제 토큰과 맞는지”를 확인하면 대부분 끝납니다.

GitHub Actions에서 OIDC 토큰을 받아서(민감 정보이므로 주의) 헤더/페이로드만 디코딩해 클레임을 확인합니다.

워크플로 예시: OIDC 클레임 출력(진단용)

아래는 진단 단계에서만 잠깐 쓰고 제거하는 것을 권장합니다.

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

permissions:
  id-token: write
  contents: read

jobs:
  debug-oidc:
    runs-on: ubuntu-latest
    steps:
      - name: Print OIDC claims (debug)
        shell: bash
        run: |
          set -euo pipefail

          echo "Requesting OIDC token"
          resp=$(curl -sS -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
            "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=sts.amazonaws.com")

          token=$(python3 - <<'PY'
import json,sys
print(json.loads(sys.stdin.read())["value"])
PY
          <<< "$resp")

          echo "$token" | awk -F'.' '{print $2}' | tr '_-' '/+' | base64 -d 2>/dev/null | python3 -m json.tool || true

위 출력에서 다음 키들을 확인하세요.

  • aud 값이 정말 sts.amazonaws.com인지
  • sub가 어떤 패턴인지(브랜치, 태그, PR)
  • repository, repository_owner 등 부가 클레임

그리고 trust policy의 조건을 “실제 클레임에 맞춰” 조정합니다.

실전 배포 워크플로 예시: configure-aws-credentials

가장 많이 쓰는 표준 구성입니다.

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/github-actions-deploy
          aws-region: ap-northeast-2

      - name: Verify identity
        run: aws sts get-caller-identity

      - name: Deploy (example)
        run: |
          # 예: ECS 업데이트, CDK/CloudFormation, S3 sync 등
          echo "deploy..."

여기서 aws sts get-caller-identity는 단순하지만 강력한 체크포인트입니다.

  • 여기서 실패하면 신뢰 정책/OIDC Provider/permissions 설정 문제
  • 여기서 성공하고 다음 단계가 실패하면 권한 정책(permissions policy) 문제

케이스별 에러 메시지 매핑표

No OpenIDConnect provider found

  • OIDC Provider 미생성
  • Provider URL 불일치

해결:

  • Provider URL을 https://token.actions.githubusercontent.com로 생성
  • Role trust policy의 Federated ARN이 해당 Provider ARN과 동일한지 확인

Not authorized to perform sts:AssumeRoleWithWebIdentity

  • trust policy의 Actionsts:AssumeRole로 되어 있음
  • aud/sub 조건 불일치
  • Principal.Federated ARN이 다른 Provider를 가리킴

해결:

  • Actionsts:AssumeRoleWithWebIdentity
  • token.actions.githubusercontent.com:audsub 조건을 실제 클레임에 맞춤

AccessDenied ... sts:AssumeRole

  • OIDC가 아니라 일반 AssumeRole을 시도 중
  • role-to-assume가 잘못된 Role을 가리킴

해결:

  • configure-aws-credentials가 OIDC 모드로 동작하도록 id-token: write 부여
  • Role ARN 재확인

The security token included in the request is invalid

  • 보통은 자격 증명 체인이 꼬였을 때(기존 키가 남아있거나, step 간 env 충돌)

해결:

  • 워크플로에서 AWS_ACCESS_KEY_ID 같은 시크릿을 동시에 쓰지 말고 OIDC로 단일화
  • configure-aws-credentials 이후에만 AWS CLI 호출

보안적으로 안전한 최소 스코프 설계 팁

  1. trust policy의 sub는 가능한 한 좁게
    • 최소: repo:ORG/REPO:ref:refs/heads/main
    • 태그 배포면 태그 패턴으로
  2. production Role은 GitHub Environment 조건을 추가
  3. 권한 정책은 리소스 ARN을 구체화
    • Resource: *는 정말 필요한 API에만 제한적으로

마무리: “AssumeRole 실패”를 빠르게 끝내는 체크리스트

  • GitHub Actions 워크플로에 permissions.id-tokenwrite인가
  • AWS 계정에 OIDC Provider가 있고 URL이 https://token.actions.githubusercontent.com인가
  • OIDC Provider의 ClientIDListsts.amazonaws.com가 포함되어 있는가
  • Role trust policy의 Principal.Federated가 올바른 Provider ARN인가
  • trust policy 조건의 aud/sub가 실제 JWT 클레임과 일치하는가
  • aws sts get-caller-identity는 성공하는가
  • 이후 실패는 권한 정책(permissions policy)으로 분리해서 해결하는가

위 순서대로만 점검하면, 대부분의 GitHub Actions OIDC AssumeRole 실패는 “원인-해결”이 깔끔하게 정리됩니다.