공급망 공격을 방어하기 위한 3가지 방어 전략

2026-05-16 hit count image

npm 공급망 공격에 대응하기 위해 세 가지 공급망 방어 전략을 정리합니다. GitHub Actions의 SHA 핀닝, Dependabot cooldown, Yarn 4의 npmMinimalAgeGate를 코드와 함께 단계별로 다룹니다.

web

들어가며

1편에서는 axios 공급망 공격 사례를 통해 npm 신뢰 모델의 구조적 한계를 살펴봤습니다. 이번 글에서는 공급망 공격이 우리 프로젝트에서 일어나지 않도록 — 정확히는, 일어나더라도 영향을 받지 않도록 — 실제 프로젝트에 적용한 세 가지 방어 전략을 코드와 함께 다룹니다.

여기서 소개하는 세 가지 전략은 각각 다른 공격 표면을 막습니다.

전략막는 공격 표면
GitHub Actions SHA 핀닝CI에서 사용하는 Actions 자체의 변조
Dependabot cooldown자동 의존성 PR을 통한 신규 악성 버전 유입
Yarn npmMinimalAgeGate로컬·CI 설치 시점에서의 신규 악성 버전 유입

방어 원칙: 시간을 끌어들이기

세 전략을 관통하는 공통 원칙은 단순합니다.

새로 게시된 것을 즉시 받아들이지 않는다.

1편에서 본 것처럼 공급망 공격은 게시 후 수 시간 안에 탐지되어 내려가는 패턴이 압도적입니다. 즉, 며칠만 기다리면 대부분의 악성 버전은 자연스럽게 걸러집니다. 이 단순한 사실을 코드와 설정으로 강제하는 것이 이번에 적용한 방어 전략의 본질입니다.

이 원칙의 유효성은 3편에서 실제 사고 데이터와 함께 이 전략의 유효성을 검증합니다.

전략 1. GitHub Actions의 SHA 핀닝

무엇을 막는가

GitHub Actions의 워크플로우는 보통 다음과 같이 작성됩니다.

- uses: actions/checkout@v6
- uses: actions/setup-node@v6

@v6 같은 태그 참조는 가독성이 좋지만, 태그는 변경 가능(mutable) 하다는 문제가 있습니다. 메인테이너가 마음만 먹으면 v6 태그가 가리키는 커밋을 다른 커밋으로 옮길 수 있습니다. 메인테이너 계정이 탈취되면 공격자도 마찬가지로 태그가 가르키는 커밋을 옮길 수 있습니다.

실제로 2025년에는 다운로드 수가 많은 tj-actions/changed-files의 태그가 악성 커밋으로 옮겨져 수많은 워크플로우가 시크릿을 유출한 사고가 있었습니다 (CVE-2025-30066).

조금 더 구체적으로 풀어 보면 이렇습니다. 워크플로우 파일에 적힌

- uses: tj-actions/changed-files@v45

라는 단 한 줄의 태그 참조가, 공격자가 우리 CI 환경에서 임의의 명령을 실행할 수 있는 RCE(Remote Code Execution, 원격 코드 실행) 의 입구가 된 것입니다. 우리 코드는 그대로지만, 그 한 줄이 가리키는 커밋이 공격자의 악성 커밋으로 바뀐 순간 다음 CI 실행부터 그 악성 코드가 우리 워크플로우의 권한으로 실행됩니다. 워크플로우는 보통 배포 토큰, 클라우드 자격증명, npm 토큰, GitHub PAT 등 환경 변수에 접근할 수 있기 때문에, 이 한 줄의 변조만으로 그 모든 시크릿이 그대로 공격자 손에 들어갈 수 있습니다.

해결: SHA로 고정

해결책은 단순합니다. 태그 대신 불변(immutable) 인 커밋 SHA로 참조하면 됩니다.

- - uses: actions/checkout@v6
+ - uses: actions/checkout@<커밋 SHA>   # v6

SHA는 커밋 내용의 해시이기 때문에, 한 번 정해진 SHA가 가리키는 코드는 절대 바뀌지 않습니다. 메인테이너가 태그를 옮겨도, 우리 워크플로우는 우리가 검증한 그 시점의 커밋을 계속 실행합니다.

읽기 어려워지는 단점은 SHA 뒤에 버전 주석을 남겨 보완할 수 있습니다.

적용 규모

