왜 이미지 스캔이 필요한가
컨테이너 이미지는 애플리케이션 코드뿐 아니라 베이스 이미지의 OS 패키지, 언어 런타임, 설치된 라이브러리까지 통째로 배포된다. 문제는 이 계층 대부분을 개발자가 직접 관리하지 않는다는 점이다. node:20 같은 베이스를 그대로 쓰면 그 안에 포함된 수백 개 패키지의 CVE를 사실상 물려받는다. 배포 시점에는 문제없던 이미지도 시간이 지나면 새 취약점이 공개되면서 "가만히 있어도" 위험해진다.
따라서 스캔은 일회성 감사가 아니라 CI 파이프라인에 상시 결합돼야 한다. 빌드할 때마다, 그리고 이미 배포된 이미지도 주기적으로 다시 검사하는 두 축이 필요하다.
스캐너 선택 기준
대표적인 오픈소스 스캐너를 비교하면 다음과 같다. 핵심은 스캔 범위(OS 패키지 vs 언어 의존성), SBOM 지원, CI 통합 난이도다.
| 도구 | OS 패키지 | 언어 의존성 | SBOM | 특징 |
|---|---|---|---|---|
| Trivy | O | O | O | 단일 바이너리, 설정 간단 |
| Grype | O | O | O(Syft 연동) | SBOM 기반 스캔에 강점 |
| Clair | O | △ | △ | 서버형, 레지스트리 연동 |
단발성 CI 스텝이라면 Trivy가 가장 진입 장벽이 낮다. 레지스트리 전체를 상시 관리하려면 Clair 같은 서버형이 유리하다.
GitHub Actions에 Trivy 붙이기
빌드 직후 스캔하고, 심각도가 높은 취약점이 있으면 빌드를 실패시키는 것이 기본 패턴이다.
name: build-and-scan
on: [push]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t app:${{ github.sha }} .
- name: Trivy scan
uses: aquasecurity/[email protected]
with:
image-ref: app:${{ github.sha }}
format: sarif
output: trivy.sarif
severity: CRITICAL,HIGH
exit-code: '1' # 취약점 발견 시 job 실패
ignore-unfixed: true # 패치 없는 건은 제외
- name: Upload to code scanning
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: trivy.sarif
ignore-unfixed는 실무에서 특히 중요하다. 수정 패치가 아직 없는 취약점까지 빌드를 막으면 배포가 멈추기 때문이다.
게이트를 무조건 막지 마라
exit-code: 1로 즉시 차단하는 방식은 초기 도입 시 오탐과 미패치 CVE 때문에 팀 전체가 스캔을 우회하게 만든다. 단계적 도입을 권한다. 처음엔 리포트만 남기고(exit-code: 0), 신규 CRITICAL만 차단하도록 좁힌 뒤, 점차 HIGH까지 확대한다.
예외 관리와 억제
특정 CVE를 근거와 함께 한시적으로 허용하는 억제 파일을 버전 관리에 두면, 무엇을 왜 예외 처리했는지 추적된다.
# .trivyignore
# CVE-2024-XXXX: 취약 함수 미사용, 2026-12-31 재검토
CVE-2024-XXXX
# CVE-2025-YYYY: 벤더 패치 대기 중 (JIRA-1234)
CVE-2025-YYYY
핵심은 만료일과 사유를 반드시 남기는 것이다. 사유 없는 억제는 시간이 지나면 "왜 무시했는지 아무도 모르는" 항목으로 남아 위험을 은폐한다.
배포된 이미지 재스캔
빌드 시점 스캔만으로는 부족하다. 이미 프로덕션에 떠 있는 이미지도 새 CVE 공개에 노출되므로 주기적으로 다시 검사해야 한다. 레지스트리의 이미지 목록을 돌며 스캔하는 스크립트를 크론으로 돌리는 방식이 간단하다.
#!/usr/bin/env bash
set -euo pipefail
REGISTRY="registry.example.com/app"
for tag in $(crane ls "$REGISTRY"); do
echo "== scanning ${REGISTRY}:${tag} =="
trivy image --severity CRITICAL,HIGH \
--ignore-unfixed --scanners vuln \
--format json --output "report-${tag}.json" \
"${REGISTRY}:${tag}" || echo "FAILED: ${tag}"
done
DB는 자동 갱신되지만, CI 캐시 환경에서는 trivy image --download-db-only로 취약점 DB를 미리 최신화해 두면 스캔 결과의 일관성이 올라간다.
정리
이미지 스캔은 "빌드 시 차단"과 "배포 후 재검사" 두 축으로 설계한다. 게이트는 단계적으로 조이고, 예외는 사유·만료일과 함께 코드로 관리한다. 스캔을 통과했다는 사실이 곧 안전을 보장하지는 않지만, 알려진 위험을 방치하지 않는 최소한의 방어선을 자동으로 유지해 준다.