워크플로가 실행될 때 자동으로 발급되는 토큰. 저장소에 접근할 때 이것을 쓴다.
permissions:
contents: read # 워크플로 기본을 읽기로
jobs:
release:
permissions:
contents: write # 필요한 잡에서만 올린다
id-token: write # OIDC 를 쓸 때
공식 문서의 권장은 명확하다 — 기본을 저장소 콘텐츠 읽기 전용으로 두고, 필요한 잡에서만 개별적으로 올린다.
왜 기본이 위험한가
저장소 설정에 따라 이 토큰이 쓰기 권한까지 갖고 시작할 수 있다. 그 상태에서 서드파티 액션을 하나 쓰면, 그 액션은 우리 저장소에 커밋을 밀어 넣을 수 있는 토큰을 손에 쥔다.
워크플로에서 permissions 를 명시하면 저장소 기본값과 무관하게 그 워크플로만 좁힐 수 있다. 조직 설정을 못 바꾸는 상황에서도 각 워크플로를 안전하게 만들 수 있다는 뜻이다.
잡 단위로 나누는 것이 요점이다
jobs:
test: # 읽기만 — 상속받은 read
runs-on: ubuntu-latest
publish:
permissions:
packages: write # 이 잡에서만 쓰기
테스트 잡은 코드를 읽기만 하면 된다. 여기서 쓰기 권한이 있을 이유가 없다. 권한을 가진 시간과 범위를 줄이는 것이 방어의 본질이다.
토큰의 유용한 성질
- 잡이 끝나면 만료된다 — 유출돼도 유효 기간이 짧다
- 이 토큰의 푸시는 워크플로를 다시 트리거하지 않는다 — 무한 루프를 막는 안전장치다
두 번째는 함정이기도 하다. 워크플로가 커밋을 푸시했는데 다음 워크플로가 안 도는 이유가 이것이다. 의도적으로 연쇄시키려면 별도 토큰이나 앱 인증이 필요하다.
포크 PR 에서는 더 좁다
외부 기여자의 PR 에서는 토큰이 읽기로 제한되고 시크릿에 접근할 수 없다. 불편해 보이지만 의도된 동작이다 — 그렇지 않으면 누구나 PR 하나로 우리 시크릿을 가져갈 수 있다.
시크릿은 정확 일치로만 가려진다
로그 마스킹은 등록된 값과 정확히 같은 문자열을 찾아 가린다. 그래서 공식 문서가 경고하는 것이 있다.
- 구조화된 데이터를 통째로 시크릿에 넣지 않는다 — JSON 을 넣으면 그 안의 개별 값은 별개의 문자열이라 그대로 노출된다
- 변형된 형태도 따로 등록한다 — base64 나 URL 인코딩된 형태는 다른 문자열이다
- 주기적으로 교체한다
무엇을 이 토큰으로 하지 않나
외부 클라우드에 배포하는 일은 이 토큰의 몫이 아니다. 그쪽은 별도 자격증명이 필요한데, 장기 액세스 키를 시크릿에 저장하는 대신 OIDC 로 단기 토큰을 받는 것이 지금의 권장이다.
정리하면 축이 둘이다 — 저장소 안의 일은 GITHUB_TOKEN 을 최소 권한으로, 저장소 밖의 일은 OIDC 로 단기 자격증명.
면접 함정
- ❌ "토큰은 자동이라 신경 쓸 것 없다" → 기본 권한이 넓으면 그대로 공격 표면이 된다.
- ❌ "워크플로 하나에 권한을 다 주는 게 편하다" → 잡 단위로 나눠야 노출 범위가 준다.
- ❌ "포크 PR 에서 시크릿이 안 나오는 건 버그" → 의도된 보호다.