본문 바로가기
Infra2026년 9월 15일10분 읽기

npm 공급망 방어 실전 — 설치 훅·출처 증명·CI 격리로 막는 7가지

YS
김영삼
조회 129
npm 공급망 방어 실전 — 설치 훅·출처 증명·CI 격리로 막는 7가지

npm 공급망 공격의 기본 구조는 단순하다. 널리 쓰이는 패키지의 게시 권한을 탈취해 악성 버전을 올린다. 그 패키지가 설치되는 순간, preinstall·postinstall 훅이 애플리케이션 코드보다 먼저 실행된다. 개발자 권한과 CI 러너 권한으로.

방어의 핵심은 세 층이다. 설치 시 코드 실행을 막고, 무엇이 설치되는지 고정하고, 설치 환경에 훔칠 것을 두지 않는 것. 셋 다 오늘 시작할 수 있다.

이 주제가 남 얘기처럼 느껴진다면 질문 하나만 해 보자. 지금 내 노트북에서 npm install을 실행하면 몇 개의 스크립트가 실행되는가? 그 스크립트들이 접근할 수 있는 건 무엇인가? SSH 키, 클라우드 자격증명, 브라우저 세션, 사내 네트워크. 대부분의 개발 환경은 프로덕션 서버보다 권한이 넓고 감시는 적다.

공격 경로 이해

탈취
메인테이너 계정 침해(피싱·토큰 유출) 또는 인수인계된 패키지 장악.
게시
악성 코드가 포함된 새 버전 배포. 대개 정상 기능은 그대로 두어 탐지를 늦춘다.
확산
의존성 트리를 타고 전파. 직접 의존이 아니라 전이 의존인 경우가 많다.
실행
설치 훅이 CI·개발 환경에서 실행. 환경 변수와 파일에서 자격증명 수집.
유출·증식
탈취한 토큰으로 다른 패키지를 게시하거나 저장소를 복제한다.
전이 의존이 문제다 직접 추가한 패키지는 신중히 고르지만, 그 패키지가 끌고 오는 수백 개는 아무도 안 본다. 실제 침해는 대부분 이 영역에서 일어난다. 그래서 "좋은 패키지만 고른다"는 전략만으로는 부족하다.

1단계 — 설치 시 코드 실행 차단

가장 효과가 큰 조치다. 설치 훅을 끄면 악성 패키지가 설치만으로 코드를 실행할 수 없게 된다. 문제는 일부 패키지가 훅에 의존한다는 것인데, 생각보다 적다.

# 훅 없이 설치
npm ci --ignore-scripts

# .npmrc 에 기본값으로 박아 두기
echo "ignore-scripts=true" >> .npmrc

# pnpm: 설치 스크립트 허용 목록 방식
# pnpm.onlyBuiltDependencies 에 꼭 필요한 패키지만 나열
# package.json
# {
#   "pnpm": {
#     "onlyBuiltDependencies": ["esbuild", "sharp"]
#   }
# }

# 어떤 패키지가 훅을 가지고 있는지 먼저 파악
npm ls --all --json 2>/dev/null | grep -c '"scripts"' || true

네이티브 빌드가 필요한 패키지(이미지 처리, 암호화 등)는 예외가 필요하다. 허용 목록 방식이 있는 패키지 매니저를 쓰면 예외를 명시적으로 관리할 수 있다. 예외 목록이 길어지면 그 자체가 리뷰 대상이 된다.

2단계 — 무엇이 설치되는지 고정

설치 결정성 확보
락파일을 커밋하고, CI에서는 npm ci(또는 --frozen-lockfile)만 사용
락파일에 무결성 해시가 포함돼 있는지 확인 — 변조 감지의 기본
버전 범위(^, ~)에 의존하지 말 것. 락파일이 없으면 매 설치가 다른 결과
의존성 추가·변경은 별도 커밋으로 분리해 리뷰 대상으로 만들기
자동 업데이트 PR은 좋지만, 자동 병합은 신중히 (봇이 악성 버전을 끌어올 수 있다)
신규 패키지 추가 시 게시 이력·저장소 연결·다운로드 추이 확인

마지막 항목에 대해 나는 팀 규칙을 하나 뒀다. 새 의존성을 추가하는 PR에는 "왜 이 패키지인가"를 한 줄 적는다. 형식적으로 보이지만, 그 한 줄을 쓰기 귀찮아서 직접 구현하게 되는 경우가 생각보다 많다. 의존성 수 자체가 공격면이다.

3단계 — 출처 증명(provenance) 활용

최근 레지스트리는 패키지가 어느 저장소의 어느 커밋에서, 어떤 CI 워크플로로 빌드됐는지를 증명하는 메타데이터를 지원한다. 이를 확인하면 "메인테이너 계정만 탈취해 로컬에서 올린" 패키지를 구분할 수 있다.

# 패키지의 provenance 확인
npm view <package> --json | grep -i provenance

# 설치 시 서명·출처 검증
npm audit signatures

# 내 패키지를 게시할 때 provenance 첨부 (CI에서)
# npm publish --provenance --access public
#   - OIDC 기반으로 CI 신원을 증명
#   - 로컬에서 수동 게시하면 provenance 가 붙지 않는다

조직 차원에서는 "provenance 없는 패키지는 새로 추가하지 않는다" 같은 정책을 점진적으로 도입할 수 있다. 기존 의존성 전부에 적용하기는 아직 무리지만, 신규 추가분부터 적용하면 시간이 지나며 비율이 올라간다.

4단계 — 설치 환경에 훔칠 것을 두지 않기

