필요한 정보 모아보기
테크

GitHub App 설치 토큰 520자 전환 완료 — stateless 토큰에서 깨지는 곳과 11월 30일 전 점검 체크리스트

#GitHub#GitHub App#GitHub Actions#보안#API 토큰

GitHub App 설치 토큰(installation access token)이 10월 2일부로 전부 새 형식으로 바뀌었다. GitHub 체인지로그에 따르면 4월 27일 시작한 단계적 롤아웃이 끝났고, 이제 새로 발급되는 설치 토큰은 기본적으로 ghs_APPID_JWT 모양의 stateless 토큰이다. 접두사 ghs_는 그대로인데 길이가 40자에서 약 520자로 늘었다. 권한, 저장소 범위, 1시간 만료, 토큰 발급 REST API는 바뀌지 않았다.

마감도 하나 생겼다. 롤아웃 기간에 형식을 골라 받을 수 있게 열어 둔 임시 헤더 X-GitHub-Stateless-S2S-Token이 11월 30일에 폐기된다. 그 뒤로는 전환 대상 환경(GitHub.com·Data Residency, Enterprise Server는 해당 없음)에서 옛 40자 토큰을 받을 방법이 없다. 토큰 길이나 모양에 기대고 있는 코드가 있다면 그 전에 고쳐야 한다는 뜻이다.

설치 토큰은 무엇이고, stateless는 뭐가 다른가

GitHub App은 사람 계정이 아니라 앱 자격으로 API를 부르는 방식이다. 흐름은 두 단계다. 먼저 앱의 개인 키로 서명한 JWT를 만든다. 그 JWT로 POST /app/installations/{installation_id}/access_tokens를 호출하면, 해당 설치(앱을 깐 조직이나 계정)에 대해 1시간짜리 토큰이 나온다. 이게 설치 토큰이다. 이후 API 호출은 Authorization: Bearer <토큰> 헤더로 한다. 발급할 때 repositories나 permissions를 넘기면 저장소(최대 500개)와 권한을 더 좁힐 수 있다.

GitHub Actions 워크플로에 자동으로 들어오는 GITHUB_TOKEN도 같은 계열이다. 체인지로그는 이번 형식 변경 범위에 Actions GITHUB_TOKEN을 명시했고, 실제로 4월 말 첫 단계가 GITHUB_TOKEN과 Dependabot·Slack·Teams 같은 GitHub 자체 연동부터 시작됐다. 직접 앱을 만든 적이 없어도 영향을 받을 수 있는 이유다.

'stateless'라는 말은 토큰 자체에 정보를 담는다는 의미로 읽으면 된다. 옛 토큰은 짧은 무작위 문자열이었다. 반면 새 토큰 안의 JWT에는 GitHub 공지 표현대로 대상 설치, 앱, 기본 검증 정보가 들어 있고, GitHub 내부 발급자가 서명한다. 그래서 길이가 길고, 담긴 데이터에 따라 길이가 조금씩 달라진다. GitHub는 이 전환으로 토큰 발급과 검증이 빨라지고 API 안정성이 좋아진다고 설명한다.

한 가지는 분명히 해 두자. JWT라고 해서 열어 보거나 서명을 검증하면 안 된다. 공지는 "클라이언트 앱이 검증할 수 없고, 검증해서도 안 된다", "JWT 내용에 의존하지 말라"고 못 박았다. 토큰은 끝까지 불투명한 문자열로 다뤄야 한다.

바뀐 것과 그대로인 것

항목이전(stateful)지금(stateless)
접두사ghs_ghs_ (동일)
길이40자 고정약 520자, 담긴 데이터에 따라 변동
모양점(.) 없는 영숫자ghs_ 뒤에 점 2개(JWT 3조각), 하이픈·밑줄 포함 가능
만료1시간1시간 (동일)
권한·저장소 범위발급 시 지정동일
발급 APIPOST /app/installations/{id}/access_tokens동일
대상—GitHub.com(Enterprise Cloud), Data Residency. GitHub Enterprise Server는 해당 없음

이미 발급된 옛 토큰은 만료될 때까지 그대로 동작한다. 사용자 대리 토큰(ghu_, user-to-server)은 이번 전환에 들어가지 않았다. GitHub는 Copilot 코드 리뷰 쪽 사용자 토큰 형식 변경을 따로 알리겠다고만 했다.

토큰 폐기는 어떨까. REST 문서에는 여전히 DELETE /installation/token(성공 시 204)이 있고, 호출에 쓴 그 토큰을 즉시 무효로 만든다고 적혀 있다. 이번 체인지로그도 폐기 방식이 바뀐다는 언급은 없다. 유출이 의심될 때 1시간 만료를 기다리지 말고 이 엔드포인트로 바로 끊는 습관은 그대로 유효하다.

