LabHub
배우기 러닝패스 코스

Container Security

A Clean Scan Does Not Mean It Is Safe

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

한 줄 요약

이미지 스캐너가 하는 일은 두 단계뿐입니다. 레이어를 풀어 설치된 패키지 목록을 만들고, 그 이름과 버전을 취약점 DB 와 대조합니다. 이미지를 실행하지도, 코드를 분석하지도 않습니다.

Concept map: (a) 패키지 메타데이터가 없으면 안 보입니다. · 나타나지 않습니다 · (b) 목록에 있으면 실행 여부와 무관하게 보고됩니다. · Total: 4

왜 이게 필요했나

스캐너를 마법 상자로 여기면 두 가지 잘못된 결론에 도달합니다. "스캔이 깨끗하니 안전하다"와 "1,247건이 나왔으니 다 고쳐야 한다". 둘 다 스캐너가 무엇을 하는지 모르는 데서 나옵니다.

동작 원리를 알면 한계도 자동으로 나옵니다.

(a) 패키지 메타데이터가 없으면 안 보입니다. 데비안 계열이면 /var/lib/dpkg/status, RPM 계열이면 RPM DB, 언어 런타임이면 락파일을 읽습니다. 그래서 Dockerfile 에서 curl | tar xz 로 집어넣은 바이너리는 SBOM 에도 스캔 결과에도 나타나지 않습니다. 이것이 스캔 결과가 깨끗하다는 것이 안전하다는 뜻이 아닌 첫 번째 이유입니다.

(b) 목록에 있으면 실행 여부와 무관하게 보고됩니다. 애플리케이션이 한 번도 호출하지 않는 tar 의 취약점도 그대로 올라옵니다.

어떻게 동작하나

실제 수치로 보면 감이 옵니다. payments:1.4.2 (debian 12.6) 을 스캔하면 Total: 1247 (LOW 812, MEDIUM 289, HIGH 137, CRITICAL 9) 가 나옵니다. 여기에 --ignore-unfixed --severity CRITICAL,HIGH 를 걸면 Total: 4 가 되고, 알려진 악용 목록(KEV)과의 교집합은 1건입니다.

정리하면 이렇습니다. 1,247건이 4건이 됐습니다. 아무것도 안전해지지 않았지만, 지금 조치할 수 있는 항목만 남았습니다.

그리고 진짜 해법은 패치가 아니라 베이스 교체입니다.

베이스 크기 감지 패키지 수정가능 CRITICAL HIGH
node:22 1.12GB 432 9 137
node:22-slim 231MB 118 2 24
node:22-alpine 148MB 41 0 3
distroless/nodejs22 187MB 19 0 1
정적 바이너리 + scratch 24MB 0 0 0

1,247개를 없애는 것은 패치가 아니라 베이스 이미지 교체입니다. 그리고 판단 기준은 이 한 문장입니다 — 애플리케이션이 실제로 링크하는 공유 라이브러리가 8개인데 이미지에 패키지가 432개 들어 있다면, 문제는 취약점이 아니라 이미지 구성입니다.

현장에서 만나는 모습

게이트 정책은 이 딜레마에서 출발합니다. Critical 전부에서 실패시키면 개발이 멈춥니다. 실패시키지 않으면 아무도 보지 않습니다. 실무에서 자리 잡는 타협은 수정본이 존재하는 Critical/High 에서만 실패시키는 것입니다. 고칠 수 없는 것으로 빌드를 세우면 사람들은 게이트를 끄는 법을 먼저 배웁니다. 그리고 예외에는 반드시 만료일(expired_at)을 붙여야 합니다. 예외가 조용히 영구화되는 것이 게이트가 무력해지는 가장 흔한 경로입니다.

우선순위는 CVSS 점수만으로 정해지지 않습니다.

CVE-2021-44228  epss 0.9444
CVE-2023-45853  epss 0.00412
CVE-2024-5535   epss 0.00196

셋 다 CVSS 로는 Critical 등급입니다. 실제 우선순위는 악용 가능성 × 노출 여부 × 도달 가능성으로 정해집니다.

SBOM 포맷은 두 갈래입니다. SPDX 는 Linux Foundation 이 만들고 ISO/IEC 5962:2021 표준이 되었으며 라이선스 컴플라이언스 쪽 뿌리가 깊습니다. CycloneDX 는 OWASP 에서 시작해 ECMA-424 가 되었고 보안 취약점 관리에 초점이 맞춰져 있습니다. 다만 SBOM 의 진짜 용도는 문서 보관이 아니라 사고 당일 5분입니다. 새 취약점이 터졌을 때 40개 서비스 중 어디에 그 패키지가 몇 버전으로 들어 있는지 즉답할 수 있느냐, 그것 하나로 SBOM 의 가치가 결정됩니다.

스캐너의 숫자를 실제 위험으로 바꾸기

처음 스캔을 돌리면 CRITICAL 이 200건씩 나온다. 그 목록을 그대로 티켓으로 만들면 아무도 처리하지 않는다. 줄이는 순서가 필요하다.

첫 번째 질문: 그 취약점이 이 이미지에서 실제로 실행되는가. 베이스 이미지에 들어 있지만 우리 프로그램이 부르지 않는 라이브러리가 대부분이다. 요즘 스캐너는 바이너리가 실제로 그 함수를 참조하는지 보는 기능을 갖고 있고, 이것만 켜도 목록이 크게 줄어든다.

두 번째 질문: 공격자가 그 경로에 닿을 수 있는가. 인터넷에 노출된 서비스의 HTTP 파서에 있는 취약점과, 내부 배치 작업이 쓰는 압축 라이브러리의 같은 점수 취약점은 위험이 다르다. CVSS 점수는 환경을 모른 채 매긴 값이므로 그대로 줄 세우면 안 된다.

세 번째 질문: 고칠 수 있는가. --ignore-unfixed 를 켜면 아직 패치가 없는 항목이 빠진다. 이것은 무시하는 것이 아니라 지금 할 수 있는 일과 지켜볼 일을 나누는 것이다. 패치 없는 항목은 따로 목록을 만들어 두고, 나온 날 처리한다.

trivy image --severity HIGH,CRITICAL --ignore-unfixed myapp:1.2.3
trivy image --format cyclonedx -o sbom.json myapp:1.2.3

대부분은 베이스 이미지를 바꾸면 사라진다. 취약점의 8할은 우리가 쓰지도 않는 OS 패키지에서 나온다. 슬림 이미지나 distroless 로 바꾸면 그 8할이 통째로 없어진다. 개별 CVE 를 하나씩 처리하는 것보다 바닥을 갈아 끼우는 쪽이 언제나 싸다.

정기적으로 다시 굽는 것이 스캔보다 중요하다. 이미지는 만든 날의 패키지를 그대로 들고 있으므로, 배포하지 않으면 취약점은 계속 쌓인다. 코드가 안 바뀌어도 주 1회 다시 빌드하는 파이프라인이 있으면 대부분이 저절로 해결된다.

예외는 기한과 함께 기록한다. "이건 못 고침" 으로 무기한 무시 목록에 넣으면 그 항목은 영원히 남는다. .trivyignore 에 만료일과 이유를 함께 적고, 만료된 항목이 있으면 빌드가 경고하게 만든다.

다음 실습에서 할 것

패키지 인벤토리를 직접 만들어 베이스와 런타임 이미지를 비교하고, 늘어난 공격 표면을 숫자로 셉니다. SBOM 을 손으로 만들어 보고 정책 게이트 스크립트를 작성한 뒤, ENV 로 넣은 토큰이 이미지에 박제되는 것을 확인하고 실행 시점 주입으로 고칩니다.