훅을 막고 락파일을 고정해도 완벽하지 않다. 그래서 마지막 방어선은 탈취당해도 잃을 게 적게 만드는 것이다.

위험 요소나쁜 방식나은 방식
CI 자격증명장기 액세스 키를 시크릿에 저장OIDC로 발급받는 단기 토큰
레지스트리 토큰만료 없는 자동화 토큰짧은 만료 + 게시 전용 스코프
빌드 환경모든 시크릿이 한 잡에 노출잡 분리 — 빌드와 배포를 다른 잡으로
네트워크무제한 아웃바운드필요한 도메인만 허용(이그레스 제어)
개발자 PC클라우드 관리자 키 상주SSO 기반 임시 자격증명, 권한 최소화
# GitHub Actions 예: 빌드 잡에는 시크릿을 주지 않는다
jobs:
  build:
    permissions:
      contents: read          # 최소 권한
    steps:
      - uses: actions/checkout@v4
      - run: npm ci --ignore-scripts
      - run: npm run build
      - uses: actions/upload-artifact@v4
        with: { name: dist, path: dist/ }

  publish:
    needs: build
    permissions:
      id-token: write         # OIDC 로 단기 자격증명 발급
      contents: read
    steps:
      - uses: actions/download-artifact@v4
        with: { name: dist }
      # 여기서만 배포 자격증명을 사용한다

5단계 — 탐지

막는 것만큼 중요한 게 빨리 아는 것이다. 침해 사례들의 공통점은 발견이 늦었다는 것이다. 몇 달 뒤 외부 제보로 아는 경우가 흔하다.

  • 레지스트리 토큰의 사용 위치·시각을 주기적으로 검토한다.
  • 저장소 대량 클론이나 비정상 시간대 접근에 알림을 건다.
  • CI 잡의 아웃바운드 연결 대상을 로깅한다 — 빌드가 낯선 도메인에 접속하면 신호다.
  • 의존성 변경 알림을 받는다 (락파일 diff를 리뷰 필수로).
  • 조직 구성원의 퇴사·역할 변경 시 발급한 모든 토큰을 회수한다.
오프보딩 체크 실제 침해 사례에서 전 직원 계정이 경로가 된 경우가 반복된다. 조직 멤버십만 제거하고 개인 액세스 토큰·OAuth 승인·SSH 키·배포 키를 남겨 두면 접근이 유지된다. 오프보딩 체크리스트에 "이 사람이 발급한 모든 토큰"이 들어 있는지 지금 확인해 보자.

현실적인 도입 순서

1
락파일 강제
CI에서 npm ci만 쓰도록 바꾼다. 비용 0, 효과 즉시.
2
설치 훅 차단
새 프로젝트부터 기본 적용하고, 기존 프로젝트는 예외 목록을 만들며 전환한다.
3
CI 자격증명 정리
장기 키를 OIDC 단기 토큰으로 교체. 가장 효과가 큰 구조 변경.
4
provenance 정책
신규 의존성부터 적용. 자사 패키지 게시에도 붙인다.
5
탐지 체계
토큰 사용 감사, 이그레스 로깅, 락파일 diff 리뷰 의무화.

전부 하려 들면 아무것도 안 된다. 1번과 3번만 해도 실제 피해 시나리오의 상당수가 막힌다. 나머지는 여유가 생길 때 하나씩 붙이면 된다.

자주 묻는 질문

설치 스크립트를 꺼도 괜찮나요?

대부분의 패키지는 문제없이 동작합니다. 네이티브 바이너리를 빌드하거나 내려받는 일부 패키지만 예외가 필요하며, 허용 목록으로 관리할 수 있습니다. 예외 목록이 짧게 유지되는지 주기적으로 확인하세요.

락파일만 있으면 안전한가요?

설치 결과를 고정해 주지만, 악성 버전이 락파일에 들어온 이후에는 소용이 없습니다. 의존성 변경을 리뷰 대상으로 만들고, 자동 업데이트의 자동 병합을 피하는 정책이 함께 필요합니다.

provenance가 있으면 안전한 패키지인가요?

해당 버전이 특정 저장소와 CI 워크플로에서 빌드됐음을 증명할 뿐, 코드가 안전하다는 보증은 아닙니다. 다만 계정만 탈취해 로컬에서 올린 패키지를 구분하는 데는 효과적이어서, 신뢰 판단의 유용한 신호가 됩니다.

CI에서 장기 토큰을 없애려면 어떻게 하나요?

OIDC를 지원하는 클라우드·레지스트리라면 CI가 워크플로 신원을 증명해 단기 자격증명을 발급받도록 구성할 수 있습니다. 빌드 잡과 배포 잡을 분리해 시크릿이 필요한 범위를 최소화하는 것도 함께 적용하세요.

전이 의존성은 어떻게 관리하나요?

전부 검토하는 것은 불가능하므로 구조적 방어에 의존해야 합니다. 설치 훅 차단, 락파일 고정, CI 권한 최소화가 핵심이며, 여기에 의존성 수 자체를 줄이는 문화(새 패키지 추가 시 필요성 검토)가 더해지면 공격면이 눈에 띄게 줄어듭니다.

침해를 당했는지 어떻게 확인하나요?

레지스트리 토큰의 사용 이력, 저장소 클론·접근 로그, CI 잡의 외부 연결 기록을 확인하세요. 의심되면 모든 토큰을 즉시 회전하고, 영향 기간에 게시된 자사 패키지와 커밋에 이상이 없는지 감사해야 합니다.

댓글 0

아직 댓글이 없습니다.
Ctrl+Enter로 등록