실제 프로젝트에서 사용하는 전체의 CI 워크플로우 50여 개를 모두 SHA 참조로 변환했습니다.

대상은 actions/checkout, actions/setup-node, actions/cache 같은 GitHub 공식 Actions부터 aws-actions/configure-aws-credentials, tj-actions/changed-files 같은 서드파티 Actions까지 모두 포함됩니다.

SHA 조회 방법

처음 적용할 때 가장 번거로운 작업이 “이 태그가 가리키는 SHA는 무엇인가”를 알아내는 일입니다. 다음 명령어로 간단히 조회할 수 있습니다.

# v6 태그가 가리키는 SHA 조회
git ls-remote --tags https://github.com/actions/checkout.git v6

반대로 어떤 SHA에 해당하는 태그를 확인하려면 다음과 같이 합니다.

git ls-remote --tags https://github.com/tj-actions/changed-files.git \
  | grep <커밋 SHA>

Dependabot과의 호환성

“SHA로 고정하면 업데이트는 어떻게 하지?”라는 의문이 따라옵니다. 결론부터 말하면 Dependabot은 SHA로 핀닝된 Actions의 업데이트도 정상적으로 감지하여 PR을 만들어 줍니다. 새 버전이 나오면 SHA를 자동으로 갱신한 PR이 열리고, 사람이 그 변경을 리뷰한 뒤 머지할 수 있습니다.

즉, 가독성 손실과 SHA 조회의 번거로움 외에는 운영상의 부담이 없습니다. 한 번 설정해두면 자동화의 이점은 그대로 유지됩니다.

전략 2. Dependabot cooldown 설정

무엇을 막는가

Dependabot은 의존성에 새 버전이 나오면 자동으로 업데이트 PR을 만들어 줍니다. 보안 패치를 빠르게 받기 위해 좋은 도구지만, 동시에 악성 버전을 가장 빠르게 받아오는 통로가 되기도 합니다.

1편에서 본 것처럼 공급망 공격의 노출 창은 보통 수 시간입니다. Dependabot이 새 버전 게시 직후 PR을 만들고, 누군가 그 PR을 머지하면 — 또는 단순히 CI가 PR의 의존성을 자동 설치하기만 해도 — 악성 코드가 자기 환경에 들어옵니다.

해결: 7일의 냉각기

GitHub Dependabot은 2025년경부터 cooldown 옵션을 제공합니다. 이 옵션을 켜면 새 버전이 게시된 뒤 일정 시간이 지나야만 Dependabot이 그 버전에 대한 PR을 만듭니다.

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: 'npm'
    directory: '/'
    schedule:
      interval: 'weekly'
    cooldown:
      default-days: 7
      semver-major-days: 7
      semver-minor-days: 7
      semver-patch-days: 7

각 옵션의 의미는 다음과 같습니다.

옵션설명
default-days모든 업데이트에 적용되는 기본 대기 일수
semver-major-daysmajor 버전 업데이트의 대기 일수
semver-minor-daysminor 버전 업데이트의 대기 일수
semver-patch-dayspatch 버전 업데이트의 대기 일수

7일이라는 숫자는 임의로 정한 것이 아닙니다. 공급망 공격 사례 분석에 따르면 알려진 사고 대부분이 7일 안에 탐지·삭제되었습니다. 자세한 근거는 3편에서 다룹니다.

보안 경고는 즉시

여기서 한 가지 주의할 점은 보안 알림은 cooldown 대상이 아니라는 것입니다. CVE가 공개된 취약점에 대한 보안 업데이트 PR은 cooldown 설정과 무관하게 즉시 만들어집니다. 즉, “신규 버전은 7일 기다리지만, 알려진 취약점은 바로 패치”라는 합리적인 동작이 가능합니다.

전략 3. Yarn 4의 npmMinimalAgeGate

무엇을 막는가

Dependabot cooldown은 “PR이 만들어지는 시점”을 늦춥니다. 하지만 PR을 거치지 않고 직접 의존성을 추가하는 경우 — 예를 들어 개발자가 새 라이브러리를 도입하면서 yarn add some-package를 실행하는 경우 — 에는 cooldown이 동작하지 않습니다.

이 빈틈을 채우는 것이 패키지 매니저 자체에서 신규 버전의 설치를 막는 기능입니다.

해결: yarn install 시점에서 차단

