목차
GitHub Actions OIDC AWS 연동은 GitHub Actions의 워크플로우가 OpenID Connect 토큰을 받아 AWS의 IAM Role을 임시로 assume 하는 방식이다. 장기 액세스 키(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)를 GitHub Secrets에 박아두는 대신 매 실행마다 단기 자격증명을 받아 쓴다. 키 로테이션 알람에서 해방되고 싶거나 키 유출 사고를 한 번이라도 본 적이 있다면 옮길 만한 가치가 있다.
이 글은 aws-actions/configure-aws-credentials v4 기준으로 작성했다(2026년 5월 시점). v3에서 v4로 넘어오면서 audience 기본값과 thumbprint 처리 방식이 바뀌었으니 기존 워크플로우를 옮기는 경우 릴리스 노트를 한 번은 훑어보는 게 좋다.
장기 키를 버리고 싶었던 이유
이전에 운영하던 파이프라인은 단순했다. ECR에 이미지 푸시, S3에 정적 자산 업로드, CloudFront 무효화. 이 세 가지를 위해 IAM 사용자 하나를 만들고 액세스 키 페어를 GitHub Secrets에 등록해뒀다.
문제는 90일 키 로테이션 정책이었다. 매 분기마다 키를 갈아끼워야 했는데, 한 번은 만료 알람을 놓치고 배포가 멈췄다. CloudTrail을 거꾸로 추적해 키 만료 시점을 찾아내는 데 30분이 더 걸렸다. 권한을 정리하다 보니 그 IAM 사용자 정책이 ec2:*, s3:*까지 열려 있는 것도 발견했다. 신규 입사자가 처음 만든 정책을 아무도 좁히지 않은 채 시간이 흐른 결과였다.
OIDC가 뭘 바꾸는가
OIDC 방식은 흐름이 다르다. GitHub Actions가 워크플로우 실행마다 JWT 토큰을 발급하고, AWS STS가 이 토큰의 issuer(token.actions.githubusercontent.com)와 sub 클레임을 검증한 뒤 임시 자격증명을 돌려준다. 액세스 키는 어디에도 저장되지 않는다.
| 항목 | 장기 액세스 키 | OIDC + IAM Role |
|---|---|---|
| 자격증명 수명 | 무제한(수동 로테이션) | 기본 1시간 |
| GitHub Secrets 저장 | 필수 | 불필요 |
| 권한 격리 단위 | IAM 사용자 | 워크플로우/브랜치/환경 |
| 감사 추적 | IAM 사용자 ARN | assumed-role 세션 ARN |
그러나, 마지막 항목이 운영에서는 가장 크다. CloudTrail 로그의 assumed-role 세션 이름에 GitHub Actions 실행 ID가 박혀 나오기 때문에, 어느 워크플로우 실행에서 무슨 API를 호출했는지 거꾸로 추적할 수 있다. 사고 분석 시간이 줄어든다.
첫 시도, 그리고 두 시간의 헤맴
처음 설정은 쉬워 보였다. AWS 콘솔에서 OIDC Identity Provider를 등록하고(URL: https://token.actions.githubusercontent.com), IAM Role을 하나 만들고, Trust Policy에 GitHub의 sub 클레임을 적었다. 워크플로우는 이렇게 짰다.
permissions:
id-token: write # OIDC 토큰 발급에 필수
contents: read
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::123456789012:role/github-actions-deploy
aws-region: ap-northeast-2
- run: aws sts get-caller-identity
그런데, 푸시하고 워크플로우를 돌렸더니 바로 떨어졌다.
Error: Could not assume role with OIDC:
Not authorized to perform sts:AssumeRoleWithWebIdentity
따라서, 이 메시지는 트라우마다. STS 호출 자체는 갔는데 Trust Policy의 Condition을 못 통과했다는 뜻이다. 처음엔 Identity Provider 등록 thumbprint 문제인 줄 알고 한참을 들여다봤다. AWS는 GitHub OIDC의 thumbprint를 자동 검증하므로 더 이상 직접 입력할 필요가 없는데, 옛 블로그 글들이 이걸 잘못 안내하는 경우가 많다(GitHub aws-actions/configure-aws-credentials 이슈 #357 참고).
진짜 원인은 Trust Policy의 sub 조건이었다. 처음엔 이렇게 써뒀다.
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:sub": "repo:myorg/myapp:*"
}
}
StringEquals로 와일드카드를 써봤자 매칭이 안 된다. AWS IAM의 StringEquals는 글자 그대로의 일치만 본다. 와일드카드를 쓰려면 StringLike를 써야 한다. 이걸 깨닫는 데 두 시간이 걸렸다.
제대로 된 Trust Policy
다시 짠 정책은 이렇다. main 브랜치 푸시와 release 태그에서만 assume을 허용했다.
{
"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:myorg/myapp:ref:refs/heads/main",
"repo:myorg/myapp:ref:refs/tags/v*"
]
}
}
}]
}
핵심은 sub 클레임의 형식이다. repo:OWNER/REPO:ref:refs/heads/BRANCH 또는 repo:OWNER/REPO:environment:ENV_NAME 같이 GitHub이 정한 포맷이 따로 있다. PR에서 돌리려면 repo:myorg/myapp:pull_request를 추가해야 한다. 운영 환경에서는 PR 빌드를 가능하면 빼는 게 맞다고 본다. 포크에서 들어온 PR이 production role을 assume 할 수 있으면 그 자체로 사고다.
물론, 다만 sub 클레임의 정확한 형식은 GitHub의 Security hardening 문서에 표로 정리되어 있다. 새 환경을 추가할 때마다 이 문서를 다시 보는 편이다.
권한을 좁히는 단계가 진짜 일이다
이처럼, OIDC 연결만 되면 끝이라고 생각했는데, 정작 시간이 들어가는 건 IAM Role의 정책을 좁히는 단계였다. 처음에는 PowerUserAccess를 붙여놓고 돌려봤다. 잘 도는 걸 확인한 뒤 CloudTrail에서 그 role이 실제로 호출한 API만 추려냈다.
# 최근 24시간 동안 deploy role이 호출한 API 추출
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=Username,AttributeValue=GitHubActions-deploy \
--start-time $(date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%SZ) \
--query 'Events[].EventName' \
--output text | tr '\t' '\n' | sort -u
따라서, 이 명령으로 뽑은 API 목록이 23개 정도였다. ECR push 관련 6개, S3 관련 8개, CloudFront 관련 4개, 나머지는 sts:GetCallerIdentity 같은 메타. 이걸 그대로 정책 Action으로 옮기고 Resource는 특정 ECR 리포지토리 ARN과 S3 버킷 ARN으로 못 박았다.
워크플로우에서 다른 리소스를 건드리려 하면 AccessDenied가 뜬다. 이 에러는 좋은 신호다. 권한을 의도한 만큼만 줬다는 증거다.
운영하면서 챙긴 디테일
role-session-name을 명시하면 CloudTrail에서 검색이 편해진다. 기본값은 GitHubActions인데, 워크플로우와 실행 ID를 박으면 사고 추적이 빨라진다.
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy
role-session-name: gh-${{ github.run_id }}-${{ github.job }}
aws-region: ap-northeast-2
role-duration-seconds: 1800
role-duration-seconds는 워크플로우 실행 시간보다 살짝 짧게 두는 편이다. 1시간짜리 빌드라면 3600 그대로 두면 토큰 만료가 빌드 끝과 겹쳐 마지막 단계가 떨어질 수 있다(체감상 2~3% 확률).
GitHub Environments와 묶으면 sub 클레임을 repo:myorg/myapp:environment:production으로 더 좁힐 수 있다. production 환경에 reviewer 승인을 걸어두면, 코드 머지가 됐어도 사람이 승인하기 전에는 prod role을 assume 할 수 없다. 이 조합이 현재까지 본 것 중에서는 가장 깔끔한 운영 형태로 보인다.
물론, 당장 적용한다면 세 가지를 추천한다. 첫째, 기존 IAM 사용자 액세스 키부터 비활성화 후보로 표시해둘 것. 둘째, 새 IAM Role의 Trust Policy는 StringLike와 정확한 sub 포맷으로 시작할 것. 셋째, PowerUserAccess로 일단 통과시킨 뒤 CloudTrail로 권한을 좁힐 것.
다음에는 이걸 Terraform 모듈로 묶어서 신규 서비스가 추가될 때 IAM Role과 OIDC Trust Policy를 자동 생성하는 쪽을 실험해볼 생각이다.
관련 글
- AWS CodeBuild GitHub Actions 비교 — 비용·성능·설정(2026) – 분당 단가는 비슷하다. 하지만 동시성, IAM 통합, 캐시 전략까지 보면 그림이 달라진다. 두 서비스를 같은 자에 놓고 비교한다.
- GitHub Actions 자체 호스팅 러너 설정 — EC2에서 CI 비용 75% 줄인 과정 – GitHub Actions 무료 러너가 느려서 EC2에 자체 호스팅 러너를 구축했다. 월 $380이던 비용이 $95까지 내려간 과정과, 그…
- GitHub Actions 비용 절감 — 과금 구조부터 self-hosted runner 손익 계산까지 – GitHub Actions 비용 절감을 위해 GitHub-hosted, self-hosted EC2, third-party 세 옵션을 두고…