PyPI 공급망 사고는 평소 쓰던 패키지의 특정 버전이 악성 페이로드가 심긴 상태로 공개 저장소에 올라가, 그 짧은 창에 설치나 업그레이드를 돌린 환경이 그대로 감염되는 사건이다. 2026년 3월 24일 여러 LLM 제공자 키가 한 호스트에 모이는 AI 게이트웨이 패키지에서 이 일이 났고, 벤더는 원인을 추정으로만 적은 채 조사 진행 중이라고 밝히고 있다.
앞선 npm 사고들과 갈라지는 지점은 셋이다. 악성 페이로드가 패키지 코드에 들어간 데 더해 한 버전에는 파이썬 시작 시 자동 처리되는 .pth 파일까지 포함됐고, npm의 postinstall은 설치 스크립트를 끄면 막을 수 있었지만 .pth에는 그에 해당하는 차단 스위치가 없다. 영향 대상이 LLM 키가 모이는 AI 게이트웨이 패키지였고, 수집 범위는 그 호스트의 자격증명 전반이었다. 그리고 사고 시점은 2026년 3월 24일, 이 글은 6개월 뒤다 — 당시 빌드한 컨테이너 이미지·빌드 캐시·가상환경이 그대로 돌고 있다면 지금도 현재진행 사건이다.
이 글은 자주 발생하는 실수 패턴, 점검·조치 절차, 자가진단을 정리했다.
한눈에 보는 핵심
Q. 패키지를 설치만 하고 쓰지 않았으면 안전한가요? A. 안전하다고 단정할 수 없습니다. 일반 원리로, CPython 공식 문서는 .pth 파일에서 import로 시작하는 행이 실행되며 그 행은 해당 모듈을 실제로 쓰는지와 무관하게 파이썬이 시작될 때마다 돌아간다고 명시합니다. 다만 이 사고에서 확인된 것은 벤더 공지가 v1.82.8에 litellm_init.pth 가 있었다는 사실을 침해 지표로 열거했다는 점까지이고, 그 파일이 무엇을 실행했는지는 공개되지 않았습니다. 악성 페이로드 자체는 두 버전 모두 패키지 코드에 있었다고 공지가 적습니다. Q. 노출 창이 40분뿐인데 걸렸는지 어떻게 판단하나요? A. 벤더 공지는 악성 버전이 2026년 3월 24일 10:39 UTC부터 약 40분간 게시된 뒤 PyPI에 의해 격리됐다고 밝혔습니다. 다만 벤더가 제시한 영향 판정 창은 더 넓습니다 — 같은 날 10:39~16:00 UTC, 한국시간 19시 39분부터 이튿날 01시 사이에 pip install 또는 upgrade를 돌렸거나, 버전 핀 없이 설치해 1.82.7·1.82.8을 받았거나, 그 구간에 해당 패키지 설치가 포함된 도커 이미지를 빌드했거나, 다른 패키지가 핀 없는 전이 의존성으로 끌어온 경우입니다. CI 빌드 로그와 이미지 빌드 시각을 이 구간과 대조하면 되고, 로그 보존기간이 지났어도 판정할 방법이 있습니다 — 두 버전은 현재 PyPI 릴리스 인덱스에 없으므로(1.82.6 있음, 1.82.7 없음, 1.82.8 없음, 1.83.0 있음) 설치 목록에 1.82.7이나 1.82.8이 보이면 그 자체가 확정 침해 지표입니다. Q. 우리는 공식 도커 이미지로 운영하는데도 전부 로테이트해야 하나요? A. 공지는 영향 없는 경로를 따로 명시합니다 — 공식 도커 이미지(ghcr.io/berriai/litellm) 사용 환경, 벤더 매니지드 클라우드 사용 환경, GitHub 소스 설치, 그리고 v1.82.6 이하에서 해당 창에 업그레이드하지 않은 환경입니다. 프록시 도커 경로는 의존성을 requirements.txt로 고정해 배포하기 때문입니다. 반대로 URL 기반 외부 점검으로는 이 판정이 불가능합니다 — 공개 영역 응답만 보므로 설치된 패키지 버전이나 .pth 파일 존재는 범위 밖이고, 설치 목록·빌드 로그 대조와 로테이트는 그 호스트에 접근 가능한 팀이 직접 해야 합니다.자주 발생하는 실수 패턴
| # | 패턴 | 위험도 |
|---|---|---|
| 1 | .pth 는 설치 산출물이라 설치 스크립트 비활성화로 막히지 않는다 — npm 의 ignore-scripts 같은 차단 스위치를 기대하고 방치 | 치명 |
| 2 | import 하지 않았으니 안전하다고 판단 — .pth 의 import 행은 인터프리터 시작마다 처리된다 | 치명 |
| 3 | 가상환경·컨테이너 레이어마다 같은 .pth 가 복제되는데 한 곳만 지우고 종료 처리 | 치명 |
| 4 | site-packages 가 여러 개(venv·시스템·사용자)인데 한 인터프리터만 보고 clean 판정 | 높음 |
| 5 | 게이트웨이 호스트 환경변수에 여러 LLM 제공자 키와 클라우드 키를 평문으로 모아둠 | 치명 |
| 6 | 빌드 컨테이너에서 설치 단계 외부 통신을 기본 허용 — 설치 중 나가는 요청이 차단되지 않는다 | 높음 |
공급망 방어 설정
# 1) 버전 핀 + 해시 고정 — 배포 파일이 바뀌면 설치 자체가 실패한다
# requirements.txt 는 pip-compile --generate-hashes 로 생성한다
pip install --require-hashes -r requirements.txt
# 2) 설치 전에 휠 안에 .pth 가 있는지 본다 (= import 없이도 시작 시 처리된다)
# 특정 패키지 전용 기법이 아니라 모든 의존성에 쓰는 사전 확인이다
pip download --no-deps --only-binary :all: -d ./_pkg '<패키지>==<버전>'
unzip -l ./_pkg/*.whl | grep -E '\.pth$' # 걸리면 내용을 직접 열어 확인
# 3) 설치 단계 외부 통신 기본 차단 — 이 사고의 반출 도메인은 벤더 공식 도메인이 아니었다
docker build --network none -t app:ci . # 의존성은 사내 미러에서 받는다
# 4) 이미지·가상환경을 다시 만들 때의 기준선
# 공지가 클린으로 확인한 재배포 기준선 이상에서 '현재' 최신 안정 버전을 고정한다.
# 사고 당시 기준선을 그대로 핀하면 반년 묵은 릴리스를 고정하게 된다.
공급망에서는 동작하는 설치와 검증된 설치가 다르다.
버전 범위·미핀 의존성은 빌드마다 다른 바이트를 끌어오므로, 해시 고정과 설치 단계 egress 차단을 파이프라인 레벨에서 강제해야 한다.
자가진단
# 0) 벤더가 1차로 안내한 확인 경로
pip show litellm # SDK 사용 환경
# AI 게이트웨이(프록시) 사용 환경은 프록시 base URL 에서 설치 버전을 확인한다
# 1) 악성 버전이 남긴 .pth 가 있는가 — 범위 우선, 전체 검색은 보조
find "$(python -c 'import sysconfig;print(sysconfig.get_paths()["purelib"])')" -name 'litellm_init.pth'
python -c "import sysconfig,glob;[print(p) for k in ('purelib','platlib') for p in glob.glob(sysconfig.get_paths()[k]+'/*.pth')]"
find / -name 'litellm_init.pth' 2>/dev/null # 다른 venv·이미지 레이어 누락 방지
# 2) 설치된 버전이 1.82.7 또는 1.82.8 인가
# 두 버전은 현재 PyPI 릴리스 인덱스에 없다(1.82.6 있음 / 1.82.7 없음 / 1.82.8 없음 / 1.83.0 있음).
# 따라서 아래에 그 버전이 보이면 그 자체가 확정 침해 지표다.
pip list --format=freeze 2>/dev/null | grep -i '^litellm=='
# 3) 벤더 판정 창(2026-03-24 10:39~16:00 UTC = KST 19:39~익일 01:00)에 설치가 돌았는가
ls -l --time-style=full-iso "$(python -c 'import sysconfig;print(sysconfig.get_paths()["purelib"])')" | grep -i litellm
# 4) 공지가 제시한 두 침해 지표 도메인으로 나간 흔적 (DNS·프록시·방화벽 로그)
grep -riE 'models\.litellm\.cloud|checkmarx\.zone' /var/log 2>/dev/null
위 시그널이 발견되면 결함이다. LiteLLM 공식 보안 공지 — Suspected Supply Chain Incident (2026-03-24, 조사 진행 중)를 함께 참조하라.
조치 절차
| 순위 | 조치 |
|---|---|
| 1 | litellm_init.pth 를 범위 우선으로 검색해 제거 — 운영 호스트 site-packages 를 먼저 보고, 이어서 컨테이너 이미지·빌드 캐시·CI 러너 캐시·로컬 가상환경까지 전부. 레이어마다 복제되므로 한 곳 제거로 끝내지 않는다 |
| 2 | 설치 버전을 v1.82.6 이하 또는 공지가 클린으로 확인한 재배포 기준선 v1.83.0 이상으로 올리고, 실무에서는 그 이후 최신 안정 버전을 고정한다 — 2026-10-06 실측 최신은 1.104.0 이므로 1.83.0 을 그대로 핀하면 반년 묵은 릴리스를 고정하게 된다 |
| 3 | 판정 창에 설치가 돌았거나 1.82.7·1.82.8 이 확인되면 그 호스트 자격증명 전부를 침해로 간주하고 로테이트 — 환경변수에 든 LLM 제공자 키, 클라우드 액세스 키(AWS·GCP·Azure), SSH 키, 쿠버네티스 토큰, DB 비밀번호 |
| 4 | 환경별·CI/CD 파이프라인별 설치 버전 이력을 감사해 영향 범위를 확정하고, 공지가 명시한 무영향 경로(공식 도커 이미지·매니지드 클라우드·소스 설치·창 밖 구버전 유지)는 과잉 로테이트 대상에서 제외한다 |
| 5 | DNS·프록시·방화벽 로그에서 models.litellm.cloud 와 checkmarx.zone 두 도메인 통신을 모두 역추적하고, 빌드·설치 단계 egress 를 허용 목록화한다 |
| 6 | 재발 방지: --require-hashes 적용, 신규 릴리스 수일 지연 반영, 설치 전 휠 내 .pth 확인 절차화. 이 사고는 상태가 조사 진행 중이므로 벤더 공지 갱신을 계속 추적한다 |
지금 해야 할 것
실무자: 설치 목록·빌드 로그 대조와 자격증명 로테이트는 그 호스트에 접근 가능한 팀이 직접 해야 한다. CodeScan 무료 스캔은 이 사고의 탐지 범위가 아니며, 같은 호스트의 공개 영역 노출 점검은 별건이다.