npm stage-only 토큰이 바꾸는 배포 경계: 자동화는 제출하고 사람은 승인한다

npm이 2026년 9월 18일 패키지 버전을 바로 배포하지 못하는 stage-only granular access token을 공개했습니다. 자동화가 버전을 만들고 검증하는 일은 계속할 수 있지만, 공개 레지스트리에 올리는 마지막 동작은 별도 승인으로 남겨 두는 방식입니다.

이 기능은 새 인증 방식 하나가 추가됐다는 소식보다 배포 파이프라인의 권한을 나누는 방법으로 보는 편이 정확합니다. CI가 가진 토큰이 탈취돼도 곧바로 새 버전을 공개하지 못하게 만들고, 유지 관리자가 2FA를 거쳐 마지막 결정을 내리게 합니다. 다만 기존 토큰의 권한을 자동으로 바꾸는 기능은 아니며, opt-in 전환입니다.

stage와 publish는 다른 동작이다

일반적인 npm 흐름은 npm publish 한 번으로 패키지를 레지스트리에 공개합니다. stage-only 토큰을 쓰면 자동화는 npm stage publish로 버전을 staging area에 제출합니다. 이 제출은 2FA 없이 가능하지만 공개 배포는 아닙니다. 유지 관리자가 CLI나 npmjs.com에서 내용을 확인하고 2FA로 승인해야 실제 버전이 공개됩니다.

실무 설계에서는 이 구분에 맞춰 배포 성공을 두 단계로 나눌 수 있습니다. 첫 단계는 빌드 산출물, tarball, 의존성, 가능한 경우 provenance를 검사하고 제출하는 일입니다. 이는 운영상 권고이지 stage-only 토큰 자체가 검사를 자동 수행한다는 뜻은 아닙니다. 두 번째 단계는 사람이 게시 대상을 확인하고 승인하는 일입니다. CI 로그에 "stage succeeded"가 남았다고 해서 사용자가 즉시 설치할 수 있는 버전이 생긴 것은 아닙니다.

npm staged publishing에서 CI 제출과 staging 검토 및 유지 관리자의 2FA 공개 승인을 분리한 흐름도

그림 1. 자동화는 npm stage publish로 제출하고, 유지 관리자가 검토·2FA 승인한 뒤 공개한다. 출처: docs.npmjs.com/staged-publishing.

공식 문서는 staged publishing의 순서를 stage, review, approve 세 단계로 설명합니다. 이미 npm 레지스트리에 존재하는 패키지여야 하고, 토큰 사용자에게 publish access가 있어야 하며, npm 계정에는 2FA가 켜져 있어야 합니다. 현재 요구 버전은 npm CLI 11.15.0 이상, Node.js 22.14.0 이상입니다.

토큰이 줄이는 사고 반경

stage-only 토큰은 읽기 권한과 publish-and-stage 권한 사이에 별도 선택지를 둡니다. 특정 패키지와 scope만 지정할 수 있고 만료일과 CIDR IP 범위도 설정할 수 있습니다. 조직의 모든 패키지에 넓은 쓰기 권한을 주는 대신 한 서비스가 관리하는 패키지만 허용하는 식으로 범위를 줄일 수 있습니다.

그래도 stage-only를 읽기 전용으로 착각하면 안 됩니다. 이 토큰은 staging 제출 권한을 가집니다.

npm access token의 read-only, stage-only, publish-and-stage 권한과 공개 가능 범위를 비교한 도식

그림 2. stage-only는 읽기 전용보다 넓지만 직접 공개할 수 있는 publish 권한과는 다르다. 출처: docs.npmjs.com/about-access-tokens#about-stage-only-tokens.

공식 문서에 따르면 dist-tag 이동과 버전 deprecate 같은 다른 쓰기 작업도 여전히 보호해야 할 대상입니다. 즉, 공개 배포를 막아도 토큰을 비밀처럼 다루고 로그·환경 변수·CI secret 저장소를 점검해야 합니다.

인증 방식과 승인 절차는 별개의 선택이다

npm은 CI/CD 패키지 배포에 OIDC 기반 trusted publishing을 우선 권합니다. GitHub Actions, GitLab CI/CD, CircleCI처럼 지원되는 환경에서는 장기 npm 토큰 대신 특정 workflow와 연결된 짧은 수명의 인증을 사용할 수 있습니다. 이 방식은 토큰을 저장하고 회전하는 부담을 없애는 방향입니다.

