주요 내용
- npm 프로비넌스는 특정 패키지가 어떤 저장소, 커밋 및 워크플로우에서 생성되었는지를 확인해 줄 뿐, 해당 코드를 실행해도 안전하다는 것을 보장하지는 않습니다.
- ‘인증 격차’란, 출처 정보가 입증하는 내용과 무언가를 설치하기 전에 실제로 알아야 할 사항 사이의 차이를 말합니다.
- 워크플로가 탈취되거나 악의적인 커밋이 이루어지더라도 여전히 유효한 출처 정보가 생성되며, 대부분의 npm 패키지에는 출처 정보가 전혀 포함되어 있지 않습니다.
- 보안 강화된 환경에서 검증된 소스 코드를 바탕으로 패키지를 재빌드하면, 신뢰의 근원이 게시자의 파이프라인에서 벗어나게 됩니다.
최근 npmjs.com을 둘러보셨다면 아마 눈치채셨을 겁니다. 많은 패키지 페이지 하단 부근에 이제 ‘출처(provenance)’ 섹션이 추가되어, 해당 패키지가 GitHub Actions에서 빌드 및 서명되었다는 내용과 함께 원본 커밋으로 바로 연결되는 링크가 표시됩니다. 마치 인증 마크처럼 보입니다. 문제가 해결된 듯한 느낌이 듭니다.
많은 팀이 그 인증 마크를 마치 결승선처럼 여기는데, 솔직히 그 심정은 이해가 갑니다. Provenance는 생태계에 있어 진정한 진전이었습니다. 하지만 이 기술은 대부분의 사람들이 생각하는 것보다 훨씬 좁은 범위의 질문에만 답을 주고 있으며, 이 기술이 답하는 질문과 여러분이 실제로 중요하게 여기는 질문 사이의 간극이야말로 최근 발생한 수많은 npm 공격이 바로 그 틈새를 노리고 있었던 곳입니다.
npm 프로비넌스가 실제로 무엇을 입증하는가
npm은 2023년 4월에 프로비넌스(provenance) 기능을 도입했으며, 2025년 7월 ‘신뢰할 수 있는 게시(trusted publishing )’ 기능이 도입되면서 이 과정이 거의 자동화되었습니다. 유지보수자가 GitHub Actions를 통해 신뢰할 수 있는 게시 기능을 사용하여 게시할 때, npm CLI는 별도의 플래그 설정 없이도 기본적으로 프로비넌스 증명서를 생성합니다. 이 증명서는 서명된 SLSA 진술서로, Sigstore의 공개 투명성 로그에 기록되며, 타르볼을 소스 저장소, 특정 커밋 및 이를 빌드한 워크플로우와 연결해 줍니다. 사용자 입장에서는 `npm audit signatures` 명령어를 통해 설치된 전체 디렉터리 트리에서 이러한 증명서를 확인할 수 있습니다.
알아두면 유용한 정보들이네요. 프로비넌스 기능이 도입되기 전에는, 패키지 페이지의 저장소 링크가 관리자가 직접 입력한 내용 그대로였습니다. package.json. 이를 확인해 준 것도, 특정 커밋에 연결해 준 것도 없었습니다. 게다가 npm publish는 게시자의 작업 디렉터리에 있는 모든 파일을 그대로 묶어 올리기 때문에, 타르볼에는 빌드 결과물이나 불필요한 파일은 물론, git에 아예 존재하지 않았던 훨씬 더 심각한 내용이 포함될 수도 있습니다. 프로비넌스(provenance)를 사용하는 패키지의 경우, 이러한 보안 취약점을 해결해 줍니다. 타르볼이 특정 커밋을 대상으로 실행된 특정 CI 실행 결과물이라는 것을 알 수 있기 때문입니다.
당신이 모르는 것은, 그 행보 속에 당신의 신뢰를 받을 만한 것이 과연 있었는지 여부입니다.
인증 격차
이 현상에 이름을 붙이는 것이 도움이 될 것 같으니, 이를 ‘인증 격차’라고 부르겠습니다. 인증 격차란, 출처 인증이 입증하는 내용(어떤 저장소, 커밋, 빌드 워크플로가 패키지를 생성했는지)과, 패키지를 설치하기 전에 실제로 알아야 할 사항, 즉 해당 아티팩트가 안전하게 제작되었으며, 사용자가 안심하고 실행할 수 있는 소스 코드에서 충실하게 빌드되었는지 여부 사이의 차이를 말합니다.
출처(Provenance)는 기원에 대한 주장입니다. 안전성(Safety)은 내용에 대한 주장입니다. 인증이 암호학적으로 완벽하더라도, 코드가 사용자의 시스템에 도착했을 때 어떤 동작을 하는지에 대해서는 아무것도 말해주지 않을 수 있습니다. node_modules 폴더. 공격자가 자신의 코드를 커밋이나 빌드에 포함시키면, 별다른 노력 없이도 유효한 인증서를 얻을 수 있습니다.
소포가 출처 검증을 통과했음에도 불구하고 어떻게 소비자에게 피해를 주는가
가장 간단한 방법은 관리자를 해킹하는 것입니다. 누군가 프로젝트에 대한 푸시 권한이 있는 GitHub 계정을 탈취하면, npm을 건드릴 필요조차 없습니다. 해당 사용자가 커밋을 푸시하면 릴리스 워크플로가 설계된 대로 정확히 실행되며, 새 버전은 공격자의 커밋을 정확히 가리키는 출처 정보를 포함해 배포됩니다. 게시 과정 자체에는 아무런 이상이 없기 때문에 모든 검사가 통과됩니다. 문제는 커밋에 있으며, 출처 정보 시스템은 애초에 커밋을 판단하도록 설계된 것이 아닙니다.
두 번째 방법은 빌드 파이프라인 자체를 거치는 것입니다. ‘신뢰할 수 있는 게시(Trusted publishing)’ 기능은 장기 유효 토큰을 CI 워크플로우에서 생성한 단기 유효 토큰으로 교체해 주는데, 이는 큰 개선점입니다. 또한 이는 워크플로우의 신원이 바로 인증 정보가 된다는 것을 의미합니다. 앤드류 네스빗(Andrew Nesbitt)이 GitHub Actions의 Python 패키지 취약점을 검토한 결과, 공격자가 프로젝트의 실제 워크플로우에서 생성된 유효한 토큰을 보유하게 되는 사례가 발견되었는데, 이는 ‘신뢰할 수 있는 게시’ 기능이 해결할 수 없는 유일한 상황입니다. 생태계는 다르지만, 그 작동 원리는 npm에도 그대로 적용됩니다.
세 번째 경로는 사람들이 흔히 간과하는 부분입니다. 릴리스 빌드는 번들러, 트랜스파일러, 플러그인 등 자체 의존성을 불러오며, 이 모든 것은 CI 러너에서 새로 해결됩니다. 릴리스 작업이 실행될 때 이들 중 하나가 침해당하면, 그 페이로드가 결국 여러분의 dist 폴더를 담아, 출처가 확실한 패키지 안에 넣어 배송합니다. 이 인증서는 해당 커밋의 이름을 명시할 뿐입니다. 해당 타르볼을 생성하는 과정에서 빌드 단계에서 설치된 수백 개의 패키지에 대해서는 보증하지 않습니다.
출처 정보가 누락된 것은 아무도 주목하지 않는 신호입니다
다음 부분이 더 신경 쓰입니다. 출처 정보는 선택적 적용 방식인데, 레지스트리의 대부분이 이를 선택하지 않았습니다. 비공개 소스 저장소에서 빌드된 패키지의 경우 이 기능을 아예 사용할 수조차 없습니다. 그리고 npm install 해당 패키지의 이전 버전에는 모두 인증서가 포함되어 있었더라도, 인증서가 없는 버전을 기꺼이 설치합니다.
이제 대규모 웜 캠페인이 어떻게 작동하는지 생각해 봅시다. Shai-Hulud와 그 뒤를 이은 공격들은 합법적인 패키지를 자동으로 재게시할 수 있도록 npm 토큰과 GitHub 인증 정보를 노렸습니다. 도난당한 토큰을 보유한 사람은 어디서든 패키지를 게시할 수 있는데, 이는 대개 CI 실행이나 게시 인증 절차가 없다는 것을 의미합니다. 이론적으로 이는 명백한 단서입니다. PyPI가 울트라리틱스(Ultralytics) 침해 사건에 대한 분석 보고서를 작성했을 때, 두 번째 악성 배포 물결은 해당 리포지토리의 활동 기록과 게시 인증이 전혀 일치하지 않는다는 점에서 두드러졌다고 지적했습니다.
누군가 그 드롭을 찾고 있다면 그 방법이 통할 수도 있습니다. 하지만 대부분의 npm 환경에서는 아무도 찾지 않습니다. 출처 정보가 갑자기 사라진 패키지는 애초에 출처 정보가 없었던 수천 개의 패키지와 전혀 다를 바가 없어 보이며, 설치 과정은 아무런 문제 없이 진행됩니다.
소스 코드부터 다시 구축하면 또 다른 격차를 해소할 수 있다
출처 정보가 어떤 것이 어디서 빌드되었는지를 알려준다면, 다음으로 자연스럽게 떠오르는 질문은 직접 빌드해도 동일한 결과를 얻을 수 있는지 여부일 것입니다. 이것이 바로 ‘독립적 재빌드’의 핵심 개념입니다. 배포자가 업로드한 타르볼을 그대로 신뢰하기보다는, 배포자가 출처라고 밝힌 소스 코드를 가져와 자신이 통제하는 환경에서 다시 빌드한 뒤, 그 결과물을 설치할 대상으로 삼는 것입니다. 이로써 배포자의 빌드 파이프라인은 더 이상 신뢰의 근원이 되지 않습니다.
이것이 바로 우리가 SecureChain에서 채택한 접근 방식입니다. 패키지는 검증된 소스 코드를 바탕으로, 보안이 강화되고 격리된 환경에서 재빌드되며, 상속된 바이너리나 불투명한 업스트림 아티팩트가 전혀 포함되지 않습니다. 또한 각 패키지는 알려진 악성코드 및 의심스러운 코드 패턴에 대해 검사를 거치며, 검사에 통과하지 못한 항목은 개발자의 노트북이나 CI 실행기에 도달하기 전에 차단됩니다. 이 솔루션은 공개 npm 레지스트리를 그대로 대체할 수 있으므로, npm, Yarn, pnpm 모두 팀이 기대하는 대로 작동하며, 이미 패키지를 프록시하는 방식으로 Artifactory, Nexus 또는 GitHub Packages를 사용하고 있다면 해당 서비스의 업스트림에 배치할 수도 있습니다.
SecureChain은 SLSA 레벨 3 출처 정보도 제공하는데, 이는 마치 제가 제 주장을 스스로 반박하는 것처럼 보일 수도 있습니다. 그렇지 않습니다. 출처 정보는 그것이 설명하는 빌드만큼만 의미가 있습니다. 업스트림 출처 정보는 유지보수자의 CI가 패키지를 생성했음을 알려주지만, 보안이 침해된 CI도 문제없이 패키지를 생성할 수 있습니다. 독립적인 재빌드에 첨부된 출처 정보는 해당 아티팩트가 강화된 빌드 환경에서, 투입되기 전에 검증된 소스 코드를 바탕으로 생성되었음을 알려줍니다. 두 경우 모두 동일한 SLSA 형식이지만, 완전히 다른 빌드를 보증하고 있는 것입니다.
재건만으로는 해결할 수 없는 것들
재컴파일에는 한계가 있으며, 이를 솔직하게 밝히는 것이 중요합니다. 악성 코드가 소스 코드 자체에 포함되어 있다면, 충실한 재컴파일 과정에서도 그 코드가 그대로 재현될 것입니다. 그렇기 때문에 재컴파일과 악성코드 검사는 반드시 함께 이루어져야 합니다. 하나는 생성된 파일이 소스 코드와 일치하는지 확인하는 것이고, 다른 하나는 해당 소스 코드가 자신의 컴퓨터에서 실행되기를 원하는 것인지 확인하는 것입니다. 두 질문 중 어느 하나만으로는 이 문제를 완전히 해결할 수 없습니다.
생태계 전반에 걸친 문제점도 있습니다. 모든 npm 패키지가 리포지토리에 포함된 내용만으로는 문제없이 빌드되는 것은 아닙니다. 커밋된 적이 없는 파일을 포함하는 패키지도 있고, 아무도 문서화하지 않은 빌드 설정에 의존하는 패키지도 있으며, 공개된 소스 코드가 아예 없는 패키지도 소수나마 존재합니다. 이는 레지스트리를 대규모로 검증하려는 누구에게나 문제가 되며, 발행자에게 더 많은 책임을 요구해야 한다는 주장을 뒷받침하는 타당한 근거가 됩니다.
어디서부터 시작할지
가장 비용 부담이 적은 첫걸음은 CI 환경에서 `npm audit signatures` 명령어를 실행하여, 적어도 어떤 의존성이 검증되었는지, 어떤 것이 검증되지 않았는지 파악하는 것입니다. 직접 패키지를 배포하는 경우, 신뢰할 수 있는 배포 방식으로 전환하여 자신의 릴리스에 출처 정보를 포함시키세요. 비용은 거의 들지 않으면서도 생태계를 이해하는 데 조금이나마 도움이 됩니다.
그런 다음 팀원들과 함께 여러분의 신뢰가 실제로 어디에 놓여 있는지에 대해 더 깊이 있는 대화를 나눠보세요. 현재 대부분의 자바스크립트 프로젝트에서 신뢰는 수천 명의 낯선 사람들의 CI 파이프라인과 npm 계정에 맡겨져 있으며, ‘프로비넌스(provenance)’는 단지 그중 어느 것들인지 알려줄 뿐입니다. 그 신뢰를 여러분이 직접 통제할 수 있는 곳으로 옮기는 것, 즉 패키지 유지보수자가 떠난 후에도 오랫동안 패키지를 재빌드하고, 검증하며, 지속적으로 패치를 적용해 주는 곳으로 옮기는 것이 훨씬 더 견고한 입지를 확보하는 방법입니다. 패키지가 수명 주기의 끝(EOL)에 도달하면, 바로 그 마지막 부분에서 TuxCare의 ‘Endless Lifecycle Support’가 그 역할을 이어받습니다.
출처 정보는 갖춰 둘 가치가 있으며, 레지스트리에서 이를 훨씬 더 많이 활용했으면 합니다. 출처 정보는 단순히 패키지가 어디서 왔는지를 알려줄 뿐이며, CI 비밀 정보를 안전하게 보호하기 위한 핵심은 패키지 내부에 무엇이 들어 있는지입니다.