형식 구분은 간단하다. GitHub 안내대로 ghs_ 뒤의 점 개수를 세면 된다. 두 개면 stateless, 없으면 옛 형식이다.

롤아웃 동안 실제로 깨진 곳들

공지 문구만 보면 "컬럼 길이만 늘리면 되겠네" 싶지만, 반년 동안 실제로 터진 사례를 보면 함정이 더 많다.

Composer 토큰 노출(CVE-2026-45793). 가장 큰 사고는 PHP 패키지 관리자 Composer에서 났다. Composer는 GitHub 토큰을 ^[.A-Za-z0-9_]+$ 정규식으로 검사했는데, 새 토큰의 base64url 구간에는 하이픈(-)이 들어간다. 검사에 떨어진 토큰을 오류 메시지에 그대로 넣어 출력하는 코드까지 겹쳤다. Actions의 비밀값 가리기(masking)는 오류 서식이나 ANSI 코드에 감싸진 문자열을 제대로 가리지 못했고, 결국 토큰 원문이 작업 로그에 남았다. Composer 공식 블로그 기준으로 5월 12일(UTC) 첫 보고가 들어왔고 이튿날 패치(2.9.8 / 2.2.28 / 1.10.28)가 나왔다. GitHub는 같은 날 형식 롤아웃을 일단 되돌렸다. GitHub 보안 권고에는 CVSS 7.5(High)로 올라 있다.

GitHub 권장 정규식도 한 번 고쳐졌다. 5월 15일 체인지로그에는 "편집자 주(5월 26일): 정규식 안내를 갱신했다"는 문장이 붙어 있다. 지금 권장안은 ghs_[A-Za-z0-9\.\-_]{36,}이다. 반면 5월 25일 GitHub 조직의 한 저장소(gh-aw-firewall)에 올라온 이슈는 비밀값 탐지 패턴을 ghs_[A-Za-z0-9._]{36,}로 바꾸자고 제안했다. 하이픈이 빠진 형태다. 옛 안내를 그대로 옮기면 Composer와 같은 구멍이 다시 생긴다.

외부 서비스의 길이 제한. AWS Amplify Hosting 저장소에는 8월 15일 이슈가 올라왔다. Amplify의 CreateApp/UpdateApp API가 accessToken을 255자까지만 받는데, 작성자의 앱이 받은 새 토큰은 383자라 연결 자체가 실패한다는 내용이다. 작성자는 임시 헤더에 disabled를 넣어 옛 토큰을 받는 식으로 버티고 있었다. 이슈는 아직 열려 있다. 11월 30일이 지나면 이 우회로도 막힌다. 내 코드가 아니라 내가 토큰을 넘기는 외부 서비스가 막히는 경우도 있다는 점에서 눈여겨볼 만하다.

가짜 토큰으로 직접 돌려 본 결과

실제 토큰을 발급할 앱이 없어서, 공지에 나온 모양대로 가짜 토큰을 만들어 흔히 쓰는 검사 코드를 통과하는지 돌려 봤다(2026-10-05, macOS, Node.js v22.16.0). ghs_123456_ 뒤에 base64url 조각 셋을 점으로 이어 붙였고, 하이픈이 하나 이상 들어가게 했다. 길이는 488자다.

// legacy: 옛 형식 40자 토큰, fake: 위에서 만든 488자 가짜 토큰(둘 다 미리 정의해 둔 문자열)
const pats = {
  legacy:   /^ghs_[A-Za-z0-9]{36}$/,
  composer: /^[.A-Za-z0-9_]+$/,
  noHyphen: /^ghs_[A-Za-z0-9._]{36,}$/,
  github:   /^ghs_[A-Za-z0-9\.\-_]{36,}$/,
};
for (const [k, r] of Object.entries(pats))
  console.log(k, r.test(legacy), r.test(fake));

결과는 이렇다.

패턴옛 40자 토큰새 형식 가짜 토큰
^ghs_[A-Za-z0-9]{36}$ (옛 형식 전용)통과실패
^[.A-Za-z0-9_]+$ (Composer 구버전)통과실패
^ghs_[A-Za-z0-9._]{36,}$ (하이픈 누락)통과실패
^ghs_[A-Za-z0-9\.\-_]{36,}$ (GitHub 권장)통과통과

