<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Trusted Publishing on jyukki's Blog</title><link>https://jyukki.com/tags/trusted-publishing/</link><description>Recent content in Trusted Publishing on jyukki's Blog</description><generator>Hugo -- 0.147.0</generator><language>ko-kr</language><lastBuildDate>Fri, 10 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://jyukki.com/tags/trusted-publishing/index.xml" rel="self" type="application/rss+xml"/><item><title>2026 개발 트렌드: npm v12 Install-Time Trust Gate, 패키지 설치가 실행 허가 계약으로 바뀐다</title><link>https://jyukki.com/posts/2026-07-10-npm-v12-install-time-trust-gate-trend/</link><pubDate>Fri, 10 Jul 2026 10:06:00 +0900</pubDate><guid>https://jyukki.com/posts/2026-07-10-npm-v12-install-time-trust-gate-trend/</guid><description>npm v12의 install script 기본 차단, Git/remote dependency opt-in, 2FA-bypass token 축소를 바탕으로 패키지 설치 시점의 신뢰 경계가 어떻게 바뀌는지 정리합니다.</description><content:encoded><![CDATA[<p>2026년 7월 8일 GitHub Changelog에서 npm v12가 <code>latest</code>로 올라왔고, install-time security 기본값이 실제로 켜졌습니다. 핵심은 간단합니다. 의존성의 <code>preinstall</code>, <code>install</code>, <code>postinstall</code> lifecycle script와 implicit <code>node-gyp</code> build는 더 이상 자동 실행되지 않습니다. Git dependency와 remote URL dependency도 기본적으로 resolve되지 않고, 프로젝트가 명시적으로 허용해야 합니다. 동시에 npm은 2FA-bypass granular access token의 민감 작업 권한을 단계적으로 줄이고, 자동 publish는 trusted publishing 또는 staged publishing으로 옮기라고 안내하고 있습니다.</p>
<p>이 변화는 JavaScript 생태계의 사소한 breaking change가 아닙니다. 패키지 설치가 &ldquo;다운로드 + 자동 실행&quot;이던 시대에서, &ldquo;다운로드와 실행 허가를 분리하는 시대&quot;로 넘어가는 신호입니다. 저는 이 흐름을 <strong>Install-Time Trust Gate</strong>라고 부르는 편이 맞다고 봅니다. dependency update, CI build, local bootstrap, package publish가 모두 같은 공급망 경계 안에 들어왔기 때문입니다.</p>
<p>이 글은 <a href="/posts/2026-05-12-package-release-quarantine-gate-trend/">Package Release Quarantine Gate</a>, <a href="/posts/2026-05-07-dependency-update-pipeline-trend/">Dependency Update Pipeline</a>, <a href="/posts/2026-03-08-ai-code-provenance-and-sbom-trend/">AI Code Provenance와 SBOM</a>, <a href="/learning/deep-dive/deep-dive-cicd-security-supply-chain/">CI/CD 보안과 공급망</a>과 이어집니다. 이전 글들이 &ldquo;새 패키지를 받아도 되는가&quot;와 &ldquo;릴리스가 믿을 만한가&quot;를 다뤘다면, 이번 글은 &ldquo;설치 중 어떤 코드가 실행될 자격이 있는가&quot;를 다룹니다.</p>
<h2 id="이-글에서-얻는-것">이 글에서 얻는 것</h2>
<ul>
<li>npm v12의 install-time security 기본값이 실제 운영 경계에서 무엇을 바꾸는지 이해합니다.</li>
<li>install script allowlist를 코드 리뷰, CI 정책, dependency update pipeline에 연결하는 기준을 잡을 수 있습니다.</li>
<li>native module, browser automation tool, Electron, Git dependency가 많은 프로젝트의 마이그레이션 순서를 정리합니다.</li>
<li>2FA-bypass token 축소와 trusted publishing 전환을 package release 운영 관점으로 해석합니다.</li>
</ul>
<h2 id="핵심-개념이슈">핵심 개념/이슈</h2>
<h3 id="1-패키지-설치는-이미-실행-표면이었다">1) 패키지 설치는 이미 실행 표면이었다</h3>
<p><code>npm install</code>은 오랫동안 &ldquo;의존성을 받는 명령&quot;처럼 보였지만, 실제로는 dependency tree 안의 코드가 개발자 장비와 CI runner에서 실행될 수 있는 표면이었습니다. lifecycle script는 native module build, browser binary download, hook 설치, code generation에 유용했습니다. 문제는 유용한 실행 경로와 악성 실행 경로가 같은 문을 쓴다는 점입니다.</p>
<p>npm v12의 변화는 이 문을 기본적으로 닫습니다.</p>
<table>
  <thead>
      <tr>
          <th>대상</th>
          <th>v12 기본값</th>
          <th>의미</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>dependency lifecycle scripts</td>
          <td>승인 없으면 실행 안 함</td>
          <td>transitive dependency script 실행 차단</td>
      </tr>
      <tr>
          <td>implicit <code>node-gyp</code> build</td>
          <td>승인 필요</td>
          <td>native addon도 allowlist 대상</td>
      </tr>
      <tr>
          <td>Git dependency</td>
          <td><code>--allow-git=none</code></td>
          <td>Git source resolve를 명시 허가로 전환</td>
      </tr>
      <tr>
          <td>remote URL dependency</td>
          <td><code>--allow-remote=none</code></td>
          <td>HTTPS tarball 같은 비 registry 경로 차단</td>
      </tr>
      <tr>
          <td>approve/deny scripts</td>
          <td><code>package.json</code> allowlist</td>
          <td>실행 허가가 코드 리뷰 대상이 됨</td>
      </tr>
  </tbody>
