모든 GitHub Actions 작업은 토큰을 받습니다. GitHub는 각 작업이 시작될 때 워크플로가 있는 리포지토리로 범위가 제한된 새 GITHUB_TOKEN을 생성하며, 작업의 모든 단계가 이 토큰을 사용할 수 있습니다. 토큰으로 무엇을 할 수 있는지는 많은 팀이 한 번도 들여다보지 않은 설정에 달려 있으며, 오래된 리포지토리에서는 그 답이 여전히 "거의 모든 곳에 쓰기"입니다.
이는 작업 안의 무언가가 악성으로 바뀔 때 중요해집니다. 2025년 8월 Nx "s1ngularity" 침해 사고에서 리포지토리의 토큰은 여전히 읽기 및 쓰기 권한을 갖고 있었습니다. pull_request_target 워크플로의 인젝션이 이 토큰으로 게시 워크플로를 트리거했고, 그 결과 악성 Nx 버전을 게시하는 데 사용된 npm 토큰이 유출되었습니다. 읽기 전용 토큰이었다면 이 단계를 막을 수 있었습니다.
이 글에서는 토큰 권한이 어떻게 설정되는지, 일반적인 작업마다 무엇이 필요한지, 워크플로에 필요한 최소 권한을 어떻게 찾는지 다룹니다.
GITHUB_TOKEN이란
GITHUB_TOKEN은 GitHub Actions 앱의 설치 액세스 토큰입니다. 놓치기 쉬운 몇 가지 특성이 있습니다.
- 사용 여부와 관계없이 존재합니다. 워크플로는 이를
secrets.GITHUB_TOKEN으로 참조하지만, 어떤 액션이든 전달받지 않아도github.token컨텍스트로 읽을 수 있습니다. - 기본적으로 디스크에 기록됩니다.
actions/checkout은 이후 git 명령을 위해 토큰을 저장합니다. 이를 막으려면persist-credentials: false를 설정하세요. v6 이전에는.git/config에 저장되었고, v6에서는$RUNNER_TEMP아래의 별도 파일로 옮겨졌습니다. 2024년 Unit 42의 ArtiPACKED 연구는.git/config가 포함된 아티팩트를 통해 토큰이 유출되는 사례를 발견했습니다. - 작업과 함께 만료됩니다. GitHub 호스팅 러너에서는 작업 시간 제한인 최대 6시간입니다. 셀프 호스팅 러너에서는 최대 24시간까지 갱신될 수 있습니다.
토큰은 항상 존재하므로, 제어할 수 있는 것은 토큰이 할 수 있는 범위뿐입니다.
기본값: 허용 또는 제한
각 엔터프라이즈, 조직, 리포지토리에는 토큰의 기본값이 있습니다.
- 허용(permissive): 모든 권한에 대해 읽기 및 쓰기.
- 제한(restricted):
contents와packages에 대해서만 읽기 전용, 그 외에는 없음.
2023년 2월 2일부터 새 엔터프라이즈, 조직, 리포지토리의 기본값은 제한입니다. 이 변경은 기존 대상에는 적용되지 않았으므로, 그 이전에 만들어진 리포지토리나 조직은 누군가 바꾸지 않았다면 허용 기본값을 유지합니다. 상위 수준에서 제한을 선택하면 하위 수준에서는 허용을 선택할 수 없습니다.
Settings → Actions → General → Workflow permissions에서 조직 수준부터 확인하세요. 같은 섹션에 Allow GitHub Actions to create and approve pull requests도 있으며, 워크플로에 필요하지 않다면 꺼 두어야 합니다.
permissions 키
워크플로의 permissions 키는 기본값에서 토큰 권한을 조정합니다. 두 수준에서 작동합니다.
permissions:
contents: read
jobs:
release:
permissions:
contents: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@<full-commit-sha>
규칙은 다음과 같습니다.
- 작업 수준이 워크플로 수준보다 우선합니다. 워크플로 기본값을 읽기 전용으로 설정하고, 필요한 작업에서만 권한을 높이세요.
- 나열하지 않은 권한은 모두
none이 됩니다. 권한을 하나라도 설정하면 나머지 권한은 모두 제거됩니다.permissions: {}는 모든 권한을 제거합니다. - 값은
read,write,none입니다.write는read를 포함합니다.read-all과write-all은 모든 권한을 한 번에 설정합니다.write-all은 피하세요.
2026년 9월 기준으로 사용할 수 있는 권한은 다음과 같습니다.
| 권한 | 제어 대상 |
|---|---|
actions | 워크플로 실행, 아티팩트, 캐시 |
artifact-metadata | 아티팩트 메타데이터 레코드(2026년에 이 API에 대해 contents를 대체) |
attestations | 아티팩트 증명 |
checks | 체크 실행과 체크 스위트 |
code-quality | 커버리지 보고서 같은 코드 품질 결과 |
contents | 리포지토리 콘텐츠, 커밋, 브랜치, 태그, 릴리스 |
deployments | 배포 |
discussions | 토론 |
id-token | OIDC 토큰 요청(write 또는 none만 가능) |
issues | 이슈와 댓글 |
packages | 컨테이너 레지스트리를 포함한 GitHub Packages |
pages | GitHub Pages 빌드 |
pull-requests | 풀 리퀘스트, 레이블, 리뷰 |
security-events | 코드 스캔 알림과 SARIF 업로드 |
statuses | 커밋 상태 |
vulnerability-alerts | Dependabot 알림(read 또는 none만 가능) |
작업에 필요한 권한이 없으면 API 호출은 403 Resource not accessible by integration으로 실패합니다. 이 오류는 write-all로 바꾸라는 신호가 아니라, 해당 단계에 필요한 권한 하나를 추가하라는 신호입니다.
일반적인 작업에 필요한 권한
| 작업 | 권한 |
|---|---|
| 코드 체크아웃 | contents: read |
| 릴리스 생성 또는 태그 푸시 | contents: write |
| 이슈 또는 풀 리퀘스트에 댓글 작성 | issues: write(풀 리퀘스트 댓글은 이슈 API를 거침) |
| 풀 리퀘스트에 레이블 또는 리뷰 추가 | pull-requests: write |
| OIDC로 클라우드 공급업체에 인증 | id-token: write |
| 아티팩트 증명 생성 | id-token: write, attestations: write, contents: read, 컨테이너 이미지의 경우 packages: write 추가 |
| GitHub Container Registry에 이미지 푸시 | packages: write |
| 코드 스캔에 SARIF 업로드 | security-events: write |
| 워크플로에서 GitHub Pages 배포 | pages: write, id-token: write |
대부분의 테스트 및 빌드 작업에는 contents: read 외에 아무것도 필요하지 않습니다.
포크, pull_request_target, Dependabot
- 포크에서 온 풀 리퀘스트는 관리자가 "Send write tokens to workflows from pull requests"를 켜지 않는 한, 워크플로가 무엇을 요청하든
pull_request이벤트에서 읽기 전용 토큰을 받습니다. 이 설정은 꺼 두세요. - **
pull_request_target**은 공개 포크에서 온 풀 리퀘스트에도 읽기 및 쓰기 토큰을 받습니다. 그래서 가장 잘 알려진 토큰 탈취 사고 다수의 배경이 되었습니다. Nx 침해, 2024년 12월 Ultralytics 침해, 2026년 3월 Trivy 액션 침해는 모두pull_request_target워크플로에서 시작되었습니다. GitHub는 이를 좁혀 가고 있습니다. 2026년 7월부터actions/checkout은 기본적으로 이러한 워크플로에서 포크 코드의 체크아웃을 거부하며, 2026년 11월 2일부터 자체 정책이 없는 공개 리포지토리에서는pull_request_target이 기본적으로 비활성화됩니다. - Dependabot 풀 리퀘스트는 포크처럼 취급되어, 읽기 전용 토큰을 받고 Actions 시크릿에 접근할 수 없습니다. 다른 업데이트 도구를 검토 중이라면 Dependabot 대안을 참조하세요.
최소 권한을 찾는 방법
모든 액션의 소스를 읽어 가며 알아낼 필요는 없습니다.
- GitHub Security Lab의 actions-permissions는 워크플로가 토큰을 어떻게 사용하는지 모니터링하고 최소 권한을 권장합니다. 현재 공개 베타입니다.
- OpenSSF Scorecard의 Token-Permissions 검사는 최상위 수준에 읽기 전용 권한을 설정하지 않은 워크플로를 표시합니다. 이 검사는 고위험으로 평가됩니다.
- 워크플로용 정적 분석기인 zizmor는 과도하거나 기본값인 권한, 자격 증명을 유지하는 체크아웃을 표시합니다.
- StepSecurity의 Secure-Repo와 Harden-Runner는 액션 지식 베이스와 작업이 수행하는 API 호출을 바탕으로 작업 수준 권한을 권장합니다.
실용적인 도입 방법은 다음과 같습니다. 조직 기본값을 제한으로 설정하고, 모든 워크플로 최상단에 permissions: contents: read를 추가한 뒤, 워크플로를 실행하세요. Resource not accessible by integration으로 실패하는 워크플로가 어떤 작업에 어떤 추가 권한이 필요한지 정확히 알려 줍니다.
CRACI의 역할
최소 권한은 탈취된 토큰이 할 수 있는 일을 제한합니다. 하지만 탈취 자체를 막지는 못하며, npm 토큰이나 클라우드 자격 증명 같은 작업의 다른 시크릿도 똑같이 노출되어 있습니다.
CRACI는 CRACI 러너에서 GitHub Actions 작업을 실행합니다. runs-on을 craci로 바꾸면 됩니다. GitHub가 여전히 토큰을 발급하므로 GITHUB_TOKEN, permissions 키, 리포지토리 설정은 이전과 똑같이 작동합니다. CRACI가 더하는 것은 러너에 있습니다. 작업이 접근할 수 있는 호스트를 결정하는 이그레스 정책은 작업 시작 전에 검증되고 실패 시 차단하며, 작업이 가져온 모든 패키지의 기록이 네트워크 연결 추적과 함께 남습니다. 작업 안의 악성 코드가 토큰을 손에 넣더라도, 정책이 토큰을 보낼 수 있는 곳을 제한하고 기록이 무슨 일이 있었는지 보여 줍니다.
작업의 나머지 시크릿에 대해서는 GitHub Actions 시크릿: 유출 경로와 보호 방법을 참조하세요. 데모를 예약하고 여러분의 빌드 하나의 기록을 확인해 보세요.
