Опирается на правила:
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-пакеты, Python-пакеты из
requirements.txt/uv.lock.- HIGH/CRITICAL →
exit-code: 1.ignore-unfixed: true— CVE без патча от upstream не блокируют, идут в отдельную задачу.- Base image:
python:3.12-slimилиgcr.io/distroless/python3-debian12:nonroot; никогдаpython:latest,ubuntu:latest,:latestбез digest.- Digest-pin обязателен —
@sha256:.... Mutable-тег со временем меняется; сборка двухнедельной давности даёт другой образ.- Non-root user
USER 1000:1000в конце Dockerfile. Без — CVE → escape → root в контейнере → доступ к metadata-service.- HEALTHCHECK или K8s liveness/readiness probe — обязательно; без них зависший uvicorn-воркер не детектируется.
- Multi-stage build — устанавливаем зависимости в builder, копируем site-packages в финальный образ; pip, setuptools и build-инструменты не попадают в production-layer.
Контейнер — это полный стек в одном файле: твой FastAPI-сервис + CPython + libc + OS packages. Уязвимость в любом слое — уязвимость в проде. Правила R-SEC-IMG-* задают: сканер на финальный образ, минимальный 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 order-service:${{ github.sha }} .
- name: Scan with Trivy
uses: aquasecurity/trivy-action@0.28.0
with:
image-ref: order-service:${{ 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 order-service:${{ github.sha }}
Параметры:
severity: HIGH,CRITICAL— только серьёзные. LOW/MEDIUM не блокируют.exit-code: 1— failed scan = failed CI (R-SEC-1).ignore-unfixed: true— CVE без патча от upstream откладываются в задачу, не блокируют сборку. Без этого флага собратьpython:3.12-slimбез единого CVE практически невозможно.- SARIF output → GitHub Code Scanning, findings в Security tab (
R-SEC-FIND-3).
Trivy на Python-образе сканирует:
- OS packages (deb/apk — зависит от base).
- Python dependencies — разбирает
site-packagesвнутри образа, идентифицирует пакеты черезMETADATA. - Misconfigurations — Docker best practices (
--security-checks config).
Важно: pip-audit сканирует PyPA Advisory DB на этапе сборки (dep-слой), Trivy — финальный образ включая транзитивные OS-зависимости. Оба нужны.
Base image
R-SEC-IMG-2: минимальный, известный, digest-pinned.
Вариант 1: python:3.12-slim
FROM python:3.12-slim@sha256:a1b2c3d4e5f6...
slim — Debian без лишних пакетов; остаются системные библиотеки, необходимые CPython. Меньше surface attack, чем полный python:3.12. Есть sh и базовые утилиты — удобнее для отладки, чем distroless.
Вариант 2: Distroless Python
FROM gcr.io/distroless/python3-debian12:nonroot@sha256:...
Distroless — образы Google без OS shell, без package manager, без curl. Только CPython + системные библиотеки. После RCE атакующий не может apt install, не может wget. :nonroot — pre-configured non-root user (UID 65532).
Цена distroless — отладка сложнее: нет sh, нет cat. Для production это правильный выбор.
Multi-stage build
FROM python:3.12-slim@sha256:a1b2c3d4e5f6... AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip install --no-cache-dir --target=/build/deps -r requirements.txt
FROM python:3.12-slim@sha256:a1b2c3d4e5f6...
RUN addgroup --gid 1000 app && adduser --uid 1000 --gid 1000 --no-create-home --disabled-password app
WORKDIR /app
COPY --from=builder /build/deps /app/deps
COPY --chown=app:app src/ /app/src/
ENV PYTHONPATH=/app/deps
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
USER 1000:1000
EXPOSE 8000
ENTRYPOINT ["python", "-m", "uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]
builder-stage устанавливает зависимости; финальный stage берёт только site-packages. pip, setuptools, wheel не попадают в production-слой — меньше пакетов, меньше CVE.
Digest-pin
# ХОРОШО
FROM python:3.12-slim@sha256:a1b2c3d4e5f6...
# СНОСНО — explicit version tag
FROM python:3.12.9-slim
# ПЛОХО — тег мутирует
FROM python:3.12-slim
# КАТАСТРОФА
FROM python:latest
FROM ubuntu:latest
:latest или mutable tags меняются независимо от кода — сборка двухнедельной давности даёт другой образ. Digest (@sha256:) immutable; обновляешь явно через PR (Renovate умеет обновлять digest автоматически).
Non-root user
R-SEC-IMG-3: USER 1000:1000 в конце Dockerfile.
FROM python:3.12-slim@sha256:a1b2c3d4e5f6...
RUN addgroup --gid 1000 app \
&& adduser --uid 1000 --gid 1000 --no-create-home --disabled-password app
WORKDIR /app
COPY --chown=app:app . /app/
RUN pip install --no-cache-dir -r requirements.txt
USER 1000:1000
EXPOSE 8000
ENTRYPOINT ["python", "-m", "uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]
Что даёт non-root:
- CVE escape через баг в CPython или uvicorn → попадаешь в контейнер как
app, неroot. - Нет доступа к privileged operations (mount, netlink, kernel modules).
- AWS/GCP metadata service (169.254.169.254) часто ограничен по UID — non-root снижает шанс достать Instance Metadata.
Для сервисов типа OrderService или CustomerService — никакой причины запускаться от root нет.
В K8s — дополнительно securityContext:
spec:
containers:
- name: order-service
securityContext:
runAsNonRoot: true
runAsUser: 1000
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true — даже после проникновения нельзя записать на диск. drop: ALL capabilities — нет даже CAP_NET_RAW. Для ProductService с FastAPI это safe default.
HEALTHCHECK или K8s probes
R-SEC-IMG-4: runtime-проблемы детектируются.
Вариант 1: HEALTHCHECK в Dockerfile
FastAPI-сервис с эндпоинтом /health:
from fastapi import FastAPI
app = FastAPI()
@app.get("/health")
async def health() -> dict[str, str]:
return {"status": "ok"}
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
wget может отсутствовать в slim-образе. curl — тоже. Fallback на python -c с urllib.request безопасен, python всегда есть в образе.
Вариант 2: K8s liveness/readiness
spec:
containers:
- name: order-service
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 15
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 8000
initialDelaySeconds: 5
periodSeconds: 5
from fastapi import FastAPI
from contextlib import asynccontextmanager
_ready = False
@asynccontextmanager
async def lifespan(app: FastAPI):
global _ready
# инициализация: DB-пул, кэш
_ready = True
yield
_ready = False
app = FastAPI(lifespan=lifespan)
@app.get("/health")
async def liveness() -> dict[str, str]:
return {"status": "ok"}
@app.get("/ready")
async def readiness() -> dict[str, str]:
if not _ready:
from fastapi import HTTPException
raise HTTPException(status_code=503, detail="not ready")
return {"status": "ready"}
liveness — JVM-аналог: процесс жив. readiness — готов принимать трафик. В K8s deployments probes достаточно; HEALTHCHECK дублировать не обязательно. Для docker-compose и standalone — HEALTHCHECK нужен.
Без probe зависший uvicorn-воркер (deadlock, event-loop saturation, OOM) не детектируется. Kubernetes продолжает слать трафик на dead pod.
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
USER root или отсутствие USER | R-SEC-IMG-X1 | USER 1000:1000 в конце Dockerfile |
:latest base image | R-SEC-IMG-X2 | digest-pin (@sha256:...) |
| Без Trivy в CI | R-SEC-IMG-1 | scan после build, до push |
Trivy с --exit-zero или без exit-code: 1 | R-SEC-IMG-1 | exit-code: 1 на HIGH/CRITICAL |
Trivy без ignore-unfixed: true | R-SEC-IMG-1 | unfixed — отдельная задача, не блокирует |
| Без HEALTHCHECK / probes | R-SEC-IMG-4 | минимум K8s probes или HEALTHCHECK |
| pip install в финальном stage (не multi-stage) | R-SEC-IMG-2 | builder-stage, копировать только deps |
Без securityContext в K8s | R-SEC-IMG-3 | runAsNonRoot: true + capabilities.drop: ALL |
PYTHONDONTWRITEBYTECODE не выставлен | R-SEC-IMG-2 | ENV PYTHONDONTWRITEBYTECODE=1 (нет .pyc в image) |
Куда дальше
- SAST по коду — bandit, semgrep, ruff
S-правила; code-level vulnerabilities. - CVE в зависимостях — pip-audit, Renovate; Python deps, не OS packages.
- Секреты в коде и истории — Gitleaks; не хранить в Docker layers через
ENV. - Реакция на findings — SLA по severity; suppressions со сроком.
- Криптография — argon2-cffi,
secrets, AES-GCM; что запрещено в Python.