</table>
<p>여기서 중요한 것은 npm이 모든 공급망 공격을 해결했다는 뜻이 아니라는 점입니다. typosquatting, compromised maintainer, malicious runtime code, dependency confusion은 여전히 남습니다. 다만 <strong>설치 시점에 자동 실행되는 가장 편한 경로</strong>가 기본 차단으로 바뀌었습니다. 공격면이 사라진 것이 아니라, 공격자가 다른 단계에서 실행 허가를 얻어야 하는 구조가 된 것입니다.</p>
<h3 id="2-allowlist는-보안-설정이-아니라-소스-코드-계약이다">2) allowlist는 보안 설정이 아니라 소스 코드 계약이다</h3>
<p>GitHub의 안내는 <code>npm approve-scripts --allow-scripts-pending</code>으로 승인 후보를 보고, 신뢰하는 script를 승인한 뒤 결과 allowlist를 <code>package.json</code>에 커밋하라고 설명합니다. 이 지점이 핵심입니다. 실행 허가가 개인 노트북의 로컬 설정이 아니라 repository 안의 계약이 됩니다.</p>
<p>좋은 allowlist에는 최소 네 가지 정보가 따라야 합니다.</p>
<ul>
<li>어떤 package script를 허용하는가</li>
<li>왜 필요한가</li>
<li>어떤 버전 또는 범위까지 허용하는가</li>
<li>누가 재검토할 책임을 가지는가</li>
</ul>
<p>실무 기준은 아래처럼 잡을 수 있습니다.</p>
<table>
  <thead>
      <tr>
          <th>패키지 유형</th>
          <th>허용 판단</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>native module (<code>sharp</code>, <code>bcrypt</code>, DB driver 등)</td>
          <td>필요성 높음, version pinning과 rebuild 환경 고정</td>
      </tr>
      <tr>
          <td>browser binary download (<code>playwright</code>, <code>puppeteer</code>, <code>cypress</code>)</td>
          <td>explicit install command 분리 검토</td>
      </tr>
      <tr>
          <td>Electron</td>
          <td>앱 build pipeline owner 승인 필요</td>
      </tr>
      <tr>
          <td>Husky/git hooks</td>
          <td>install script 대신 명시 bootstrap으로 이동 가능</td>
      </tr>
      <tr>
          <td>unknown transitive script</td>
          <td>기본 보류, owner 확인 전 deny 후보</td>
      </tr>
      <tr>
          <td>Git/remote URL dependency</td>
          <td>registry package나 vendored source로 대체 우선</td>
      </tr>
  </tbody>
