누구도 공급망 보안을 보장해주지 않는다
Rust의 패키지 저장소인 crates.io를 겨냥한 공급망 공격 비판에 대해 패키지 생태계의 근본적인 한계를 지적하며 반박하는 글입니다. 네임스페이스 도입이나 샌드박싱 같은 기술적 해결책이 오히려 사용자의 인지적 부담을 가중시키거나 시스템적 한계에 부딪힌다고 설명합니다. 또한 저장소 코드와 일치 여부 검증 등 제기되는 문제들이 실질적으로 해결 불가능한 근본적인 딜레마를 내포하고 있음을 강조합니다.
누구도 공급망 보안을 보장해주지 않는다
혹시 모르실 수 있으니 말씀드리자면, 저는 개발자가 아닙니다. 저는 컴퓨팅 파워의 비최적적 사용에 짜증을 내는 자폐 스펙트럼의 고양이 소녀일 뿐이며, 이 문제를 해결하다 보니 우연히 프로그래밍을 하게 되었습니다. 결정적으로, 이 과정에는 백스테이지에서 기반 기술에 대해 논의하는 것도 포함되며, 그 덕분에 저는 이 분야의 사회적 측면을 더 잘 인식하게 된 것 같습니다. 그래서 저는 공급망 공격과 관련해 crates.io를 비판하는 것에 대해 제 생각이 있습니다. 비슷한 글이 한두 편이 아니고 나온 지금, 왜 그런 비판이 핵심을 벗어났는지 몇 마디 하고자 합니다.
오타 스쿼팅(Typo-squatting) 핵심을 다루기 전에, 애초에 공급망 공격이 어떻게 발생하는지, 그리고 이를 고치려는 일반적인 아이디어가 왜 효과가 없는지 이야기해 봅시다. 악의적인 의존성이 프로젝트에 추가되는 데에는 여러 이유가 있습니다. 가장 노골적인 이유 중 하나가 오타 스쿼팅입니다. 이는 악의적인 라이브러리가 진짜 라이브러리와 비슷한 이름을 가질 때 발생합니다(예: num_cpu vs num_cpus). 흔히 제시되는 해결책으로는 직접 URL을 사용하거나 네임스페이싱(Namespacing)을 도입하는 것이 있습니다. 자, 이게 도움이 되는지 봅시다. Cargo.toml에 다음 줄들을 추가하는 Pull Request(PR)를 받았다고 상상해 보세요:
[dependencies] bitflags = { git = "https://github.com/bitflags/bitflags" } itertools = { git = "https://github.com/itertools/itertools" } rand_core = { git = "https://github.com/rust-random/rand_core" }
이 URL 중 하나는 가짜입니다. 어느 것인지 알 수 있나요? 정답은 itertools입니다. 올바른 URL은 https://github.com/rust-itertools/itertools 입니다. https://github.com/itertools 는 어떤 랜덤한 계정입니다. 참고로 https://github.com/rust-bitflags 는 아예 등록조차 되어 있지 않습니다. 사용하는 모든 패키지의 URL을 기억할 수 있다고 생각한다면, 아마 틀릴 것입니다. 많은 크레이트(crates)가 개인이 아닌 깃허브 조직에서 관리되기 때문에 dtolnay나 BurntSushi를 (아마도) 신뢰할 수 있다는 것만 기억하는 것으로는 충분하지 않습니다. 이것마저도 보수적인 접근이 아닙니다. https://gitlab.com/BurntSushi 는 사용 가능하고 https://glthub.com 는 판매 중이므로, 공격자에게는 선택할 수 있는 다른 방법이 무궁무진합니다. crates.io 내부의 네임스페이싱, 깃허브 조직, 도메인 등 어떤 방식을 통해 크레이트 ID를 더 길게 만들면, 사용자가 이를 정확히 기억하기가 더 어려워지고, 결과적으로 오타 스쿼팅을 인식하기도 더 어려워집니다.
샌드박싱(Sandboxing) Rust는 빌드 스크립트(build.rs)와 절차적 매크로(procedural macros)에 PC에 대한 전체 접근 권한을 부여합니다. 더 심각한 것은, 프로젝트 디렉터리를 열면 rust-analyzer가 cargo check를 실행하므로, 이는 사실상 0-클릭 원격 코드 실행(0-click RCE)이 될 수 있습니다. 몇몇 사람들이 이 문제를 해결하려고 시도했습니다. build.rs 샌드박싱에 대한 오픈 이슈가 있고, 절차적 매크로를 WebAssembly로 컴파일하는 실험도 있었습니다. 하지만 이는 거의 실용적이지 않습니다. cargo build는 안전해질 수 있지만, 보통 그 직후에 샌드박스화가 불가능한 cargo test나 cargo run을 실행하게 됩니다. Rust 개발을 안전하게 만드는 것은 빌드 시간 이상을 포함하며, 카고(cargo) 단독으로 책임질 수 없는 강력한 시스템 수준의 격리가 필요합니다.
VCS의 코드 자주 제기되는 또 다른 문제는 crates.io에 있는 코드와 Git 저장소의 코드가 항상 일치하지 않는다는 것입니다. 기본적으로 이는 해결하기가 쉽지 않습니다. crates.io를 크레이트 이름을 저장소 URL에 매핑하는 DNS처럼 만들 수는 없습니다. crates.io는 항목을 삭제함으로써 다운스트림 사용자에게 피해를 줄 수 있는 권한을 패키지 관리자에게 주지 않도록 설계되었기 때문입니다:
crates.io의 주요 목표 중 하나는 시간이 지나도 변경되지 않는 크레이트의 영구적인 아카이브 역할을 하는 것이며, 버전 삭제를 허용하는 것은 이 목표에 위배됩니다.
이 제한은 아마도 유명한 라이브러리가 npm에서 삭제되어 CI 빌드가 중단되었던 left-pad 사건 때문에 설정된 것으로 보입니다. npm은 중앙 집중식이기 때문에 이 문제를 빠르게 해결할 수 있었습니다. 단순한 역할만 하는 thin crates.io는 이를 감당할 수 없었을 것이므로, 사본을 저장하고 제공하는 방식을 택한 것입니다.
crates.io는 cargo publish 시점에 저장소에서 파일을 가져올 수는 있을 것입니다. 하지만 관리자가 그 후에 강제 푸시(force-push)를 해버릴 수도 있다면, 이는 좋은 보안 메커니즘이 아닙니다. 어쩌면 crates.io가 주기적으로 저장소의 기록 변경을 스캔할 수도 있을 것입니다. 하지만 그것이 정확히 무엇을 의미하는 걸까요?