하지만 모든 환경이 trusted publishing을 쓸 수 있는 것은 아닙니다. self-hosted runner처럼 아직 지원되지 않는 환경이나 다른 CI 제공자를 쓰는 팀에는 stage-only 토큰이 현실적인 중간 단계가 됩니다. 기존 bypass-2FA 토큰을 즉시 폐기하는 기능은 아니므로, 먼저 파이프라인이 npm stage publish와 승인 절차를 처리할 수 있는지 검증한 뒤 교체해야 합니다.

중요하게도 OIDC와 staged publishing은 양자택일이 아닙니다. trusted publisher에도 stage-only 권한을 적용해 장기 토큰을 없애면서 사람의 2FA 승인까지 요구할 수 있습니다. 공식 문서는 지원되는 환경에서 이 조합과 전통적인 토큰 접근 제한을 함께 권합니다. 새 stage-only 토큰을 발표한 9월 18일과 staged publishing 자체의 도입 시점을 혼동해서도 안 됩니다.

전환 일정도 구분해야 합니다. 9월 18일 발표는 bypass-2FA 토큰의 직접 배포 제거 목표를 2027년 1월로 안내합니다. 지금 모든 기존 토큰이 자동으로 stage-only가 됐다는 뜻은 아닙니다. 운영 전에는 최신 공식 일정과 계정 설정을 다시 확인해야 합니다.

다음은 공식 기능을 실제 운영에 적용하기 위한 편집상 권고입니다. GitHub Actions에서는 자동 단계와 수동 승인을 분리하는 것이 핵심입니다. 자동 job에는 stage-only 토큰만 넣고, 승인자는 staging 결과를 확인한 뒤 npmjs.com 또는 승인 가능한 CLI 환경에서 2FA를 수행합니다. 승인 job에 같은 토큰을 재사용하지 말고, 누가 무엇을 승인했는지 CI와 npm 쪽 기록을 함께 남겨야 합니다.

도입 전에 확인할 네 가지

첫째, 현재 workflow에서 실제로 npm publish를 호출하는 위치를 찾습니다. package.json script, reusable workflow, release action 안에 호출이 숨어 있을 수 있습니다. 둘째, stage가 성공한 뒤 사람이 검토할 수 있는 링크와 artifact를 남깁니다. tarball의 해시, 버전, 변경 로그, provenance를 한 화면에서 확인할 수 있어야 합니다.

셋째, staging 성공과 공개 완료를 서로 다른 상태로 기록합니다. STAGED_PENDING_APPROVALPUBLISHED로 바꾸는 주체는 승인 단계여야 합니다. 네트워크 timeout 뒤에 자동으로 npm publish를 다시 호출해서는 안 됩니다. 먼저 staging 목록과 npm package metadata를 읽고 같은 버전이 이미 처리됐는지 확인해야 합니다.

넷째, 만료·IP 제한·패키지 범위를 작은 테스트 패키지에서 검증합니다. stage-only 토큰이 직접 npm publish를 거부하는지, npm stage publish는 허용하는지, 승인 후에만 공개 버전이 보이는지를 각각 테스트해야 합니다. 토큰이 있다고 해서 사용자가 가진 권한보다 더 큰 권한을 얻는 것도 아닙니다.

결론

stage-only 토큰의 장점은 자동화를 멈추지 않고 마지막 공개 권한만 떼어 놓는 데 있습니다. 이미 trusted publishing을 쓸 수 있다면 장기 토큰을 없애는 편이 더 낫습니다. 그렇지 않다면 stage-only를 이용해 CI의 역할을 "배포"에서 "검토 가능한 제출"로 낮추고, 사람의 2FA 승인을 별도 경계로 둘 수 있습니다.

중요한 것은 토큰 종류를 바꾸는 명령 하나가 아닙니다. staging 완료, 사람 검토, 공개 승인이라는 세 상태를 시스템과 팀 문서에서 실제로 분리하는 일입니다. 이 경계가 없으면 새 토큰을 도입해도 어느 순간 자동화가 다시 publish 권한을 요구하게 됩니다.

참고 자료

댓글

이 블로그의 인기 게시물

체크아웃은 분리됐는데 Git 상태는 공유된다: Codex 0.154 worktree의 세 경계

잠근 작업이 커밋 뒤 다시 나온다: PostgreSQL 에이전트 큐의 소유권 설계

코딩 에이전트는 API 문서보다 AGENTS.md를 먼저 읽었다