왜 CI인가
공급망 보안을 CI에서 해야 하는 이유
출시하는 소프트웨어는 CI 빌드가 그 순간 내려받은 것으로 만들어 낸 결과물입니다. 그 빌드야말로 무엇이 들어갔는지 보고, 무엇을 허용할지 결정하고, 릴리스에 무엇이 들어갔는지 기록할 수 있는 유일한 곳입니다.
업데이트
What software supply chain security means
발표의 실무적 정의: 소프트웨어는 직접 작성한 코드와 그 코드가 의존하는 모든 것이며, 공급망은 이 부분들을 릴리스에 담기까지의 모든 단계입니다.
Kubernetes Community Days Helsinki 2026의 Petteri Pulkkinen 및 Erika Marttinen, SBOMbastic: Getting Ready for Upcoming EU Cybersecurity Regulation in Software Supply Chains 중 2:35~4:28.
프로덕션 빌드는 노트북의 빌드가 아닙니다
대부분의 팀은 로컬에서 테스트한 코드가 그대로 출시된다고 가정합니다. 소스는 같지만 소프트웨어는 다른 경우가 많습니다. 빌드는 소스 코드에 더해, 실행 시점에 가져오는 모든 것과 그 주변의 모든 것으로 이루어지며, 그중 고정된 것은 거의 없습니다.
고정되지 않은 종속성은 매번 다르게 해석됩니다
종속성 범위 지정은 일반적인 관행입니다. npm에서 ^1.0.4는 이후의 모든 1.x 릴리스를 허용하므로, 같은 package.json이라도 날짜에 따라 다른 버전을 설치합니다. 잠금 파일이 버전을 고정하지만, 이는 잠금 파일이 커밋되어 있고 빌드가 이를 따를 때만 해당합니다. npm ci는 잠금 파일과 package.json이 일치하지 않으면 실패하지만, npm install은 잠금 파일을 조용히 업데이트할 수 있습니다. 잠금 파일을 아예 커밋하지 않는 프로젝트도 많습니다. 널리 쓰이는 웹 프레임워크인 Express 5.2.1도 커밋하지 않습니다.
버전을 고정하더라도 그것은 파일이 아니라 이름일 뿐입니다. pip 문서는 버전 고정만으로는 패키지 인덱스가 같은 버전에 대해 다른 파일을 제공하는 것을 막을 수 없다고 설명하며, 그래서 해시 검사 모드를 제공합니다.
시간이 입력을 바꿉니다
레지스트리는 빌드 사이에 바뀝니다. 새 버전이 게시되고, 이전 버전이 철회되며, 월요일에 안전하게 해석된 범위가 화요일에는 손상된 릴리스를 가져올 수 있습니다. 컨테이너 태그도 바뀝니다. Docker 문서는 "이미지 태그는 변경 가능하므로 게시자가 태그를 새 이미지를 가리키도록 업데이트할 수 있다"고 경고합니다. 따라서 다이제스트로 고정하지 않는 한 FROM node:20은 달마다 같은 베이스 이미지가 아닙니다.
시간은 결과물 안에도 들어갑니다. Reproducible Builds 프로젝트는 많은 도구가 빌드 결과물에 현재 날짜와 시간을 기록하기 때문에 타임스탬프를 "재현성 문제의 가장 큰 원인"이라고 부릅니다.
환경이 다릅니다
- 운영체제와 아키텍처. macOS와 ARM의 노트북, Linux와 x86-64의 CI 러너는 같은 패키지에 대해 서로 다른 사전 빌드 바이너리를 설치합니다. 이제 많은 npm 도구가 컴파일된 바이너리를 감싼 플랫폼별 패키지로 배포됩니다.
- 툴체인. 컴파일러, 런타임, 패키지 관리자 버전은 하나하나 모두 고정하지 않는 한 머신마다 달라집니다.
- 캐시. 로컬 머신과 CI는 서로 다른 시점에 서로 다른 해석 결과로 만들어진 서로 다른 캐시를 복원합니다.
- 설치 스크립트와 빌드 단계. 설치 후 훅, 설정 액션, 빌드 스크립트는 빌드 시점에 도구와 데이터를 내려받습니다. 이 중 어느 것도 매니페스트에 나타나지 않습니다.
- 구성. 환경 변수, 빌드 플래그, 시크릿은 CI에만 존재합니다.
Reproducible Builds 프로젝트는 재현 가능한 빌드를 "같은 소스 코드, 빌드 환경, 빌드 지침이 주어지면 누구든 지정된 모든 아티팩트의 비트 단위로 동일한 사본을 다시 만들 수 있는" 빌드로 정의합니다. 이 정의는 얼마나 많은 것을 일정하게 유지해야 하는지를 보여 줍니다. 대부분의 팀의 빌드는 여기에 이르지 못하며, 이른 팀은 많은 노력을 들였습니다. 그 수준에 이르기 전까지, 무엇이 출시되었는지에 대한 신뢰할 수 있는 유일한 기록은 출시된 빌드가 실행되는 동안 남긴 기록입니다.
A software supply chain, walked end to end
Kubernetes Community Days Helsinki 2026의 Petteri Pulkkinen 및 Erika Marttinen, SBOMbastic: Getting Ready for Upcoming EU Cybersecurity Regulation in Software Supply Chains 중 15:20~16:38.
점검할 수 있는 곳, 그리고 각각이 볼 수 있는 것
보안 점검은 네 곳에서 실행할 수 있습니다. 각각 맡은 역할이 있지만, 실제로 릴리스를 만들어 낸 빌드를 보는 곳은 그중 하나뿐입니다.
| 기능 | CI 빌드 내 | 개발자 머신 | 리포지토리 스캔 | 아티팩트 스캔 |
|---|---|---|---|---|
| 프로덕션이 실제로 받은 버전 확인 | 가져오는 시점에 기록 | 다른 설치 | 잠금 파일로 고정된 경우만 | 인식 가능한 것만 |
| 전이적 패키지와 캐시에서 복원된 패키지 확인 | CI가 아닌 자체 캐시 | 잠금 파일만 | ||
| 설치 스크립트가 내려받는 것 확인 | ||||
| 악의적인 외부 호출 차단 | 러너의 이그레스 정책 | |||
| 코드 푸시 전 문제 발견 | 풀 리퀘스트에서 |
- 포함
- 일부 지원
- 미포함
리포지토리 스캔은 프로젝트가 선언한 것을 보고합니다. 완성된 아티팩트의 스캔은 그 안에서 인식할 수 있는 것을 보고합니다. 어느 쪽도 빌드가 일어나는 과정을 보지 못했습니다. Express 5.2.1은 잠금 파일을 커밋하지 않으므로, 깨끗한 체크아웃을 대상으로 한 스캐너는 해석할 프로젝트의 npm 종속성이 하나도 없어 그 밖에 인식한 것만 보고합니다. 같은 도구를 같은 리포지토리의 설치된 사본에 실행하고 카탈로거 플래그 하나만 바꿔도 결과가 다시 달라집니다. 결과는 스캔 전에 무엇이 설치되어 있었는지에 달려 있는데, 이것이 바로 스캔이 통제하지 못하는 부분입니다. 빌드 시점 SBOM 페이지에서 스캔 결과가 서로 다른 이유와 빌드를 기록하면 무엇이 달라지는지 다룹니다.
파이프라인 자체가 공격 대상입니다
CI는 소스 코드, 종속성, 시크릿이 만나는 곳이므로 공격할 가치가 있습니다.
- tj-actions/changed-files (2025). 널리 쓰이는 GitHub Action이 손상되어 CI 시크릿을 빌드 로그에 출력했습니다.
- Codecov Bash Uploader (2021). 고객의 CI에서 실행되는 스크립트가 변조되어 환경 변수를 공격자의 서버로 보냈습니다.
- Shai-Hulud npm 웜 (2025). 손상된 패키지가 설치 시 자격 증명을 탈취하고, 이를 이용해 더 많은 손상된 패키지를 게시했습니다.
- xz utils (2024). 백도어가 눈에 보이는 소스가 아니라 릴리스 tarball과 빌드 프로세스 자체에 숨겨져 있었습니다.
이들은 모두 빌드나 설치 중에 코드를 실행했습니다. 그 전에 실행한 리포지토리 스캔이나 그 후에 실행한 아티팩트 스캔으로는 빌드 프로세스가 무엇을 했는지 볼 수 없었을 것입니다. 작업이 접근할 수 있는 범위를 관찰하고 제한하는 통제는 작업이 실행되는 곳에 있어야 합니다. 이는 내부용이나 눈에 띄지 않는 앱에도 마찬가지입니다. 이런 공격은 앱이 하는 일이 아니라 모든 앱이 공유하는 파이프라인과 패키지를 노립니다. 내부 및 저위험 소프트웨어를 위한 공급망 보안을 참고하세요.
코드와 종속성은 늘고, 읽는 사람은 줄고 있습니다
AI 코딩 에이전트는 팀이 모든 줄을 검토할 수 있는 속도보다 빠르게 코드를 작성하고 변경하며, 그 과정에서 종속성을 추가합니다. 존재하지 않는 패키지 이름을 대기도 합니다. 생성된 코드 샘플 576,000개를 분석한 USENIX Security 2025 연구에 따르면, 환각된 패키지의 비율은 상용 모델에서 최소 5.2%, 오픈 소스 모델에서 21.7%였습니다. 공격자가 그런 이름 하나를 등록하면, 그 이름을 신뢰하는 다음 빌드가 공격자의 패키지를 설치합니다.
누가 또는 무엇이 작성했든, 모든 변경은 프로덕션으로 가는 길에 빌드를 통과합니다. 그래서 빌드는 누군가 살펴봤는지에 의존하지 않는 유일한 검문소입니다.
증거는 출시된 것을 설명해야 합니다
규제 당국과 고객은 의도가 아니라 증명을 요구하고 있습니다. EU Cyber Resilience Act는 제조업체에 제품의 구성 요소를 식별하고 문서화하며, 제품 지원 기간 동안 그 구성 요소의 취약점을 처리하도록 요구합니다. 매니페스트에서 생성한 SBOM은 프로젝트가 요청한 것을 설명합니다. 빌드에서 기록한 SBOM은 책임져야 할 릴리스에 실제로 들어간 것을 설명합니다. 새 취약점이 공개되면, 후자가 어떤 릴리스가 영향을 받는지 알려 줍니다. CRA의 요구 사항을 읽어 보세요.
"CI에서"가 의미해야 하는 것
스캐너를 CI 단계로 실행하는 것은 시작이지만, 여전히 도착한 뒤의 파일을 들여다볼 뿐입니다. 제대로 된 "CI에서"는 다음을 의미합니다.
- 추론하지 말고 기록하세요. 전이적 종속성과 캐시에서 복원된 것을 포함해 작업이 실제로 가져오는 모든 패키지와 다운로드를 수집합니다.
- 기록이 얼마나 완전한지 밝히세요. SBOM은 찾은 것을 나열하는 데 그치지 않고, 작업이 가져온 모든 것을 다루는지 명시해야 합니다.
- 이그레스를 통제하세요. 작업이 접근할 수 있는 호스트를 정하고, 손상된 종속성이 외부와 통신하기 전에 나머지를 차단합니다.
- 출시 후에도 계속 지켜보세요. 새 취약점이 나타날 때마다 출시된 것을 다시 점검하고, 발견 사항을 수정으로 이어지게 합니다.
이것이 CRACI가 하는 일입니다. CRACI는 GitHub Actions 작업을 위한 러너이므로, 각 빌드가 가져오는 것을 기록하고, 이그레스 정책을 적용하며, 출시한 것을 모니터링합니다. 워크플로 이전은 한 줄이면 됩니다: runs-on: craci. CRACI의 작동 방식을 참고하세요.
CI가 대체하지 않는 것
공급망 보안을 빌드에 둔다고 해서 다른 계층이 선택 사항이 되는 것은 아닙니다.
- 개발자 머신은 여전히 자격 증명을 보관하고 설치 스크립트를 실행합니다. 이것도 보호하세요. 손상된 노트북은 빌드가 실행되기도 전에 코드를 푸시할 수 있습니다.
- 코드 리뷰와 정적 분석은 자체 코드의 결함을 찾아내며, 이는 종속성 기록으로는 할 수 없는 일입니다.
- 버전 고정, 잠금 파일, 다이제스트는 여전히 좋은 관행입니다. 빌드 간의 차이를 좁혀 주며, 빌드를 기록하면 그와 상관없이 무슨 일이 있었는지 알 수 있습니다.
빌드만이 점검할 수 있는 유일한 곳은 아닙니다. 하지만 실제로 출시하는 소프트웨어를 보는 곳은 빌드뿐입니다.