Rust 생태계의 공급망 공격 표면, 공격 방식, 그리고 오늘부터 실천할 수 있는 완화 전략을 살펴봅니다.
"내가 그렇게 말했잖아"라고 말할 수 있으려면, 실제로 그렇게 말했어야 한다는 것이 핵심이다. 자, 바로 그 상황이 왔다.
최근 몇 주 동안 매우 인기 있는 여러 소프트웨어 패키지가 손상되었다는 사실을 모르는 분들을 위해 말하자면(운이 좋으시네요), axios 같은 사례가 있었다. 이는 JavaScript 생태계에 큰 일이었는데, 이런 패키지들 중 일부는 주당 거의 1억 회 다운로드되기 때문이다(맞다, 100,000,000이다). 배우는 가장 저렴한 방법은 다른 사람의 실수에서 배우는 것이다. 배우는 가장 비싼 방법은 쉽게 막을 수 있었던 일의 추락을 완화하기 위해 변호사와 PR 컨설턴트 비용을 치르는 것이다. 그래서 오늘은 Rust 생태계가 어떻게 공격받게 될지(혹은 이미 공격받았는지), 그리고 그 불가피한 일을 어떻게 완화할 수 있는지 살펴보겠다.
하지만 먼저, 짧은 푸념부터. 나는 조종사들이 자신의 일과 경험에 대해 이야기하는 영상을 보는 것을 좋아한다. 그때마다 눈에 띄는 것은 항공 업계와 소프트웨어 산업 사이의 사고방식 차이다. 항공 업계에서는 사건과 사고를 매우 गंभीर하게 받아들이며, 인명이나 값비싼 항공기의 손실을 피하기 위해 타인의 사례에서 배우는 모든 것이 학습의 기회가 된다. 그들은 지속적인 학습과 개선의 기술을 숙달했다. 반면 위에서 언급한 공급망 공격 같은 사건들은 소프트웨어 산업에서 너무 가볍게 받아들여지는 듯하고, 수년간 이루어진 개선도 아주 작으며 대개 문제의 근본 원인을 해결하지 못한다. 나는 기술 업계 사람들이 항공 업계로부터 배울 점이 많다고 생각하며, 그런 영상을 보기를 권한다. 아니면 단지 내가 항공 업계의 지저분한 현실적인 "비하인드 스토리"를 보지 못하는 것일 수도 있다. 누가 알겠는가.
Rust는 JavaScript와 마찬가지로, 작은 표준 라이브러리와 중앙집중식 패키지 저장소(crates.io)에 의존하는 설계상 안전하지 않은 동일한 방식의 의존성 관리 방식을 선택했다. 사실상 "Hello World"보다 조금만 복잡한 일을 하려 해도 이것을 사용해야 한다.
중앙집중식 패키지 저장소는 공격자에게는 꿈과 같다. 단일 장애 지점을 추가하고, 악성 코드를 개발자와 패키지 소비자 사이에 숨길 수 있는 그림자 지대를 만들기 때문이다.
최근 분석에서 Adam Harvey는 crates.io에서 가장 인기 있는 999개의 크레이트 중 약 17%가 코드 저장소와 일치하지 않는 코드를 포함하고 있음을 발견했다.
17%다!
다시 말해 보자. 가장 인기 있는 Rust 패키지의 17%에는 사실상 아무도 그것이 무엇을 하는지 모르는 코드가 들어 있다(관심을 덜 받는 긴 꼬리 구간은 상상조차 하기 싫다).
모든 사이버 공격의 첫 단계는 정찰과 표적화다. Rust에서는 crates.io의 API 덕분에 쉽다. 가장 인기 있는 모든 크레이트를 나열하고, 이 크레이트들의 개발자 중에서 표적을 고를 수 있기 때문이다.
$ curl 'https://crates.io/api/v1/crates?sort=downloads&per_page=100'
그다음 공격자는 GitHub에서 유지관리자들을 추적하고, 피싱 이메일을 보내거나, 그 유지관리자들이 스스로 사용하고 있는 프로젝트를 공격하려 시도할 것이다.
Rust 생태계를 노리는 공격자는 아마 먼저 탈취된 자격 증명을 사려고 할 것이다.
일부 사이버 범죄 집단은 탈취된 시스템과 자격 증명을 판매하는 데 특화되어 있으며, 스스로 익스플로잇을 수행하는 데는 관심이 없다.
인기 있는 Rust 패키지의 개발자가 자신의 컴퓨터에 API 토큰과 쿠키를 모두 유출하는 백도어 프로그램을 설치했다고 상상해 보자. 그러면 이런 자격 증명은 해킹 포럼이나 채팅 그룹에서 판매될 것이다.
운이 좋다면, 공격자는 crates.io 쿠키나 토큰을 직접 구매할 수 있을 것이다.
탈취된 자격 증명을 살 수 없다면, 공격자는 스스로 그것들을 얻어야 한다.
악성코드를 퍼뜨리는 가장 명백하고 쉬운 첫 번째 방법은 타이포스쿼팅이다. 즉, 합법적이고 인기 있는 패키지를 복제하되 이름의 철자를 1~2글자 바꿔, 의심하지 않는 개발자가 잘못된 패키지를 사용하게 만드는 것이다.
여기 num_cpu 패키지 이름을 선점한 예가 있다. 이것은 다운로드 수가 4억 회가 넘는 인기 크레이트 num_cpus보다 글자 하나 짧다.

