Skip to content
Published on

운영 이미지를 Trivy 로 검사했더니 — 768건 중 실제로 손댈 것은 세 패키지와 Dockerfile 한 줄이었다

공유하기
Authors

왜 검사했나

이 사이트는 파이썬 백엔드 하나가 로그인·블로그·언어 학습·실습 채점을 다 합니다. 의존성은 requirements.lock 에 해시까지 고정되어 있어 "무엇이 들어 있는지" 는 명확하지만, "그 안에 알려진 취약점이 있는지" 는 한 번도 재 본 적이 없었습니다. Trivy 로 두 곳을 검사했습니다.

  • 저장소requirements.txt 의 파이썬 의존성, 쿠버네티스 매니페스트, 비밀 문자열
  • 운영 이미지 — 실제로 떠 있는 컨테이너의 파일시스템(OS 패키지 + 설치된 파이썬 패키지)

둘째 것이 핵심입니다. 저장소만 보면 OS 패키지가 빠지고, 이미지만 보면 어디서 고쳐야 하는지가 안 보입니다.

운영 이미지 안에서 검사하는 방법

이미지 레지스트리(Harbor)는 인증이 필요하고 파드에는 pull secret 이 없습니다(노드가 자격증명을 가집니다). 그래서 레지스트리에서 이미지를 꺼내는 대신, 운영 이미지 자체를 컨테이너로 띄우고 init 컨테이너로 Trivy 바이너리를 넣어 그 안에서 루트 파일시스템을 검사했습니다.

initContainers:
  - name: get-trivy
    image: docker.io/aquasec/trivy:0.58.0
    command: ["cp", "/usr/local/bin/trivy", "/tools/trivy"]
    volumeMounts: [{ name: tools, mountPath: /tools }]
containers:
  - name: scan
    image: 192.168.219.202/labhub/backend:build-634      # 운영과 같은 이미지
    command: ["sh", "-c", "/tools/trivy rootfs --scanners vuln --format json /"]

자격증명 없이 운영 이미지를 검사할 수 있고, 노드가 이미지를 이미 갖고 있어 빠릅니다. 처음에는 runAsUser: 0 을 넣었다가 CronJob 템플릿의 runAsNonRoot 정책에 걸려 파드가 Pending 으로 멈췄습니다 — 빼니 됐습니다. 루트 파일시스템은 일반 사용자로도 읽힙니다.

첫 결과: 935건 — 그중 167건은 검사 도구 자신

{'LOW': 207, 'HIGH': 348, 'MEDIUM': 340, 'CRITICAL': 12, 'UNKNOWN': 28}
대상별: debian 13.6 → 720 · Python → 48 · tools/trivy → 167

세 번째 줄을 보십시오. tools/trivy 167건 — 제가 init 컨테이너로 넣은 Trivy 바이너리(Go 프로그램)의 의존성입니다. 검사 도구가 검사 대상 안에 있으니 자기 자신을 세었습니다. CRITICAL 12건 중 4건(go-git, x/crypto, grpc, Go stdlib)이 그것이었습니다. 빼고 다시 세면 이렇습니다.

검사 도구 제외: {'LOW': 197, 'HIGH': 275, 'MEDIUM': 264, 'CRITICAL': 8, 'UNKNOWN': 24}  = 768건
그중 고칠 판이 있는 것: {'CRITICAL': 1, 'HIGH': 53, 'MEDIUM': 55, 'LOW': 32}          = 141건

768건이라는 숫자는 겁을 주지만, 조치가 정해지는 것은 "고칠 판이 있는가" 로 갈랐을 때입니다. 141건이 손댈 수 있는 것이고, 나머지 627건은 데비안에 아직 수정판이 없는 것입니다(그 안에 CRITICAL 7건 — glib, mbedtls, libxml2, perl-base).

고칠 수 있는 141건은 결국 세 덩어리

심각도어디무엇지금 → 고친 판
CRITICAL파이썬authlib1.4.0 → 1.6.12 (10건)
HIGH파이썬Pillow11.3.0 → 12.3.0 (18건)
HIGH파이썬python-multipart0.0.20 → 0.0.31 (7건)
HIGH파이썬starlette0.41.3 → 0.49.1 이상 (7건, fastapi 가 끌어옴)
HIGHOSutil-linux 계열 9개2.41-5 → 2.41.5-0+deb13u1 (13건)
HIGHOSopenssl 계열 3개3.5.6 → 3.5.7 (10건)
MEDIUM파이썬pip25.0.1 → 26.x (6건)

authlib — 로그인의 서명 검증 우회

가장 먼저 봐야 할 것은 CRITICAL 하나입니다. CVE-2026-27962, JWS 의 JWK 헤더 주입입니다. key=None 으로 토큰을 검증하면 라이브러리가 토큰 안에 들어 있는 jwk 헤더의 키로 서명을 확인합니다. 공격자가 자기 키로 서명하고 그 키를 헤더에 넣으면 통과합니다. 1.6.9 에서 고쳐졌습니다.