Yarn 4.10부터 npmMinimalAgeGate 옵션이 도입되었습니다. 이 옵션을 설정하면 npm 레지스트리에 게시된 지 지정한 기간이 지나지 않은 패키지 버전은 설치 자체가 차단됩니다.

# .yarnrc.yml
npmMinimalAgeGate: 7d

이 한 줄로 yarn install, yarn add 모두에 일관되게 7일 게이트가 적용됩니다. 로컬 개발자 PC든 CI 서버든 동일하게 작동합니다.

설정이 제대로 적용되었는지는 다음 명령어로 확인할 수 있습니다.

yarn config get npmMinimalAgeGate
# 10080 (= 7일 × 24시간 × 60분)

Yarn 4 업그레이드를 함께 진행한 이유

이 옵션을 쓰기 위해 기존의 Yarn 3.7.0을 Yarn 4.14.1로 업그레이드했습니다. 업그레이드 자체가 부수 효과로 여러 보안 개선을 가져옵니다.

개선 사항설명
enableScripts 기본값 false서드파티 패키지의 postinstall 스크립트가 기본 비활성화
Hardened Modelockfile과 패키지 메타데이터의 무결성 검증
npmMinimalAgeGate신규 게시 패키지 설치 제한 (이번 변경의 주 목적)
npm 메타데이터 캐싱설치 속도 약 4배 향상 (보안과는 별개의 이점)

특히 enableScripts: false1편에서 본 axios 공격의 핵심 실행 트리거였던 postinstall을 기본적으로 막는다는 점에서 중요합니다. axios 공격은 plain-crypto-jspostinstall이 실행되어야 성립하는데, Yarn 4 환경에서는 그 단계 자체가 사라집니다.

마이그레이션 시 주의점

Yarn 4 업그레이드에는 약간의 부수 작업이 필요합니다.

  • package.jsonpackageManager 필드 갱신 ([email protected][email protected])
  • yarn.lock을 v9 형식으로 재생성 (대규모 diff 발생)
  • CI 워크플로우의 --frozen-lockfile 플래그를 --immutable로 치환 (Yarn 4에서 --frozen-lockfile은 폐지됨)

lockfile 변경이 크기 때문에 머지 후에는 다른 작업 브랜치들의 rebase가 필요할 수 있다는 점도 사전에 공유해 두는 것이 좋습니다.

세 전략의 역할 분담

세 전략은 서로 다른 진입점을 막기 때문에 함께 적용했을 때 비로소 의미가 있습니다.

진입점1. SHA 핀닝2. Dependabot cooldown3. npmMinimalAgeGate
워크플로우의 Actions 변조O
Dependabot이 만든 신규 의존성 PROO
yarn add로 새 라이브러리 도입O
CI에서 yarn install 시 신규 버전 유입O

특히 axios 같은 사례에 대입해 보면, 2번과 3번 어느 한쪽만 있어도 7일 이내에 적발·삭제될 악성 버전이 설치되는 일은 막을 수 있습니다. 두 가지를 함께 적용한 것은 “PR 단계”와 “설치 단계”라는 서로 다른 시점에서 이중으로 게이트를 거는 효과를 노린 것입니다.

마무리

이번 글에서 다룬 세 전략은 모두 새로 게시된 것을 즉시 받아들이지 않는다는 같은 원칙을 다른 도구·다른 시점에 적용한 것입니다.

특별히 정교하거나 복잡한 기법은 없습니다. 설정 파일 몇 줄과 워크플로우 정리만으로 끝납니다. 그럼에도 이 변경이 의미 있는 이유는 — 단순한 변경이 실제로 얼마나 많은 공격을 막을 수 있는가 — 그 데이터에 있습니다.

다음 글 공급망 방어 전략의 유효성에서는, “7일이라는 숫자는 정말로 충분한가?”, “최근 알려진 공급망 사고 중 이 방어로 막을 수 있었던 것은 몇 건인가?”, 그리고 “이 전략이 막지 못하는 공격은 무엇인가?”를 정리합니다.

참고 자료

제 블로그가 도움이 되셨나요? 하단의 댓글을 달아주시면 저에게 큰 힘이 됩니다!

앱 홍보

책 홍보

블로그를 운영하면서 좋은 기회가 생겨 책을 출판하게 되었습니다.

아래 링크를 통해 제가 쓴 책을 구매하실 수 있습니다.
많은 분들에게 도움이 되면 좋겠네요.



SHARE
Twitter Facebook RSS