하지만 그게 전부가 아니다! crates.io의 모든 크레이트는 전역 네임스페이스 아래에 존재하며, 이는 조직 단위 스코프가 없다는 뜻이다.
그래서 조직, 프로젝트, 개발자들은 자신의 패키지를 찾기 쉽게 하고 묶기 위해 접두사를 사용한다. 예를 들어 tokio-stream이나 actix-http 같은 식이다.
문제는 누구나 특정 접두사를 가진 패키지를 업로드할 수 있다는 점이다. 예를 들어, 나는 tokio-backdoor라는 크레이트를 업로드했다. 물론 이보다 더 노골적인 이름을 짓기는 어렵겠지만, 만약 내가 이 크레이트의 이름을 tokio-workerpool이나 tokio-future라고 지었다면 어떨지 상상해 보라.
README, 저장소, 태그 같은 오해를 부르는 메타데이터를 사용하면 공격자는 그런 오해를 부르는 크레이트를 공식 패키지처럼 보이게 만들 수 있다.
이런 사기를 어떻게 탐지할 수 있을까?
어렵다. 그리고 사용하는 모든 패키지에 대해 꼼꼼한 감사를 해야 한다!
Rust의 매크로는 컴파일 시점이나 cargo check 시점에 실행되는 코드다. 이것이 악용될 수 있을까?
결론부터 말하면 그렇다! 컴파일 시점에 코드를 실행할 수 있다는 것은, 의존성 중 어느 것이든 악성코드를 다운로드하거나 여러분의 컴퓨터에서 파일을 유출할 수 있다는 뜻이다.
이 위험은 rust-analyzer도 프로젝트를 불러올 때 매크로를 확장한다는 사실로 인해 더 커진다. 따라서 어떤 크레이트의 의존성 중 하나라도 백도어가 심어져 있다면, 코드 편집기(그리고 rust-analyzer 플러그인)로 그 폴더를 여는 것만으로도 시스템이 손상될 수 있다.
직접 의존성이든 간접 의존성이든 마찬가지다!
이런 공격은 공격자에게 특히 매력적인데, 이런 공격의 표적인 개발자 머신과 CI/CD 서버에는 흔히 자격 증명이 들어 있어, 이를 이용해 추가 침투를 하거나 더 많은 악성코드를 퍼뜨릴 수 있기 때문이다.
다음은 악성 매크로의 두 가지 예다.
먼저, 속성 매크로다:
use proc_macro::TokenStream;
fn something_evil(file: &str) {
let home = std::env::var("HOME").unwrap();
let home = std::path::Path::new(&home);
let npmrc_path = home.join(".npmrc");
let _ = std::fs::read(npmrc_path);
// upload the npmrc to a server
}
#[proc_macro_derive(EvilMacro)]
pub fn evil_macro_derive(_item: TokenStream) -> TokenStream {
something_evil();
"".parse().unwrap()
}
use malicious_macro::EvilMacro;
#[derive(EvilMacro)]
pub struct RandomStruct {}
그다음은 함수형 프로시저 매크로다:
#[proc_macro]
pub fn evil(_item: TokenStream) -> TokenStream {
something_evil();
"".parse().unwrap()
}
다시 말하지만, (전이 의존성이든 아니든) 여러분의 의존성 중 어느 것이라도 이 매크로를 호출하면, 컴파일 시점 손상에는 그것만으로 충분하다.
pub fn do_something() {
println!("do something...");
}
malicious_macro::evil!();
main.rs
// main doesn't call evil! but the machine is compromised nonetheless
fn main() {
lib::do_something();
}
build.rs악성 매크로와 마찬가지로, build.rs 파일은 cargo check와 rust-analyzer에 의해 실행된다. 따라서 의존성 중 하나가 백도어에 감염된 크레이트의 폴더를 코드 편집기로 여는 것만으로도 시스템이 손상될 수 있다.
build.rs
use std::path::Path;
fn main() {
let home = std::env::var("HOME").unwrap();
let home = std::path::Path::new(&home);
let npmrc_path = home.join(".npmrc");
let _ = std::fs::read(npmrc_path);
// upload the npmrc to a server
}
이 기법은 악성 매크로보다 은밀성이 떨어지는데, build.rs 파일은 컴파일 과정에서 표시되며 더 눈에 띄기 쉽기 때문이다.
요약: Rust 크레이트에 백도어를 심는 것은 쉽고, 탐지하기는 어렵다.
이제 직업을 바꿔야 할 때라고 느끼는가? AI로 대체될 수 없는 실전 암호학, 보안 엔지니어링, 그리고 안전하고 프로덕션 준비가 된 Rust 코드를 작성하는 방법 같은 깊이 있는 기술을 내 책 Black Hat Rust which is currently in promotion에서 배워 보라. 그 책에서는 특히 원격 접근 도구를 제어하기 위한 종단간 암호화 프로토콜을 설계하고, 웹 서버를 구축하며, 어셈블리 대신 no_std Rust로 셸코드와 익스플로잇을 작성하게 된다.
좋은 소식은 이미 공급망 보안의 황금 표준이 존재한다는 점이다. 바로 Go 프로그래밍 언어다.
공급망 공격에 대한 단연 최고의 방어책은 Go의 사례처럼 전문가가 개발한 포괄적인 표준 라이브러리다.
단순한 HTTP 서버 하나를 작성하기 위해서만 수백 명의 서로 다른 작성자가 만든 수백 개의 패키지를 가져와야 하는 프로그래밍 언어는(솔직히 말해, 보안 분야에서 RAT를 만들든 AI 스타트업에서 일하든, 이제 HTTP 서버는 새로운 Hello World다) 공격자에게는 최고의 환경이다.
이것은 공격 표면이 거대하다는 뜻일 뿐만 아니라(공격 표면이 무엇인지 궁금한가? 여기서 살펴보라), 성공적인 공격이 많은 시스템과 조직으로 통하는 문을 열게 된다는 뜻이기도 하다.
여러 이념적 이유 때문에, Rust 같은 대부분의 프로그래밍 언어는 현재 "정체"를 피하기 위해 작은 표준 라이브러리를 선호한다.
그들은 틀렸다, 아주 끔찍하게 틀렸다! (아니, 나는 이 문제에 절대 감정적으로 지나치게 몰입한 것이 아니다)
첫 번째 문제는 안정성이다. 나는 이런 나쁜 설계 결정을 내리는 사람들은 오랫동안 시스템을 유지보수해 본 적이 없다는 이론을 가지고 있다. 대신 그들은 새 프로젝트에서 새 프로젝트로 옮겨 다니며, 매달 수백 개의 의존성을 업데이트하고 기존 코드를 계속 깨뜨리는 결과를 감당할 일이 없다. 광범위한 표준 라이브러리는 이 문제를 해결한다. (내 다른 이론은 이것이 은밀한 고용 보험 프로그램이라는 것이다. 복잡성과 기하급수적인 잡무를 만들어 냄으로써, 그들은 자신의 일자리를 지키거나 아니면 조직이 파산하도록 보장받는다.)
두 번째 문제는 파편화된 생태계다. 프로그래밍 언어는 제품이 아니라 플랫폼이다. 개발자가 격주마다 바퀴를 재발명하고, 프레임워크 X를 써야 할지 프레임워크 Y를 써야 할지 고르는 데 너무 많은 시간을 쓰는 대신, 실제로 일을 끝내고 문제를 해결할 수 있게 해야 한다.
그리고 마지막으로, 오늘의 주제인 보안이 있다. 의존성에는 취약점(비의도적)이나 백도어(의도적)가 있을 수 있으므로, 취약점을 패치하기 위해 자주 업데이트해야 하지만, 백도어가 탐지되기 전에 미끄러져 들어오지 않도록 너무 자주 업데이트해서도 안 된다. 표준 라이브러리는 취약점을 도입할 가능성이 더 낮은 전문가들이 작성한 공통 기반을 제공하며, 거기에 백도어를 넣는 것은 훨씬, 훨씬 더 어렵다.