이 사이트가 그 경로를 타는지 확인했습니다. authlib 을 부르는 곳은 한 군데입니다.

token = await oauth.google.authorize_access_token(request)

starlette 클라이언트는 구글의 JWKS 로 받은 키를 명시해 ID 토큰을 검증하므로 key=None 경로를 직접 타지는 않습니다. 그래도 그 라이브러리를 그 판으로 두고 있을 이유가 없어 올렸습니다.

OS 패키지 — 다이제스트 고정의 뒷면

util-linux 와 openssl 은 데비안에 수정판이 있는데 왜 이미지에 없었을까요. Dockerfile 이 이렇게 시작했기 때문입니다.

FROM python:3.12-slim@sha256:2c941e86…      # 다이제스트로 고정
RUN apt-get update && apt-get install -y --no-install-recommends fonts-nanum ffmpeg

다이제스트로 고정하면 빌드가 재현되지만, OS 보안 수정이 영원히 들어오지 않습니다. apt-get install 은 새 패키지를 넣을 뿐 이미 있는 것을 올리지 않습니다. 두 줄을 바꿨습니다 — 다이제스트를 지금 것으로 올리고, apt-get upgrade -y 를 넣어 빌드마다 그 시점의 수정을 받게 했습니다.

고친 뒤 다시 재기

같은 Dockerfile 로 로컬에서 빌드해 같은 방법으로 다시 검사했습니다.

고치기 전고친 뒤
전체(검사 도구 제외)768640
고칠 판이 있는 것14113
그중 CRITICAL10
그중 HIGH533
util-linux2.41-52.41.5-0+deb13u1
openssl3.5.6-1~deb13u23.5.7-1~deb13u2

남은 HIGH 3건은 starlette 1.x 에서만 고쳐지는 것이라 fastapi 판올림과 함께 따로 다룹니다. 남은 CRITICAL 7건은 데비안에 아직 수정이 없어 이 이미지로는 닿지 않습니다 — 그것은 "고쳤다" 가 아니라 "기다린다" 로 적어 두어야 합니다.

판올림이 무해하다는 것을 어떻게 아나

세 패키지를 올리고 lock 을 다시 만들면 다른 것도 따라 움직일 수 있습니다. 운영 빌드와 같은 python:3.12 + pip-tools 7.5.1 로 lock 을 다시 만들었더니 바뀐 것은 세 패키지뿐이었습니다.

그 lock 을 --require-hashes 로 설치한 컨테이너에서 전체 시험 2,014개를 돌렸습니다. 4개가 실패했습니다. 여기서 멈추면 "판올림이 무언가를 깼다" 로 읽힐 수 있습니다. 그래서 같은 컨테이너에 원래 lock 을 설치하고 같은 4개를 다시 돌렸습니다 — 똑같이 실패합니다. 로컬 스택(실행 중인 서비스, 훅, 채점 도구)이 필요한 시험들이었습니다. 대조군이 없으면 "무관하다" 는 말은 추정입니다.

Pillow 는 큰 판 하나를 건너뛰는 것이라 사용처를 따로 봤습니다. Image.open, ImageDraw, ImageFont.truetype, ImageCms, ImageOps — 전부 12.x 에 그대로 있습니다.

비밀 탐지: 12건, 실제 유출 0건

Trivy 의 비밀 탐지는 12건을 냈습니다. 하나씩 열어 봤습니다.

탐지실체
RSA 개인키 2개실습 재료 — gen-tokens.py 가 만드는 데모 키(demo-key-id-001)
JWT 토큰 3개실습 본문의 예시 토큰
Slack 웹훅도구 안의 예시 문자열(curl 빌더의 정규식)

12건 모두 실습 교재이거나 예시입니다. 다만 "실습 재료라서 괜찮다" 는 판단은 파일을 열어 본 뒤에만 할 수 있습니다. 목록만 보고 넘기면 진짜가 섞여 있어도 모릅니다.

정리

  • 저장소와 운영 이미지를 둘 다 검사한다. 저장소만 보면 OS 가 빠지고, 이미지만 보면 고칠 자리가 안 보인다.
  • 검사 도구를 컨테이너 안에 넣었다면 결과에서 그 도구 자신을 뺀다. 167건이 그것이었다.
  • 숫자를 겁내지 말고 "고칠 판이 있는가" 로 가른다. 768건 중 141건, 실제 조치는 세 패키지와 Dockerfile 두 줄.
  • 다이제스트로 고정한 베이스 이미지에는 apt-get upgrade 가 없으면 OS 수정이 영원히 안 들어온다.
  • 판올림 뒤 실패한 시험은 원래 판으로 같은 시험을 다시 돌려 대조한다.
  • 비밀 탐지는 열어 보기 전까지 판단하지 않는다.