Як сканувати Docker образи на вразливості?
Vulnerability-сканування відповідає на «чи мої image містять відомі security-діри?» Сучасний tooling робить це near-zero-effort кроком у CI. Скіпання означає шиппінг відомих CVE у прод.
Теорія
TL;DR
- Scanner інспектує встановлені пакети image (OS + мова) і матчить їх проти CVE-баз (NVD, vendor-advisory, GitHub Security Advisories).
- Три популярні tools: Trivy (Aqua), Grype (Anchore), Docker Scout (Docker Inc).
- Severities: LOW, MEDIUM, HIGH, CRITICAL. Більшість команд gate на HIGH+ або CRITICAL.
- Три моменти сканування: build-time (CI gate), admission-time (cluster gate), continuously (ловити нові CVE у вже-running image).
- Дві CVE-категорії: OS-level (фікс через base-image update) і app-level (фікс через package-update).
- Завжди парь з
.trivyignoreабо еквівалентом для задокументованих винятків.
Trivy (найпопулярніший)
# Scan локального image
trivy image myorg/api:1.0
# Завали на HIGH або CRITICAL
trivy image --severity HIGH,CRITICAL --exit-code 1 myorg/api:1.0
# Ігнорувати unfixed CVE (немає patch, чекаємо upstream)
trivy image --ignore-unfixed myorg/api:1.0
# Формати output
trivy image --format json myorg/api:1.0
trivy image --format sarif --output report.sarif myorg/api:1.0Trivy opinionated і швидкий. Вбудовані CI-флаги. Дефолт severity це усе; прод-CI зазвичай фільтрує на HIGH+.
Grype
grype myorg/api:1.0
grype --fail-on high myorg/api:1.0Від Anchore. Трохи інша DB; іноді ловить, що Trivy пропускає (і навпаки). Хороший для second-opinion-сканів.
Docker Scout
docker scout cves myorg/api:1.0
docker scout recommendations myorg/api:1.0 # пропонує base-image-upgrade для фіксу CVEScanner Docker Inc. Тісно інтегровано з Docker CLI і Docker Hub. Команда recommendations унікальна і корисна: каже точно, який base-image bump фіксить які CVE.
CI-інтеграція
# GitHub Actions з Trivy
- name: Build image
run: docker build -t myorg/api:${{ github.sha }} .
- name: Scan with Trivy
uses: aquasecurity/trivy-action@master
with:
image-ref: myorg/api:${{ github.sha }}
severity: 'HIGH,CRITICAL'
exit-code: '1'
ignore-unfixed: true
- name: Upload SARIF to GitHub
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: 'trivy-results.sarif'Заливає у GitHub Code Scanning-tab, вразливості з'являються поряд з рештою code-quality сигналів.
Multi-stage scan під час білду
# syntax=docker/dockerfile:1.7
FROM golang:1.23-alpine AS build
# ... build app ...
FROM aquasec/trivy:0.59.0 AS scan
COPY / /scan-target
RUN trivy filesystem --severity HIGH,CRITICAL --exit-code 1 /scan-target
FROM alpine:3.21
COPY /out/server /server
ENTRYPOINT ["/server"]Стейдж scan крутиться як частина білду. Білд падає, якщо HIGH/CRITICAL CVE знайдено. Не може бути скіпнуто careless CI-config.
Admission-time-сканування
Kubernetes admission-controllers можуть відмовити деплою unscanned або vulnerable image:
- OPA Gatekeeper / Kyverno з політиками, що перевіряють Cosign-attestation scan-результатів.
- Trivy Operator для K8s — continuously сканує running-image.
- Cosign + sigstore-policy-controller — верифікує, що image було підписано і attested як scanned.
Pipeline: CI підписує attestation («це image було scanned at time X, found N HIGH CVE»); admission верифікує attestation і перевіряє policy.
Continuous re-scanning
CVE, розкритий сьогодні, впливає на image, що ти зібрав минулого тижня. Байти ті самі; threat-landscape змінився.
# Cron-driven re-scan у проді
0 6 * * * trivy image --severity HIGH,CRITICAL myorg/api:current 2>&1 | mail security@example.comАбо continuous-scanner (Trivy Operator, Snyk Container, Sysdig Secure), що стежить за running-image і re-scan'ить періодично.
OS vs app CVE
Результати зазвичай падають у дві групи:
OS-level (alpine:3.20 → CVE у libssl):
- Фікс: bump base-image (
alpine:3.21), rebuild. - Або чекай, поки OS-vendor опублікує patched-version.
App-level (requests==2.30.0 → CVE у requests):
- Фікс: update залежності у твоєму
requirements.txt/package.json/go.mod. - Часто бере довше, якщо нова версія має breaking changes.
Sканери виявляють обидва. Шлях фіксу різний.
Типові помилки
Сканування без gating
- run: trivy image myorg/api:${{ github.sha }} || true # ← завжди проходитьРезультати показано, але не валять білд. Деви ігнорують. Через місяці у тебе сотні HIGH CVE у проді. Завжди постав --exit-code 1 на реальний severity-поріг.
Сканування лише при build-time
HIGH CVE, розкритий минулої ночі, зараз у твоєму прод-image. Твій CI пройшов учора; реальність змінилася за ніч. Додай nightly re-scan running-image.
Не підтримувати .trivyignore
Реальні CVE іноді не мають patch («won't fix»). Або толеруєш їх, або замінюєш пакет. Документуй рішення у .trivyignore:
# .trivyignore
CVE-2023-12345 # Нема patch, mitigated через network-policy. Re-evaluate quarterly.Довіряти одному scanner
Різні scanner мають різні DB. CVE у твоїй image-базі може бути відсутній в одного scanner. Для high-stakes роботи крути два scanner і union результати.
Ігнорувати SBOM
Scanner-результат без SBOM це просто «ці CVE існують». З SBOM (Software Bill of Materials) у тебе точний список того, що в image, queryable пізніше: «які image містять log4j?». Генеруй SBOM під час білду (docker buildx build --sbom=true), зберігай з image.
Реальний setup
Мінімум
- Trivy у CI, gate на HIGH/CRITICAL.
.trivyignoreдля задокументованих винятків.- Готово.
Зрілий
- Trivy у CI + scan-стейдж у Dockerfile.
- SBOM-генерація + зберігання.
- Cosign-attestation scan-результатів.
- Admission-controller верифікує attestation.
- Nightly re-scan running-image.
- Trivy Operator на K8s.
- Findings → Slack-канал + ticket-створення.
Питання для поглиблення
Q: Яка різниця між CVE і vulnerability?
A: CVE (Common Vulnerabilities and Exposures) це публічний ID для відомого security-issue. Vulnerability це лежачий баг. Більшість сучасних scanner репортять по CVE-ID, з severity, scored через CVSS.
Q: Чи scanner може знайти zero-day?
A: Ні, вони знають лише про публічно розкриті вразливості. Zero-day за визначенням ще не відомі. Сканування це один шар; доповни runtime-monitoring, sandbox-testing і code-review.
Q: Що таке SBOM і навіщо генерувати?
A: Software Bill of Materials — список кожного пакета і версії у твоєму image. SPDX або CycloneDX формат. З SBOM можеш пізніше відповісти «які з моїх image містять $vulnerable_package» без re-pull і re-scan.
Q: Як обробляти CVE без доступного фіксу?
A: Документуй у .trivyignore з обґрунтуванням і re-evaluation-датою. Mitigate через runtime-control (network-policy, capability-drops). Re-check quarterly.
Q: (Senior) Як побудувати scanning-pipeline, що запобігає регресіям?
A: Три gate: (1) CI-build падає на новий HIGH/CRITICAL — навіть fixing-an-unrelated-bug PR не може merge, якщо вводить новий CVE. (2) Admission-control відкидає image без свіжого підписаного scan-attestation. (3) Прод runtime-scanner alert'ить на нові CVE проти running-image і тригерить remediation-workflow (auto-PR до bump base-image, ticket до dev-команди). Результат: будь-який CVE, що дістав прод, був або свідомо проігнорований (у .trivyignore з sign-off), або розкритий після deploy з tracked-response-window.
Приклади
Trivy у GitHub Actions
name: ci
on: [push]
jobs:
build-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: docker build -t myorg/api:${{ github.sha }} .
- name: Trivy scan (gate)
uses: aquasecurity/trivy-action@master
with:
image-ref: myorg/api:${{ github.sha }}
severity: 'HIGH,CRITICAL'
ignore-unfixed: true
exit-code: '1'
- name: Trivy scan (full report, ніколи не падає)
if: always()
uses: aquasecurity/trivy-action@master
with:
image-ref: myorg/api:${{ github.sha }}
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'LOW,MEDIUM,HIGH,CRITICAL'
- name: Upload SARIF
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: 'trivy-results.sarif'Два проходи: gating на HIGH+, повний звіт залито для видимості.
Multi-stage з вбудованим scan
# syntax=docker/dockerfile:1.7
FROM golang:1.23-alpine AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /out/server ./cmd/server
FROM aquasec/trivy:0.59.0 AS scan
COPY /out /scan
RUN trivy filesystem --severity HIGH,CRITICAL --exit-code 1 --ignore-unfixed /scan
FROM scratch
COPY /out/server /server
USER 65532:65532
ENTRYPOINT ["/server"]docker build падає, якщо HIGH/CRITICAL CVE знайдено. Можна обійти через --target для runtime-стейджу при потребі, але дефолтний build-шлях enforce'ить gate.
Порівняти два scanner
#!/bin/bash
IMG=$1
echo "=== Trivy ==="
trivy image --severity HIGH,CRITICAL $IMG --no-progress --quiet
echo "=== Grype ==="
grype $IMG --output table --quietРізні scanner flag'ують різні речі. Для high-stakes image union це твоя true risk-картина.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.
Коментарі
Ще немає коментарів