그래서 Rust에는 확장된 표준 라이브러리가 필요하다.
확장된 표준 라이브러리를 stdx라고 불러 보자. 이것은 언어와 독립적이어야 하며, 그래야 자체 속도로 발전하고 필요할 때 파괴적 변경도 할 수 있다.
이 확장 표준 라이브러리는 rust-lang GitHub 조직 아래에서, 대부분의 개발자가 일상적으로 필요로 하는 많은 크레이트로 구성된 cargo 워크스페이스 형태의 모노레포가 될 수 있다.
다음과 같은 것들로 시작할 수 있다:
xz 공격에서 중요했던 세부 사항 중 하나는 표적들이 소스를 직접 가져오는 대신 미리 빌드된 릴리스를 가져오고 있었다는 점이다. 이 덕분에 공격자는 git 저장소에는 존재하지 않는 몇몇 파일을 릴리스 아카이브 안에 숨길 수 있었다.
Go는 처음부터 올바르게 접근했고, 의존성을 관리하기 위해 중앙집중식 패키지 레지스트리를 사용하지 않았다. 대신 패키지의 소스 코드 위치를 직접 가리켜야 한다.
import (
"example.com/myorganization/mypackage"
)
다시 말하지만, 공급망 보안에 관해서는 Go가 앞서간다.
Go 패키지는 분산되어 있기 때문에(앞에서 보았듯 보안상 훌륭하다), 우리는 여전히 배포되는 소스 코드가 정당하며 모두에게 동일한지 확인할 필요가 있다. 악의적이거나 손상된 코드 플랫폼이 small corp Inc.에는 패키지 A의 내용으로 XXX를 제공하고, big corp Inc.에는 패키지 A의 내용으로 XXX + backdoor를 제공하는 상황은 원하지 않는다.
Go 팀은 매우 우아한 해결책을 내놓았다. 바로 체크섬 데이터베이스다.
이것은 API를 통해 접근 가능한 제3자 서비스로, 방금 다운로드한 패키지의 무결성을 인증하고 검증한다. 여러분의 패키지 체크섬이 다른 모든 사람이 다운로드받는 패키지의 체크섬과 일치하는지, 그리고 여러분에게만 특별히 제공된 백도어 버전이 아닌지를 (자동으로) 검증할 수 있게 해 준다.