</table>
<p>무조건 하나씩 토론하다가 rollout이 멈추는 것도 위험합니다. 기존 프로젝트는 먼저 현재 tree를 snapshot approve하고, 새로 등장하는 script를 막는 방식이 현실적입니다. 그다음 2~4주 동안 불필요한 script를 <code>deny-scripts</code>로 줄이면 됩니다.</p>
<h3 id="3-warning-only와-fail-closed-사이에-정책이-필요하다">3) warning-only와 fail-closed 사이에 정책이 필요하다</h3>
<p>npm v12의 기본 동작은 모든 unapproved script 때문에 install을 즉시 실패시키는 방식이 아니라, script를 skip하고 warning을 남기는 쪽입니다. 이 기본값은 생태계 충격을 줄이지만, 기업 CI에서는 warning이 쉽게 묻힙니다. 따라서 팀은 어디서 fail-closed로 바꿀지 정해야 합니다.</p>
<p>권장 정책:</p>
<ul>
<li>local developer install: warning + 링크 + bootstrap 안내</li>
<li>pull request CI: unapproved new script 발견 시 실패</li>
<li>main branch CI: unapproved script 0건 유지</li>
<li>release build: strict allowlist + lockfile diff 검증</li>
<li>production image build: install-time secret 최소화, publish token 없음</li>
</ul>
<p>특히 release build runner가 cloud credential, package publish 권한, deploy key를 가진다면 warning-only는 부족합니다. 설치 중 실행되는 코드는 그 권한을 읽거나 네트워크로 보낼 수 있습니다. <a href="/posts/2026-07-04-ci-native-agent-runner-actions-token-trend/">CI-native Agent Runner와 Actions Token</a>에서 다룬 것처럼 runner 권한은 작업 편의가 아니라 신뢰 경계입니다.</p>
<h3 id="4-gitremote-dependency-차단은-비표준-경로를-드러낸다">4) Git/remote dependency 차단은 &ldquo;비표준 경로&quot;를 드러낸다</h3>
<p>많은 팀이 <code>package.json</code>에 Git URL이나 HTTPS tarball을 잠깐 넣고 잊습니다. hotfix, fork, private patch, vendor SDK 미공개 버전 같은 이유입니다. npm v12는 이 경로도 기본 차단합니다. 이유는 단순합니다. registry package와 달리 Git/remote dependency는 provenance, review, caching, integrity, lifecycle script 정책이 흐려지기 쉽습니다.</p>
<p>의사결정 기준은 아래처럼 둡니다.</p>
<ul>
<li>임시 fork가 2주 이상 유지된다 → 내부 registry package로 publish하거나 upstream PR 계획을 세운다.</li>
<li>remote tarball이 production build에 들어간다 → checksum, mirror, owner, 만료일을 기록한다.</li>
<li>Git dependency가 transitive로 들어온다 → 직접 dependency owner에게 registry release를 요구한다.</li>
<li>보안 패치 때문에 급히 쓴다 → exception 만료일 30일 이하와 follow-up issue를 둔다.</li>
</ul>
<p>이 기준은 <a href="/posts/2026-05-07-dependency-update-pipeline-trend/">Dependency Update Pipeline</a>과 맞물립니다. 업데이트 자동화가 dependency를 올리는 것만 보고, 설치 경로가 registry인지 Git인지 remote URL인지 보지 않으면 중요한 신뢰 정보를 놓칩니다.</p>
<h3 id="5-publish-권한도-장기-토큰에서-승인-흐름으로-이동한다">5) Publish 권한도 장기 토큰에서 승인 흐름으로 이동한다</h3>
<p>이번 발표에는 install-time security만 있는 것이 아닙니다. npm은 2FA-bypass GAT가 민감한 account/package/org 관리 작업에서 2FA를 건너뛰지 못하게 하고, 이후에는 direct publish 권한도 줄여 staged publishing과 trusted publishing으로 이동하라고 안내했습니다.</p>
<p>흐름은 분명합니다.</p>
<ul>
<li>설치 시점: dependency script 자동 실행을 줄인다.</li>
<li>배포 시점: 장기 publish token의 직접 공개 권한을 줄인다.</li>
<li>승인 시점: 사람 2FA, OIDC trusted publishing, staged approval을 결합한다.</li>
</ul>
<p>이제 package release는 &ldquo;CI가 토큰으로 publish&quot;가 아니라 &ldquo;어떤 workflow가 어떤 identity로 어떤 package를 어떤 승인 뒤 publish하는가&quot;가 되어야 합니다. <a href="/posts/2026-05-12-package-release-quarantine-gate-trend/">Package Release Quarantine Gate</a>에서 말한 격리·검증 기준이 npm v12 이후에는 더 자연스러운 기본 운영 모델이 됩니다.</p>
<h2 id="실무-적용">실무 적용</h2>
<h3 id="1-현재-install-script-표면을-snapshot한다">1) 현재 install script 표면을 snapshot한다</h3>
<p>첫 주 목표는 완벽한 차단이 아니라 보이는 상태를 만드는 것입니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>npm --version
</span></span><span style="display:flex;"><span>npm install
</span></span><span style="display:flex;"><span>npm approve-scripts --allow-scripts-pending
</span></span></code></pre></div><p>결과를 아래 표로 정리합니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#ff79c6">install_script_inventory</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">project</span>: web-console
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">npm_min_version_checked</span>: <span style="color:#f1fa8c">&#34;11.16.0&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">packages</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#ff79c6">name</span>: sharp
</span></span><span style="display:flex;"><span>      <span style="color:#ff79c6">reason</span>: image transform native addon
</span></span><span style="display:flex;"><span>      <span style="color:#ff79c6">owner</span>: platform-web
</span></span><span style="display:flex;"><span>      <span style="color:#ff79c6">decision</span>: approve_pinned
</span></span><span style="display:flex;"><span>    - <span style="color:#ff79c6">name</span>: playwright
</span></span><span style="display:flex;"><span>      <span style="color:#ff79c6">reason</span>: browser binary download
</span></span><span style="display:flex;"><span>      <span style="color:#ff79c6">owner</span>: qa-platform
</span></span><span style="display:flex;"><span>      <span style="color:#ff79c6">decision</span>: move_to_explicit_install
</span></span><span style="display:flex;"><span>    - <span style="color:#ff79c6">name</span>: unknown-transitive-package
</span></span><span style="display:flex;"><span>      <span style="color:#ff79c6">reason</span>: not understood
</span></span><span style="display:flex;"><span>      <span style="color:#ff79c6">owner</span>: dependency-review
</span></span><span style="display:flex;"><span>      <span style="color:#ff79c6">decision</span>: deny_or_replace
</span></span></code></pre></div><p>처음부터 모든 package를 보안팀이 직접 판단하려 하면 속도가 안 납니다. native module, browser tool, Electron, Git hooks, unknown transitive script로 나누고 owner를 붙이는 편이 빠릅니다.</p>
<h3 id="2-ci-정책을-세-단계로-나눈다">2) CI 정책을 세 단계로 나눈다</h3>
<p>1단계는 관측입니다. PR마다 allowlist diff와 pending script 목록을 comment나 artifact로 남깁니다.</p>
<p>2단계는 신규 script 차단입니다. 기존 allowlist는 인정하되, PR에서 새 install script가 추가되면 실패시킵니다.</p>
<p>3단계는 release strict 모드입니다. main/release branch에서는 unapproved script, Git dependency, remote URL dependency를 모두 실패시킵니다.</p>
<p>운영 숫자:</p>
<ul>
<li>unapproved install script: release branch 0건</li>
<li>Git/remote dependency exception: 30일 이하 만료</li>
<li>install allowlist owner 없는 항목: 0건</li>
<li>dependency bot PR 중 allowlist 변경 포함 비율: 주간 리뷰</li>
<li>CI install warning 미분류: 7일 이상 방치 금지</li>
</ul>
<p>이 수치를 <a href="/posts/2026-06-25-code-quality-policy-gate-trend/">Code Quality Policy Gate</a>처럼 조직 정책으로 연결하면 좋습니다. 단순히 &ldquo;보안 옵션을 켰다&quot;가 아니라 &ldquo;새 실행 경로가 들어오면 리뷰가 필요하다&quot;는 gate가 됩니다.</p>
<h3 id="3-개발자-경험을-망치지-않는-bootstrap-경로를-제공한다">3) 개발자 경험을 망치지 않는 bootstrap 경로를 제공한다</h3>
<p>보안 기본값이 바뀌면 현장은 &ldquo;설치가 깨졌다&quot;로 느낍니다. 특히 Playwright, Cypress, Puppeteer, Electron, native addon을 쓰는 팀은 불편을 바로 맞습니다. 그래서 개발자 안내는 짧고 실행 가능해야 합니다.</p>
<p>좋은 안내:</p>
<ul>
<li><code>npm install</code> 뒤 어떤 warning이 정상인지 보여준다.</li>
<li>browser binary download는 <code>npm run setup:browsers</code>처럼 explicit command로 분리한다.</li>
<li>native module build 실패 시 필요한 OS package와 Node version을 안내한다.</li>
<li>allowlist 변경 PR에는 owner와 이유를 요구한다.</li>
<li>모르는 transitive script는 임시 승인보다 issue 생성으로 보낸다.</li>
</ul>
<p>나쁜 안내:</p>
<ul>
<li>&ldquo;보안상 알아서 승인하세요&rdquo;</li>
<li>&ldquo;warning은 무시해도 됩니다&rdquo;</li>
<li>모든 프로젝트에 같은 allowlist를 복사</li>
<li>CI에서만 엄격하고 로컬은 문서 없음</li>
</ul>
<p>Install-Time Trust Gate는 개발자 경험과 충돌할 수 있지만, 충돌을 줄이는 해법은 차단을 푸는 것이 아니라 explicit bootstrap을 제공하는 것입니다.</p>
<h3 id="4-publish-권한을-같이-정리한다">4) publish 권한을 같이 정리한다</h3>
<p>install script만 정리하고 publish token을 방치하면 절반짜리 개선입니다. 릴리스 workflow에서 아래를 같이 점검합니다.</p>
<ul>
<li>npm publish가 장기 토큰으로 직접 실행되는가?</li>
<li>trusted publishing OIDC subject가 repo/branch/tag 기준으로 제한되는가?</li>
<li>staged publishing 또는 human 2FA approval이 필요한 package는 무엇인가?</li>
<li>package maintainer 변경, access 변경, token 생성이 2FA 없는 자동화로 가능한가?</li>
<li>release workflow가 외부 PR artifact나 cache를 신뢰하지 않는가?</li>
</ul>
<p>이 점검은 <a href="/posts/2026-04-22-third-party-oauth-supply-chain-trend/">Third-party OAuth 공급망</a>과도 연결됩니다. 패키지 publish 권한은 단순 secret이 아니라 생태계에 코드를 배포하는 권한입니다.</p>
<h3 id="5-pr-리뷰와-릴리스-증거를-분리한다">5) PR 리뷰와 릴리스 증거를 분리한다</h3>
<p>Install-Time Trust Gate를 도입할 때 자주 생기는 실수는 모든 검증을 하나의 CI job에 밀어 넣는 것입니다. PR에서는 &ldquo;새 실행 경로가 생겼는가&quot;를 빠르게 판정해야 하고, release에서는 &ldquo;승인된 실행 경로만 실제로 실행되는가&quot;를 더 엄격하게 봐야 합니다. 둘을 같은 기준으로 묶으면 PR은 느려지고, release는 느슨해집니다.</p>
<p>PR 단계의 증거는 작고 명확해야 합니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#ff79c6">dependency_execution_review</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">lockfile_changed</span>: <span style="color:#ff79c6">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">package_json_allowlist_changed</span>: <span style="color:#ff79c6">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">new_lifecycle_scripts</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#ff79c6">package</span>: <span style="color:#f1fa8c">&#34;@vendor/native-image&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#ff79c6">script</span>: <span style="color:#f1fa8c">&#34;install&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#ff79c6">owner</span>: <span style="color:#f1fa8c">&#34;platform-web&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#ff79c6">reason</span>: <span style="color:#f1fa8c">&#34;native binary build&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#ff79c6">decision</span>: <span style="color:#f1fa8c">&#34;approve_pinned&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">git_or_remote_dependencies</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#ff79c6">package</span>: <span style="color:#f1fa8c">&#34;internal-sdk&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#ff79c6">source</span>: <span style="color:#f1fa8c">&#34;git&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#ff79c6">expires_at</span>: <span style="color:#f1fa8c">&#34;2026-08-09&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#ff79c6">replacement</span>: <span style="color:#f1fa8c">&#34;publish scoped registry package&#34;</span>
</span></span></code></pre></div><p>이 정도면 reviewer가 봐야 할 질문이 분명해집니다. &ldquo;버전이 올라갔는가&quot;가 아니라 &ldquo;새 코드가 설치 중 실행될 권한을 얻었는가&rdquo;, &ldquo;registry가 아닌 경로가 들어왔는가&rdquo;, &ldquo;예외 만료일이 있는가&quot;입니다. dependency bot이 만든 PR도 같은 방식으로 봐야 합니다. 봇이 lockfile만 바꾼 것이 아니라 install allowlist까지 바꿨다면 그 PR은 보안·플랫폼 owner의 리뷰를 타야 합니다.</p>
<p>release 단계에서는 증거가 더 엄격해야 합니다.</p>
<ul>
<li><code>npm ci</code> 또는 동등한 clean install에서 pending script가 0건인지 확인한다.</li>
<li>allowlist가 변경된 commit에는 owner와 사유가 붙어 있는지 확인한다.</li>
<li>Git/remote dependency exception이 만료되지 않았는지 확인한다.</li>
<li>release runner에 npm publish token, cloud admin credential, deploy key가 동시에 노출되지 않는지 확인한다.</li>
<li>install warning을 artifact로 남기되, release branch에서는 warning을 실패 조건으로 승격한다.</li>
</ul>
<p>이렇게 분리하면 developer experience를 너무 망치지 않으면서도 release 경계는 닫을 수 있습니다. 로컬과 PR은 관측과 빠른 피드백, release는 실패 조건과 감사 증거를 맡는 구조입니다.</p>
<h3 id="6-롤백은-보안-해제가-아니라-예외-축소로-설계한다">6) 롤백은 보안 해제가 아니라 예외 축소로 설계한다</h3>
<p>엄격 모드를 켰다가 build가 깨지면 가장 쉬운 롤백은 전체 차단을 끄는 것입니다. 하지만 그렇게 하면 도입 이유가 사라집니다. 더 나은 롤백은 범위를 줄이는 것입니다.</p>
<p>예를 들어 release job에서 unapproved script 때문에 실패했다면 아래 순서로 되돌립니다.</p>
<ol>
<li>해당 package가 직접 dependency인지 transitive dependency인지 확인한다.</li>
<li>native build, browser download, git hook, unknown script 중 어느 그룹인지 분류한다.</li>
<li>필요한 경우 특정 package만 임시 approve하고, 만료일과 owner를 적는다.</li>
<li>Git/remote dependency라면 registry package 전환 issue를 같이 만든다.</li>
<li>실패한 CI log와 승인 diff를 release note나 운영 기록에 남긴다.</li>
</ol>
<p>롤백 문서에는 &ldquo;검증을 껐다&quot;가 아니라 &ldquo;어떤 실행 경로를 얼마 동안 예외로 열었다&quot;가 남아야 합니다. 이 차이가 중요합니다. 전자는 보안 설정을 원점으로 돌리는 것이고, 후자는 운영 계약을 좁게 예외 처리하는 것입니다.</p>
<p>간단한 exception ledger는 아래처럼 충분합니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#ff79c6">install_time_exceptions</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#ff79c6">package</span>: <span style="color:#f1fa8c">&#34;playwright&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">script</span>: <span style="color:#f1fa8c">&#34;install&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">allowed_until</span>: <span style="color:#f1fa8c">&#34;2026-08-10&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">owner</span>: <span style="color:#f1fa8c">&#34;qa-platform&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">reason</span>: <span style="color:#f1fa8c">&#34;browser binary download used by e2e tests&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">follow_up</span>: <span style="color:#f1fa8c">&#34;move browser install to explicit setup job&#34;</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#ff79c6">package</span>: <span style="color:#f1fa8c">&#34;internal-sdk&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">source</span>: <span style="color:#f1fa8c">&#34;git&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">allowed_until</span>: <span style="color:#f1fa8c">&#34;2026-07-31&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">owner</span>: <span style="color:#f1fa8c">&#34;platform-api&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">reason</span>: <span style="color:#f1fa8c">&#34;security hotfix before registry publish&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">follow_up</span>: <span style="color:#f1fa8c">&#34;publish @company/internal-sdk 4.8.2&#34;</span>
</span></span></code></pre></div><p>예외가 ledger에 들어가면 dependency update pipeline과 연결할 수 있습니다. 30일이 지난 Git dependency는 자동으로 알림을 보내고, owner 없는 install script는 release gate에서 실패시키며, 반복 승인되는 package는 별도 bootstrap command로 분리합니다.</p>
<h2 id="트레이드오프주의점">트레이드오프/주의점</h2>
<p>첫째, npm v12는 공급망 보안의 끝이 아닙니다. 설치 script가 막혀도 package runtime code는 애플리케이션 안에서 실행됩니다. 악성 dependency가 import된 뒤 credential을 읽거나 네트워크 호출을 하는 위험은 여전히 남습니다. 따라서 SBOM, lockfile review, runtime permission, egress policy와 같이 봐야 합니다.</p>
<p>둘째, allowlist를 너무 넓게 잡으면 효과가 줄어듭니다. <code>--all</code>로 현재 tree를 승인하는 것은 rollout 시작점으로는 현실적이지만, 그것을 영구 정책으로 방치하면 &ldquo;새 script만 막는&rdquo; 수준에 머뭅니다. 2~4주 안에 불필요 script를 줄이는 follow-up이 필요합니다.</p>
<p>셋째, strict 정책을 너무 빨리 전체 적용하면 생산성이 깨질 수 있습니다. 특히 native addon과 browser automation 도구가 많은 프로젝트는 install 경로가 복잡합니다. 우선 release branch, production image, publish workflow부터 엄격하게 하고, 로컬 개발은 명확한 warning과 setup command를 제공하는 편이 낫습니다.</p>
<p>넷째, Git/remote dependency 예외는 계속 부채가 됩니다. 보안 패치 때문에 어쩔 수 없이 쓰더라도 owner, 만료일, checksum, 대체 계획이 없으면 몇 달 뒤 아무도 기억하지 못하는 공급망 구멍이 됩니다.</p>
<h2 id="체크리스트-또는-연습">체크리스트 또는 연습</h2>
<h3 id="체크리스트">체크리스트</h3>
<ul>
<li><input disabled="" type="checkbox"> npm 11.16.0+ 또는 v12에서 install script pending 목록을 확인했다.</li>
<li><input disabled="" type="checkbox"> allowlist 변경은 <code>package.json</code> diff로 코드 리뷰된다.</li>
<li><input disabled="" type="checkbox"> release branch CI는 unapproved script, Git dependency, remote URL dependency를 차단한다.</li>
<li><input disabled="" type="checkbox"> native module과 browser binary download는 owner와 이유가 문서화되어 있다.</li>
<li><input disabled="" type="checkbox"> 장기 npm publish token을 trusted publishing 또는 staged publishing으로 전환하는 계획이 있다.</li>
<li><input disabled="" type="checkbox"> dependency bot PR에서 install allowlist 변경이 생기면 보안/플랫폼 owner가 리뷰한다.</li>
</ul>
<h3 id="연습-과제">연습 과제</h3>
<ol>
<li>프로젝트 하나를 골라 <code>npm approve-scripts --allow-scripts-pending</code> 결과를 <code>native build</code>, <code>browser download</code>, <code>git hook</code>, <code>unknown transitive</code> 네 그룹으로 분류해 보세요.</li>
<li>release workflow에서 <code>npm install</code>이 실행되는 시점에 어떤 secret과 token이 환경에 있는지 적어 보세요. 설치 중 실행 코드가 볼 수 있는 값이 곧 위험 표면입니다.</li>
<li>Git/remote URL dependency가 하나 있다고 가정하고, 30일 만료 예외 문서와 registry package 전환 계획을 10줄로 작성해 보세요.</li>
</ol>
<h2 id="출처-링크">출처 링크</h2>
<ul>
<li><a href="https://github.blog/changelog/2026-07-08-npm-install-time-security-and-gat-bypass2fa-deprecation/">GitHub Changelog: npm install-time security and GAT bypass2fa deprecation</a></li>
<li><a href="https://github.blog/changelog/2026-06-09-upcoming-breaking-changes-for-npm-v12/">GitHub Changelog: Upcoming breaking changes for npm v12</a></li>
<li><a href="https://github.com/orgs/community/discussions/198547">GitHub Community: Preparing for npm v12</a></li>
</ul>
<h2 id="관련-글">관련 글</h2>
<ul>
<li><a href="/posts/2026-05-12-package-release-quarantine-gate-trend/">Package Release Quarantine Gate</a></li>
<li><a href="/posts/2026-05-07-dependency-update-pipeline-trend/">Dependency Update Pipeline</a></li>
<li><a href="/posts/2026-03-08-ai-code-provenance-and-sbom-trend/">AI Code Provenance와 SBOM</a></li>
<li><a href="/learning/deep-dive/deep-dive-cicd-security-supply-chain/">CI/CD 보안과 공급망</a></li>
<li><a href="/posts/2026-07-04-ci-native-agent-runner-actions-token-trend/">CI-native Agent Runner와 Actions Token</a></li>
</ul>
]]></content:encoded></item></channel></rss>