npm publish — PAT에서 OIDC로 갈아탄 이야기

어느 날 release job이 E404로 죽었다. 원인을 파고들다 npm이 권장하는 OIDC trusted publishing으로 인증 방식을 아예 바꿨다. 이 글은 그 전환 과정과, OIDC가 왜 PAT보다 관리가 편한지를 정리한 기록이다. changesets + pnpm 환경에서 실제로 적용한 설정까지 담았다.

1. 발단: release job이 E404로 죽었다

평소처럼 배포가 돌던 release job이 어느 날 이렇게 실패했다.

🦋  error an error occurred while publishing <package_name>: E404 Not Found - PUT https://registry.npmjs.org/<package_name> - Not found
🦋  error The requested resource '<package_name>' could not be found or you do not have permission to access it.

4xx 중 401과 404는 보통 이런 뜻이다.

  • 404: 서버가 요청한 리소스를 못 찾았다.
  • 401: 유효한 인증 자격 증명이 없어서 요청을 끝내지 못했다.

여기서 함정이 하나 있다. npm은 publish에서 인증에 실패해도 401이 아니라 404를 준다. 권한 없는 사람에게 “이 패키지가 존재한다”는 사실조차 노출하지 않으려는 설계다. 그래서 E404 PUT...을 만나면 “패키지가 없나?“보다 “토큰이 만료됐나?“부터 의심하는 게 맞다.

실제로 이번 실패도 패키지 문제가 아니라 PAT(Personal Access Token) 만료가 원인이었다.

2. 임시방편이던 PAT — 만료될 때마다 사람이 갈아 끼운다

그동안은 .npmrc에 PAT를 넣어 publish 때 인증했다.

- name: Create .npmrc file
  run: |
      cat << EOF > "$HOME/.npmrc"
        //registry.npmjs.org/:_authToken=${NPM_TOKEN}
      EOF
  env:
      NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

문제는 토큰이 만료될 때마다, 혹은 만료 전 정해진 주기마다 사람이 새 토큰으로 갈아 끼워야 한다는 점이다. 깜빡하면 이번처럼 release가 404로 멈춘다. npm은 이 방식을 비권장으로 돌렸고, 대신 이 글의 주제인 OIDC 방식을 권장한다.

두 방식의 차이는 결국 “저장하는 비밀이 있느냐 없느냐” 하나로 갈린다.

PAT와 OIDC 비교

3. OIDC trusted publishing이란

한 마디로 비유하면 “열쇠 없이, 문 앞에서 매번 신분증을 확인받는 방식“이다. OIDC(OpenID Connect)는 비밀번호 같은 자격 증명 대신 “누구인지”를 증명하는 표준 프로토콜이다.

<repo_name>의 release.yml이라는 신원은 훔쳐갈 수 없고, 어디에도 저장되는 비밀이 없다. 그러니 유출·만료·로테이션이라는 개념 자체가 사라진다.

동작은 이렇다. trusted publisher로 지정된 쪽(GitHub)이 신원을 보증하는 OIDC 토큰을 발급하고, npm이 그 토큰으로 신원을 확인한 뒤 일회용 토큰을 내준다. 이 일회용 토큰은 수명이 짧고, trusted publisher로 등록된 레포의 워크플로우에서만 쓸 수 있다.

전체 흐름은 다음과 같다.

OIDC trusted publishing 핸드셰이크 흐름

  1. npm에 trusted publisher로 내 레포의 특정 워크플로우(GitHub - release.yml)를 등록한다.
  2. 워크플로우가 OIDC 신원 토큰을 발급·서명할 수 있도록 id-token: write 권한을 준다.
  3. publish 워크플로우가 돌면 워크플로우가 OIDC 신원 토큰을 발급받는다.
  4. 그 토큰을 npm에 제시하면, npm이 GitHub 공개키로 서명을 검증한다.
  5. 검증을 통과하면, npm은 토큰 payload의 repo·workflow를 trusted publisher 명단과 대조해 인가한다.
  6. 인가되면 npm이 단기 토큰을 발급하고, 워크플로우는 이 토큰으로 배포한다.

4. changesets + pnpm 환경에 적용하기

요구 조건은 세 가지다.

  • npm 11.15.1 이상
  • 워크플로우에 id-token: write 권한
  • 패키지별 trusted publisher 등록

npm 11.15.1 이상이 필요한 이유

이 버전부터 OIDC trusted publishing handshake가 들어갔다. GitHub의 ID 토큰을 요청하고, 레지스트리에 제시해 짧은 수명의 publish 토큰으로 교환하는 로직이다.

그 이전 버전은 handshake를 아예 모른다. .npmrc의 정적 토큰만 읽는 옛날 흐름을 쓰기 때문에, 토큰이 없으면 ENEEDAUTH로 실패한다.

npm은 전역 설치, publish는 pnpm이 맡는다

@changesets/cligetPublishTool()pnpm-lock.yaml을 감지하면 publish 도구를 pnpm으로 판단하고, 실제 레지스트리 PUT을 pnpm publish로 실행한다.

그럼 전역에 설치한 npm은 무슨 일을 하나. npm은 OIDC trusted publishing handshake를 담당하고, 이 과정에서 이미 배포된 패키지 정보를 확인하는 수단으로 쓰인다. 즉 인증·핸드셰이크는 npm, 실제 배포는 pnpm으로 역할이 나뉜다.

id-token: write 권한

워크플로우 job이 OIDC 토큰을 요청하고 쓸 수 있게 허용하는 권한이다. 이게 없으면 3번 단계에서 토큰 발급 자체가 막힌다.

npm에 trusted publisher 등록

npm 쪽에 GitHub을 trusted publisher로 등록한다. → npm 공식 문서

등록할 정보는 세 가지다.

  • 조직 혹은 사용자 이름
  • 레포지토리
  • 워크플로우 파일명 (예: release.yml)