불일치가 발생하면 패키지 설치는 실패한다.
나쁜 소식은 우리가 올바른 방향으로 가고 있다는 징후를 전혀 보지 못한다는 것이다. 그래서 공격이 일어날지 여부의 문제가 아니라 언제 일어날지의 문제다. 따라서 여러분은 프로젝트, 사용자, 조직을 보호하기 위해 자신의 운명을 스스로 붙잡아야 하며, 미래 공격의 폭발 반경을 제한하는 것이 여러분의 일이다.
대부분의 공급망 공격 영향을 완화하기 위해 여러분이 할 수 있는 단 하나의 일이 있다면, 그것은 Dev Containers를 사용하는 것이다. 이것은 컨테이너 기반 샌드박싱과 재현 가능한 개발 환경을 가능하게 하기 위해 소프트웨어 프로젝트(git 저장소)에 설정해 두어야 하는 몇 개의 구성 파일이다.
지금 바로 하지 않을 이유는 전혀 없다. 설정하는 데 10분도 채 걸리지 않고, 유지보수 부담도 적으며, 프로젝트에 기여하는 신규 개발자의 생산성도 높여 준다.
Dev Containers를 설정하는 것은 내가 새 프로젝트를 만들거나 기존 프로젝트에 기여하려고 할 때 가장 먼저 하는 일이다.
여러분이 이해해야 할 한 가지는, Dev Containers는 100% 선택 사항이며 보안과 생산성을 오직 높이기만 한다는 점이다. 저장소에 .devcontainer 폴더가 있는 것에는 단점이 전혀 없다. 잡무를 좋아하고 위험하게 살고 싶어 하는 동료가 이를 쓰고 싶지 않다면 괜찮다. 그건 그들의 문제다. 적어도 그들은 프로젝트 작업에 어떤 도구와 라이브러리를 설치해야 하는지 알 수 있는 참고용 Dockerfile은 갖게 된다.
보안 애호가인 내 관점에서 Dev Containers의 가장 큰 이점은 프로젝트를 샌드박싱하는 것이다. 의존성 안에 어떤 악성코드가 숨어 있다 해도 민감한 파일에 접근하지 못한다. AI 에이전트가 통제를 벗어나더라도 rm -rf /를 실행하지 못한다.
편집기를 닫고, 컨테이너를 날려 버리고, 다시 git clone한 뒤 아무 일도 없었다는 듯 계속 살면 된다.

