- Published on
GitHub Actions OIDC로 AWS 배포 - AssumeRole 실패 해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버리스든 ECS/EKS든, GitHub Actions에서 AWS로 배포할 때 가장 깔끔한 인증 방식은 OIDC(OpenID Connect) 기반의 AssumeRoleWithWebIdentity입니다. 장기 액세스 키를 저장하지 않아도 되고, 토큰은 워크플로 실행 시점에만 발급되며, 레포/브랜치/환경 단위로 강하게 스코프를 제한할 수 있습니다.
그런데 막상 적용하면 다음과 같은 에러를 자주 만납니다.
Not authorized to perform sts:AssumeRoleWithWebIdentityInvalidIdentityToken: No OpenIDConnect provider found in your accountAccessDenied: ... 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단계입니다.
- 워크플로가
id-token: write권한을 가지고 GitHub OIDC 토큰(JWT)을 발급받음 - AWS STS가 해당 JWT를 검증하고, IAM Role의 신뢰 정책이 허용하면
AssumeRoleWithWebIdentity성공 - 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
여기서 ClientIDList에 sts.amazonaws.com가 없으면, Role trust policy에서 aud를 sts.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 providersNo 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"
}
}
}
]
}
여기서 자주 틀리는 지점:
sub를repo:ORG/REPO:*로 열어두거나, 반대로 실제 값과 다르게 너무 빡빡하게 잠가서 실패ref:refs/heads/main인데 실제는ref:refs/tags/v1.2.3또는pull_request이벤트여서 mismatchaud가sts.amazonaws.com가 아닌 다른 값으로 들어오는데StringEquals로 고정해둠
sub는 이벤트에 따라 달라진다
pushon 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의
FederatedARN이 해당 Provider ARN과 동일한지 확인
Not authorized to perform sts:AssumeRoleWithWebIdentity
- trust policy의
Action이sts:AssumeRole로 되어 있음 aud/sub조건 불일치Principal.FederatedARN이 다른 Provider를 가리킴
해결:
Action을sts:AssumeRoleWithWebIdentity로token.actions.githubusercontent.com:aud및sub조건을 실제 클레임에 맞춤
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 호출
보안적으로 안전한 최소 스코프 설계 팁
- trust policy의
sub는 가능한 한 좁게- 최소:
repo:ORG/REPO:ref:refs/heads/main - 태그 배포면 태그 패턴으로
- 최소:
- production Role은 GitHub Environment 조건을 추가
- 권한 정책은 리소스 ARN을 구체화
Resource: *는 정말 필요한 API에만 제한적으로
마무리: “AssumeRole 실패”를 빠르게 끝내는 체크리스트
- GitHub Actions 워크플로에
permissions.id-token이write인가 - AWS 계정에 OIDC Provider가 있고 URL이
https://token.actions.githubusercontent.com인가 - OIDC Provider의
ClientIDList에sts.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 실패는 “원인-해결”이 깔끔하게 정리됩니다.