GitHub Actions OIDC로 AWS 장기 자격증명을 걷어내기 — Trust Policy 설계와 2026년 sub 클레임 변경 대응
저도 한동안은 GitHub Secrets에 AWS_ACCESS_KEY_ID와 AWS_SECRET_ACCESS_KEY를 넣어두고 배포 파이프라인을 굴렸습니다. "어차피 Secret이니까 괜찮지 않나?"라고 스스로를 합리화했죠. 근데 솔직히 그 자격증명은 만료가 없는 장기 키였고, 한 번 유출되면 탐지도 늦고 피해도 컸습니다.
GitHub Actions OIDC를 사용하면 AWS 장기 자격증명을 아예 없앨 수 있습니다. GitHub가 직접 Identity Provider 역할을 맡아 워크플로 실행 시 서명된 JWT를 발급하고, AWS STS가 이를 검증해 수명이 짧은 임시 자격증명을 내줍니다. 장기 키가 없으니 유출될 키도 없고, 로테이션 걱정도 사라집니다.
다만 설정이 쉽지만은 않습니다. Trust Policy의 sub 클레임 조건을 잘못 쓰면 오히려 GitHub 전체 워크플로가 역할을 가정할 수 있는 심각한 취약점이 생기고, 2026년 7월 15일부터는 신규 repository의 OIDC 토큰 sub 형식 자체가 바뀌어 기존 파이프라인이 갑자기 깨지는 사례도 나오고 있습니다. 이 글에서는 올바른 설정 방법부터 2026년 변경 대응, 최소 권한 설계 원칙까지 정리해 보겠습니다.
왜 지금 이 방식이 중요한가
장기 자격증명의 구조적 한계
GitHub Secrets에 저장된 AWS 장기 자격증명에는 몇 가지 피하기 어려운 문제가 있습니다.
- 만료가 없습니다. IAM 사용자 액세스 키는 명시적으로 삭제하기 전까지 유효합니다.
- 로테이션이 수동입니다. 주기적으로 새 키를 발급하고, GitHub Secrets를 업데이트하고, 구 키를 삭제하는 과정을 빠뜨리기 쉽습니다.
- 공급망 공격에 노출됩니다. 2025년 3월 tj-actions/changed-files 사건처럼 서드파티 Action이 침해돼 워크플로 환경변수와 시크릿이 로그로 유출된 실제 사례가 있었습니다.
OIDC 방식에서는 임시 자격증명이 기본 1시간 후 자동 만료됩니다. GitHub OIDC 토큰 자체도 발급 후 짧은 시간 안에 만료되므로 탈취 공격의 유효 시간이 극히 짧아집니다.
보안 요구가 높아진 배경
Datadog Security Labs는 wildcard sub 조건(repo:* 형태 등)으로 설정된 IAM 역할이 실제 환경에 다수 존재한다는 연구 결과를 발표했습니다. AWS와 GitHub 모두 이런 오설정 탐지·경고 기능을 강화하는 방향으로 움직이고 있고, OIDC 도입만큼이나 올바른 Trust Policy 설계 자체가 실제 보안 수준을 좌우합니다.
OIDC 인증 흐름 한눈에 보기
글로 쓰면 복잡해 보이지만, 단계를 순서대로 따라가면 명확합니다.
aws-actions/configure-aws-credentials@v4 액션이 OIDC 토큰 요청부터 임시 자격증명 수신까지의 전 과정을 자동으로 처리해 줍니다. 워크플로 작성자 입장에서는 role-to-assume과 리전만 지정하면 됩니다. Trust Policy 평가는 STS 내부 처리이므로 별도 API 호출로 보이지 않습니다.
설정 단계별 코드
AWS에 GitHub OIDC Provider 등록
AWS 콘솔에서 직접 해도 되지만, Terraform으로 IaC 관리하는 편이 훨씬 낫습니다. 계정당 한 번만 등록하면 됩니다.
resource "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
# AWS는 token.actions.githubusercontent.com에 대해
# 자체 신뢰 CA를 사용하며 thumbprint 검증을 수행하지 않습니다.
# (AWS 공식 문서: Creating OpenID Connect (OIDC) identity providers 참조)
# 아래 값은 형식상 필요하지만 실제 검증에는 사용되지 않으므로 유지 관리 부담이 없습니다.
thumbprint_list = ["ffffffffffffffffffffffffffffffffffffffff"]
}AWS는 2023년 하반기부터 GitHub OIDC provider에 대해서는 자체 신뢰 CA 스토어를 사용하므로 thumbprint 값을 신경 쓰지 않아도 됩니다. 특정 thumbprint 값에 의존하는 예제를 그대로 복사하지 않는 편이 좋습니다.
IAM 역할 및 Trust Policy 생성
sub 클레임을 배열로 지정한 이유는 바로 아래 섹션에서 설명합니다.
resource "aws_iam_role" "github_actions_ecr_deploy" {
name = "github-actions-ecr-deploy"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "sts:AssumeRoleWithWebIdentity"
Principal = {
Federated = aws_iam_openid_connect_provider.github.arn
}
Condition = {
StringEquals = {
"token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
"token.actions.githubusercontent.com:sub" = [
"repo:my-org/my-repo:ref:refs/heads/main",
"repo:my-org@123456/my-repo@789012:ref:refs/heads/main"
]
}
}
}]
})
}sub도 정확히 일치하는 값들만 허용하고 싶다면 StringLike가 아니라 StringEquals로 배열을 넘기는 것이 안전합니다. 와일드카드(*)를 실제로 써야 할 때만 StringLike로 바꾸는 것이 원칙에 맞습니다.
GitHub Actions 워크플로
name: Deploy to ECR
on:
push:
branches: [main]
permissions:
id-token: write # OIDC 토큰 요청에 필수
contents: read
env:
AWS_REGION: ap-northeast-2
ECR_REPOSITORY: my-repo
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::ACCOUNT_ID:role/github-actions-ecr-deploy
aws-region: ${{ env.AWS_REGION }}
- id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Build and push Docker image
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAGamazon-ecr-login@v2 스텝에 id: login-ecr를 붙여야 아래에서 steps.login-ecr.outputs.registry를 참조할 수 있고, ECR_REPOSITORY도 workflow 레벨 env에 정의해 둬야 빈 문자열이 치환되는 사고를 막을 수 있습니다.
Trust Policy sub 클레임 설계 — 이게 핵심입니다
2026년 7월 sub 클레임 형식 변경
GitHub Changelog 2026년 4월 23일 발표에 따르면, 2026년 7월 15일부터 신규 생성 repository에는 숫자 ID가 포함된 불변(immutable) sub 클레임 형식이 적용됩니다.
| 구분 | sub 클레임 형식 |
|---|---|
| 기존 (2026.07.15 이전 생성 repo) | repo:my-org/my-repo:ref:refs/heads/main |
| 신규 (2026.07.15 이후 생성 repo, 또는 opt-in) | repo:my-org@{ORG_ID}/my-repo@{REPO_ID}:ref:refs/heads/main |
ORG_ID와 REPO_ID는 GitHub REST API로 확인할 수 있습니다. 조직 ID는 GET /orgs/{org} 응답의 id 필드, repository ID는 GET /repos/{owner}/{repo} 응답의 id 필드입니다. gh api /orgs/my-org --jq .id처럼 GitHub CLI로 즉시 조회해도 됩니다.
기존 Trust Policy를 그대로 두면 신규 생성 repository의 토큰과 조건이 불일치하여 Not authorized to perform sts:AssumeRoleWithWebIdentity 오류가 납니다. 양쪽 형식을 모두 허용하도록 배열로 업데이트하는 것이 현시점 권장 대응입니다.
{
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": [
"repo:my-org/my-repo:ref:refs/heads/main",
"repo:my-org@123456/my-repo@789012:ref:refs/heads/main"
]
}
}어떤 sub 조건을 써야 할까
배포 성격에 따라 적절한 sub 조건 문자열이 달라집니다. 상황별 예시를 표로 정리합니다.
| 배포 성격 | 권장 sub 조건 (기존 형식 기준) | 비고 |
|---|---|---|
| dev 브랜치 배포 | repo:my-org/my-repo:ref:refs/heads/dev |
브랜치 단위 최소 범위 |
| 태그 기반 릴리스 | repo:my-org/my-repo:ref:refs/tags/v* |
태그 네임스페이스 지정, 와일드카드 사용 시 StringLike |
| PR 검증 (읽기 권한 한정) | repo:my-org/my-repo:pull_request |
쓰기 권한 부여 금지 |
| 프로덕션 배포 | repo:my-org/my-repo:environment:production |
GitHub Environment + 필수 승인자 조합 권장 |
프로덕션 배포에서 브랜치 필터만으로는 부족합니다. GitHub Environment를 활용하면 sub 클레임이 repo:my-org/my-repo:environment:production 형식으로 좁혀지고, 필수 승인자(Reviewer) 설정과 조합해 실제 배포 게이트 역할을 할 수 있습니다.
절대 쓰면 안 되는 조건
아래처럼 와일드카드를 너무 넓게 쓰면 조직 내 임의의 워크플로가 이 역할을 가정할 수 있는 상태가 됩니다.
{
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:*"
}
}Datadog Security Labs 연구에서 이런 설정이 실제 다수 발견됐고, 신뢰 정책의 조건 부분이 너무 넓으면 fork나 PR에서 실행된 임의 코드가 역할을 가정할 여지가 생깁니다.
사용 사례별 최소 권한 설계
하나의 IAM 역할이 모든 repository와 모든 환경을 커버하는 건 최소 권한 원칙에 위배됩니다. 역할은 환경별(dev/staging/prod)로, 가능하다면 repository별로 분리하는 것이 좋습니다.
ECR 이미지 푸시 역할
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["ecr:GetAuthorizationToken"],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:PutImage",
"ecr:InitiateLayerUpload",
"ecr:UploadLayerPart",
"ecr:CompleteLayerUpload"
],
"Resource": "arn:aws:ecr:ap-northeast-2:ACCOUNT_ID:repository/my-repo"
}
]
}ecr:GetAuthorizationToken은 리소스 ARN을 지정할 수 없어 "*"을 써야 하지만, 나머지 액션은 특정 ECR 리포지토리 ARN으로 범위를 좁힐 수 있습니다.
ECS 서비스 업데이트 역할
ECR 이미지 태그를 변경하고 ECS 서비스를 재배포하는 데 필요한 최소 권한 구성입니다.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ecs:RegisterTaskDefinition",
"ecs:DescribeTaskDefinition"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"ecs:UpdateService",
"ecs:DescribeServices"
],
"Resource": "arn:aws:ecs:ap-northeast-2:ACCOUNT_ID:service/my-cluster/my-service"
},
{
"Effect": "Allow",
"Action": "iam:PassRole",
"Resource": "arn:aws:iam::ACCOUNT_ID:role/ecs-task-execution-role"
}
]
}RegisterTaskDefinition과 DescribeTaskDefinition은 리소스 레벨 권한을 지원하지 않아 "*"가 불가피하지만, UpdateService와 DescribeServices는 특정 서비스 ARN으로 좁혀야 원칙에 맞습니다.
S3 정적 배포 + CloudFront 캐시 무효화
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:DeleteObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::my-static-bucket",
"arn:aws:s3:::my-static-bucket/*"
]
},
{
"Effect": "Allow",
"Action": "cloudfront:CreateInvalidation",
"Resource": "arn:aws:cloudfront::ACCOUNT_ID:distribution/DISTRIBUTION_ID"
}
]
}역할 설계 원칙 한눈에 보기
| 원칙 | 나쁜 예 | 좋은 예 |
|---|---|---|
| 환경 분리 | 단일 역할로 dev/prod 통합 | 환경별 별도 역할 |
| Repository 범위 | repo:my-org/* 와일드카드 |
repo:my-org/specific-repo:... 정확 매칭 |
| 리소스 범위 | "Resource": "*" 남발 |
특정 ARN 지정 |
| 권한 상승 차단 | iam:* 허용 |
Permission Boundary 적용 |
| 감사 | 별도 감사 없음 | IAM Access Analyzer + CloudTrail |
iam:* 같은 권한 상승 액션은 배포 역할에 부여하지 않는 것이 좋습니다. 역할이 스스로 권한을 확대하는 걸 막으려면 Permission Boundary를 추가로 설정하는 방법이 있고, AWS IAM Access Analyzer를 활용하면 CloudTrail 로그 기반으로 실제 사용된 권한만 남긴 최소 권한 정책을 제안받을 수 있습니다.
트레이드오프와 흔한 실수
OIDC 방식 vs 장기 자격증명 비교
| 항목 | OIDC 방식 | 장기 자격증명 방식 |
|---|---|---|
| 자격증명 노출 위험 | 낮음 (임시 자격증명, 기본 1시간) | 높음 (만료 없는 장기 키) |
| 설정 복잡도 | 중간 (Trust Policy 설계 필요) | 낮음 (키 발급 후 바로 사용) |
| 자격증명 로테이션 | 자동 | 수동 (주기적 키 교체 필요) |
| 감사 추적 | 우수 (CloudTrail에 repo, ref 기록) | 보통 (어떤 파이프라인인지 특정 어려움) |
| 멀티 클라우드 | audience 분리 설계 필요 | 클라우드별 독립 |
실무에서 자주 만나는 실수들
id-token: write 권한 누락. 워크플로 최상단에 이 한 줄이 없으면 configure-aws-credentials 스텝이 Error: Credentials could not be loaded, please check your action inputs: Could not load credentials from any providers 형태의 오류로 실패합니다. 실제 원인이 권한 누락이라는 힌트가 잘 드러나지 않으니 항상 permissions 블록을 먼저 확인하는 습관이 좋습니다.
브랜치 필터만 믿고 Environment 안 쓰기. 프로덕션에 ref:refs/heads/main 조건만 걸면, main에 푸시 권한이 있는 사람 누구나 배포를 트리거할 수 있습니다. GitHub Environment에 승인자를 설정하면 추가 보안 게이트를 확보할 수 있습니다.
configure-aws-credentials 스텝 배치를 뒤로 미루기. OIDC 토큰은 워크플로 실행 중 발급되며 유효 시간이 길지 않습니다. 인증 스텝을 워크플로 초반에 두고, 장시간 실행 잡에서는 세션 만료 시점을 고려해 재인증 스텝을 넣는 것이 안전합니다.
신규 repository에서 갑자기 배포 실패. 2026년 7월 15일 이후 생성한 repository라면 sub 클레임 형식이 바뀌어 기존 Trust Policy와 불일치합니다. Trust Policy를 배열로 업데이트하는 것이 현시점 권장 대응입니다.
audience 설정에 대한 최근 논의
2026년 8월 보안 연구에서, GitHub OIDC 토큰의 aud 클레임이 관례적으로 sts.amazonaws.com 같은 값으로 고정 사용되는 현실 자체가 위험 요소라는 지적이 나왔습니다. 실제로는 configure-aws-credentials 액션의 audience 입력이나 actions/core의 getIDToken() 인자를 통해 임의 audience로 토큰을 요청할 수 있으므로, 멀티 클라우드 환경이라면 클라우드별로 audience 문자열을 분리하고 Trust Policy 조건에도 StringEquals로 정확한 audience를 명시해두는 것이 안전합니다.
대규모 팀을 위한 Role Vending Machine 패턴
팀이 커지면 IAM 역할을 수작업으로 하나씩 만들기가 버거워집니다. aws-samples/role-vending-machine은 AWS 공식 샘플로, 각 팀이 GitHub repository 단위로 IAM 역할을 PR로 신청하면 보안팀이 리뷰·승인하고 자동으로 프로비저닝해주는 패턴입니다.
소규모 팀에서는 다소 과한 설정이지만, 수십 개 이상의 파이프라인을 운영하는 조직이라면 권한 일관성 유지와 보안 감사 측면에서 상당히 유용합니다.
정리하며
GitHub Actions OIDC는 설정 자체보다 Trust Policy 설계와 최소 권한 원칙을 얼마나 정확히 이해하고 적용하느냐가 실제 보안 수준을 결정합니다. 자격증명을 없앴다고 끝이 아니라, sub 클레임 조건을 얼마나 좁게 잡느냐, audience를 명시적으로 검증하는가, IAM 정책의 리소스 범위를 얼마나 조여두는가가 핵심입니다.
2026년 7월 sub 클레임 형식 변경처럼, OIDC 기반 파이프라인도 GitHub나 AWS 정책 변화에 영향을 받습니다. Trust Policy를 Terraform 같은 IaC로 관리해두면 이런 변경에 훨씬 빠르게 대응할 수 있습니다. 기존에 GitHub Secrets로 AWS 자격증명을 관리 중이라면, OIDC로 전환하면서 IAM Access Analyzer로 실제 사용 권한을 분석해 최소 권한 정책을 다듬어보는 것이 좋은 출발점이 될 것입니다.
참고 자료
- Configuring OpenID Connect in Amazon Web Services — GitHub 공식 문서
- Use IAM roles to connect GitHub Actions to AWS — AWS Security Blog
- aws-actions/configure-aws-credentials — GitHub 공식 액션 저장소
- Immutable subject claims for GitHub Actions OIDC tokens — GitHub Changelog (2026.04.23)
- tj-actions/changed-files 침해 사례 분석 — StepSecurity (2025.03)
- Provision least-privilege IAM roles by deploying a role vending machine solution — AWS Prescriptive Guidance
- aws-samples/role-vending-machine — AWS 공식 샘플
- Exploring GitHub-to-AWS keyless authentication flaws — Datadog Security Labs
- GitHub Actions needs OIDC audience constraints — 보안 연구 (2026.08)
- OpenID Connect reference — GitHub Docs
- Build and push Docker images to Amazon ECR using GitHub Actions and Terraform — AWS Prescriptive Guidance