리댁션(가리기) 쪽이 더 무섭다. 하이픈 없는 패턴을 앵커 없이 로그 가리기에 쓰면 매치가 첫 하이픈 앞에서 끊긴다. 이번 가짜 토큰에서는 488자 중 461자만 가려지고 27자가 평문으로 남았다. 하이픈 위치는 토큰마다 다르니 남는 양도 매번 다르다. 일부만 노출돼도 비밀값 관리상으로는 사고로 보는 게 맞다.

API 쪽도 확인했다. 518자짜리 가짜 ghs_ 토큰을 Authorization 헤더에 실어 GET /installation/repositories와 DELETE /installation/token을 호출하니 둘 다 401 Bad credentials가 돌아왔다. 긴 헤더 자체는 GitHub까지 문제없이 들어간다는 얘기다. 그러니 중간에 낀 프록시나 게이트웨이가 헤더를 잘라서 넘기면, GitHub에서는 똑같이 401이 돌아오는데 원인은 우리 쪽에 있는 상황이 된다. 헤더를 아예 거부할 때의 응답 코드는 프록시마다 다르다. 디버깅할 때 헷갈리기 쉬운 지점이다.

11월 30일 전에 우리 쪽에서 볼 곳

개인적으로는 DB 컬럼보다 로그 쪽을 먼저 보길 권한다. 컬럼이 짧으면 저장이 실패하거나 잘려서 곧바로 장애로 드러난다. 반면 리댁션 패턴이 낡으면 아무 오류 없이 토큰이 로그에 쌓인다. Composer 사례가 정확히 그랬다.

점검 순서는 이 정도면 된다.

  1. 코드 전체에서 토큰 정규식을 찾는다. ghs_, {36}, {40}, [A-Za-z0-9] 같은 문자열로 검색하면 대부분 걸린다. 검증용이면 아예 빼는 게 낫다. 탐지·리댁션용이라면 GitHub 권장안처럼 점·하이픈·밑줄을 모두 허용하고 상한 없이 바꾼다.
  2. 저장소 길이를 넉넉하게. VARCHAR(40)·VARCHAR(255) 컬럼, 비밀 저장소 항목 크기, 환경변수 길이 제한을 확인한다. GitHub 기준은 "최소 520자"지만 길이가 데이터에 따라 변한다고 했으니, Postgres라면 TEXT로 두는 쪽이 마음 편하다. 1시간짜리 토큰을 굳이 DB에 오래 둘 필요가 있는지도 이참에 다시 볼 만하다.
  3. 프록시·게이트웨이 헤더 한도. Nginx 같은 리버스 프록시나 사내 API 게이트웨이를 거쳐 GitHub를 부른다면, 헤더 크기 제한과 로깅 규칙을 본다.
  4. 토큰을 넘기는 외부 서비스. Amplify처럼 토큰을 받아 가는 SaaS가 있다면 길이 제한이 있는지 문서나 이슈 트래커를 찾아본다. 우리 코드로는 못 고치는 부분이라 일찍 알수록 낫다.
  5. 임시 헤더를 걷어낸다. X-GitHub-Stateless-S2S-Token: disabled로 버티던 코드는 11월 30일이 지나면 그냥 무시된다. 그 전에 enabled로 새 형식을 끝까지 테스트하고, 헤더를 지운다.
  6. Actions 로그를 다시 본다. Composer를 쓰는 워크플로라면 2.9.8 / 2.2.28 이상인지 확인한다. 5월 롤아웃 기간 로그에 토큰이 찍혔는지도 한 번 훑는다. 토큰은 이미 만료됐겠지만 로그 보존 정책은 별개 문제다.

GitHub의 메시지는 결국 하나다. 토큰은 불투명한 문자열이다. 길이도, 문자 종류도, 내부 구조도 언젠가 또 바뀔 수 있다. 이번에 정규식을 고치면서 상한을 520으로 새로 박아 넣는다면, 같은 일을 몇 년 뒤에 또 하게 된다.

이 글의 일부 내용은 GitHub Docs(CC BY 4.0)를 바탕으로 정리했다.

출처 github.blog/…/2026-10-02-stateless-github-app-installation-tokens-rolled-outgithub.blog/…/2026-05-15-github-app-installation-tokens-per-request-override-headergithub.blog/…/2026-04-24-notice-about-upcoming-new-format-for-github-app-installation-tokensdocs.github.com/…/generating-an-installation-access-token-for-a-github-appdocs.github.com/…/installationsblog.packagist.comgithub.com/…/GHSA-f9f8-rm49-7jv2github.com/…/3785github.com/…/4106

내용이 틀렸나요? 정정 요청

관련 글