Docker 기반 샌드박싱이 완벽한 것은 아니지만, 그래도 아무것도 없는 것보다는 무한히 낫다.
2026년에는 SSH 키와 다른 비밀 정보를 여러분의 시스템 파일시스템 위에 평문처럼 두고 있을 이유가 없다. 사실상 모든 비밀번호 관리자는 SSH 키와 ssh-agent 통합을 지원하므로, (암호화된) 개인 키가 비밀번호 관리자를 벗어나지 않게 할 수 있으며, 더 나아가 시스템의 보안 요소 밖으로도 나가지 않게 할 수 있다.
토큰을 여러분의 시스템에서 떼어내는 또 다른 방법은 GitHub Actions 같은 CI 파이프라인에서만 패키지의 새 버전을 릴리스하고 게시하는 것이다. 어차피 이것은 좋은 개발 관행이기도 하다.
GitHub Actions의 비밀 정보는 개발자 시스템의 파일보다 유출하기가 훨씬 더 어렵다고 볼 수 있다.
Rust는 의존성의 출처를 재정의하는 방법을 제공한다.
이것은 흔히 공개 패키지를 비공개 포크로 대체할 때 사용되지만, 의존성을 crates.io에서 가져오는 대신 Git 저장소에서 가져오도록 바꾸는 데에도 사용할 수 있다. 이를 위해 Cargo.toml 파일의 [patch.crates-io] 객체를 사용하라.
Cargo.toml
[dependencies]
serde = { version = "1", features = ["derive"] }
[patch.crates-io]
serde = { git = "https://github.com/serde-rs/serde", rev = "fa7da4a93567ed347ad0735c28e439fca688ef26" }
과부하를 피하기 위해, 이 전략은 널리 알려지지 않은 패키지들에 사용하는 것이 가장 좋다. 그런 패키지가 손상되었을 때 더 넓은 커뮤니티가 알아차리기까지 시간이 더 오래 걸릴 수 있기 때문이다.
Rust 커뮤니티는 현재 패키지 관리 시스템의 결함을 인식하고 있으며, 의존성을 검사하는 두 가지 도구를 만들었다.
cargo-audit는 데이터베이스를 대조해 여러분의 의존성 중 일부가 알려진 취약점을 포함하는지, 혹은 버려졌는지를 확인한다.
cargo-vet는 제3자 Rust 의존성이 신뢰할 수 있는 주체에 의해 감사되었는지 보장하기 위한 도구다.
최소한 Google과 Mozilla는 자신들의 감사 결과를 공개적으로 게시하고 있으므로, 이를 cargo-vet에 가져올 수 있다: https://github.com/google/rust-crate-audits 및 https://github.com/mozilla/supply-chain
보안은 최종 상태가 아니라 여정이다, 어쩌고저쩌고...
위에서 언급한 모든 기술은 대부분의 위협에 맞서 방어하는 데 도움이 되겠지만, (보안에는 언제나 하지만 이 있다) 그것만으로는 충분하지 않다. 공격자는 자신의 표적인 여러분에게 도달하기 위한 수십, 수백 가지 다른 방법을 찾아낼 것이다.
선택의 여지는 없다. 설계부터 안전한 시스템을 만들려면, 공격자처럼 생각하고 그들의 전술, 기법, 절차(TTPs)를 배워야 한다.
위협 모델이란 무엇인가? CIA triad란 무엇인가? 악성코드는 어떻게 퍼지는가? 셸코드란 무엇이며 어떻게 개발하는가? 암호학은 어떻게 도움이 되는가?
그 모든 것과 그 이상을 내 책 **Black Hat Rust**에서 배울 수 있다. 예를 들어 안전하고 프로덕션 준비가 된 Rust 코드를 작성하는 방법, 실전 암호학, 보안 엔지니어링, 그리고 특히 웹 서버, 종단간 암호화된 Remote Access Tool, Rust로 작성한 익스플로잇을 만드는 방법 등을 다룬다.
내 말을 무조건 믿지 말고, 독자들이 이 책에 대해 뭐라고 말하는지 직접 살펴보라.
안전하게 지내시길 ✌️