Опирается на правила:
R-SEC-IMG-1…R-SEC-IMG-4иR-SEC-IMG-X1…R-SEC-IMG-X2из Security Style Guide → раздел 4. Container/image-уязвимости.
Важно знать
- Trivy обязателен для всех Docker-образов — сканирует base image, OS-пакеты, библиотеки.
- HIGH/CRITICAL →
exit-code: 1.ignore-unfixed: true— CVE без патча от upstream не блокируют.- Base image:
eclipse-temurin:21-jre-alpineилиgcr.io/distroless/java21-debian12:nonroot.- Digest-pin обязателен —
@sha256:.... Не:latest, не plain tag без digest для production.- Non-root user
USER 1000:1000в конце Dockerfile. Без — CVE escape → root.- HEALTHCHECK или K8s liveness/readiness — обязательно. Без — зависший JVM не детектируется.
:latestbase image — сборка не воспроизводима.
Контейнер — это полная стек технологий в одном файле: твой Java-код + JVM + libc + OS packages. Уязвимость в любом слое = уязвимость в проде. UCP формулирует: сканер на финальный образ, минимальный base, non-root user, явный digest.
Trivy в CI
R-SEC-IMG-1: после docker build, до docker push.
name: Build and scan
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- name: Scan with Trivy
uses: aquasecurity/trivy-action@0.28.0
with:
image-ref: myapp:${{ github.sha }}
severity: HIGH,CRITICAL
exit-code: 1
ignore-unfixed: true
format: sarif
output: trivy-results.sarif
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: trivy-results.sarif
- name: Push image
if: success()
run: docker push myapp:${{ github.sha }}
Параметры:
severity: HIGH,CRITICAL— только серьёзные. LOW/MEDIUM не блокируют.exit-code: 1— failed scan = failed CI.ignore-unfixed: true— CVE, для которых upstream не выпустил патч, не блокируют. Иначе невозможно собрать никакой образ — всегда есть unfixed CVE в системе.- SARIF output → GitHub Code Scanning, findings в Security tab.
Trivy сканирует:
- OS packages (apk, deb, rpm).
- Language dependencies (Maven, npm, Go modules — то, что есть в image).
- Application dependencies через манифесты (
pom.xml,build.gradleесли включены в layer). - Misconfigurations (Docker best practices) с
--security-checks config.
Base image
R-SEC-IMG-2: минимальный, известный, digest-pinned.
Вариант 1: Eclipse Temurin Alpine
FROM eclipse-temurin:21-jre-alpine@sha256:abc123def456...
Eclipse Temurin — production-ready OpenJDK distribution. Alpine — минимальный Linux (~5 MB), меньше surface attack. JRE-only (не JDK) — нет javac, нет лишних утилит.
Вариант 2: Distroless
FROM gcr.io/distroless/java21-debian12:nonroot@sha256:...
Distroless — образы Google без OS shell, без package manager, без curl. Только JRE + системные библиотеки. Атакёр после RCE не может apt install, не может wget — surface drastically reduced.
:nonroot tag — pre-configured non-root user (UID 65532).
Цена distroless — debugging сложнее (нет sh, нет cat). Для production это правильный trade-off.
Digest-pin
# ХОРОШО
FROM eclipse-temurin:21-jre-alpine@sha256:a1b2c3d4...
# СНОСНО — explicit version tag
FROM eclipse-temurin:21.0.5_11-jre-alpine
# ПЛОХО — drift возможен
FROM eclipse-temurin:21-jre-alpine
# КАТАСТРОФА — невоспроизводимо
FROM eclipse-temurin:latest
FROM openjdk:latest
FROM ubuntu:latest
:latest или mutable tags меняются — сборка из месячной давности commit даёт другой образ. Невозможно reproduce production-version, невозможно debug «работало вчера».
@sha256: digest — immutable. Pin to specific digest, обновляешь явно через PR.
Non-root user
R-SEC-IMG-3: USER 1000:1000 в конце.
FROM eclipse-temurin:21-jre-alpine@sha256:...
RUN addgroup -g 1000 app && adduser -D -u 1000 -G app app
WORKDIR /app
COPY --chown=app:app build/libs/app.jar /app/app.jar
USER 1000:1000
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
Что даёт non-root:
- CVE escape (sandbox break через JVM bug) → попадаешь в контейнер как
app, не какroot. - Нет доступа к privileged operations (mount, kernel modules).
- К metadata service (AWS EC2 instance metadata, GCP metadata) часто привязан root access — non-root ограничивает.
Distroless :nonroot tag — это уже сделано.
В K8s — дополнительно securityContext:
spec:
containers:
- name: app
securityContext:
runAsNonRoot: true
runAsUser: 1000
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true — даже attacker внутри контейнера не может писать на disk. drop: ALL capabilities — нет даже CAP_NET_RAW.
HEALTHCHECK или K8s probes
R-SEC-IMG-4: runtime-проблемы детектируются.
Вариант 1: HEALTHCHECK в Dockerfile
HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \
CMD wget --quiet --tries=1 --spider http://localhost:8081/actuator/health/liveness || exit 1
При unhealthy — Docker помечает контейнер, можно настроить auto-restart.
Вариант 2: K8s liveness/readiness
spec:
containers:
- name: app
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8081
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8081
initialDelaySeconds: 5
periodSeconds: 5
Подробнее — Observability → health checks.
В K8s deployments — обычно достаточно probes, HEALTHCHECK в Dockerfile дублируется. Для docker-compose / standalone — HEALTHCHECK нужен.
Без него — зависший JVM (deadlock, native memory leak, GC overhead) не детектируется, traffic продолжает идти на dead pod.
Что запрещено
USER root или отсутствие USER
R-SEC-IMG-X1:
FROM eclipse-temurin:21-jre-alpine
COPY app.jar /app/app.jar
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
# нет USER — запуск от root
В контейнере процесс работает как root. RCE → root в контейнере → escape к host (через CVE в Docker / runc) → root на host. Catastrophic.
:latest base image
R-SEC-IMG-X2:
FROM openjdk:latest
Через месяц openjdk:latest — другой образ. Production build не воспроизводим. Невозможно roll-back, невозможно forensic analysis «что было в production вчера».
Always digest-pin или explicit version.
Что запрещено — таблица
| Антипаттерн | Правило | Что взамен |
|---|---|---|
USER root или отсутствие USER | R-SEC-IMG-X1 | USER 1000:1000 |
:latest base image | R-SEC-IMG-X2 | digest-pin (@sha256:...) |
| Без Trivy в CI | R-SEC-IMG-1 | scan после build, до push |
Trivy без exit-code: 1 | R-SEC-IMG-1 | блокировать на HIGH+ |
Trivy без ignore-unfixed | R-SEC-IMG-1 | unfixed — отдельная задача |
| Без HEALTHCHECK / probes | R-SEC-IMG-4 | минимум K8s probes |
JDK image для production (:21-jdk) | R-SEC-IMG-2 | :21-jre (JRE-only) |
Без securityContext в K8s | R-SEC-IMG-3 | runAsNonRoot: true + capabilities drop |
CMD без ENTRYPOINT | R-SEC-IMG-2 | явный ENTRYPOINT |
Куда дальше
- Security → раздел 4. Container/image — нормативные формулировки.
- SAST по коду — code-level vulnerabilities.
- CVE в зависимостях — Java deps, не OS packages.
- Секреты в коде и истории — не в Docker layers через ENV.
- Реакция на findings — SLA per severity.
- Observability → health checks — liveness vs readiness.
- Graceful shutdown — Docker SIGTERM, K8s terminationGracePeriod.