Як реалізувати security hardening Docker контейнерів?
Container-security-hardening це практика зменшення attack-surface і blast-radius для containerized-workload. Container це process-group зі shared kernel-access; якщо скомпрометований, attacker за один kernel-bug від host. Hardening мінімізує цей ризик через least-privilege, immutable-filesystem, capability-drop і сильні isolation-policy. CIS Docker Benchmark кодифікує checklist.
Теорія
TL;DR
- Build-time: non-root
USER, мінімальний base (distroless/scratch), без secrets у layer, scan на CVE. - Run-time:
--read-only,--cap-drop=ALL,no-new-privileges, seccomp-profile, user-namespace remapping. - Host: kernel hardened, Docker-daemon TLS-only, audit-logs, AppArmor/SELinux.
- Secrets: Docker-secrets / mounted-file / external-vault. Ніколи env-var.
- Network: isolated user-defined-мережі, без
--network=hostдля app-workload. - Image-supply: підписані images (Docker Content Trust, cosign), private-registry з RBAC.
Чому container'и потребують hardening
Container-isolation не security-boundary за замовчуванням. Kernel shared. За замовчуванням, container крутиться як root всередині, що на Linux значить повні capability, якщо не drop'нуті. Невірно сконфігурований container може escape через:
- Privileged-mode
- Mount
/var/run/docker.sock - Kernel-exploit (рідко, але реально)
- Уразливий application-код + writable-filesystem
- Надмірні capability (CAP_SYS_ADMIN, CAP_NET_ADMIN)
Hardening прибирає most-common escape-path.
Hardening-піраміда
┌─────────────────┐
│ Image-signing │ ← supply-chain
├─────────────────┤
│ CVE-scanning │
├─────────────────┤
│ Minimal base │
├─────────────────┤
│ Non-root USER │ ← build-time
├─────────────────┤
│ Drop caps, RO │
├─────────────────┤
│ Seccomp/AppArmor│ ← run-time
├─────────────────┤
│ User-namespaces │
├─────────────────┤
│ TLS-daemon, RBAC│ ← host
└─────────────────┘Кожен layer редукує attack-class. Skip USER і покладатися лише на seccomp лишає легкі виграші для attacker.
CIS Docker Benchmark категорії
Benchmark групує перевірки:
- Host-конфігурація (Docker-installation, audit, partition для
/var/lib/docker) - Daemon-конфігурація (TLS, audit, log-level, ulimit)
- Daemon-файли (perms на docker.sock, daemon.json)
- Image і Build-файли (USER, без setuid, без dockerd в image)
- Container-runtime (cap-drop, seccomp, без privileged, ulimit)
- Operations (image-lifecycle, secrets, registries)
Бери docker-bench-security (open-source-tool від Docker Inc) для автоматичного scanning.
Приклади
Build-time: hardened Dockerfile
FROM golang:1.22 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# Static-binary, без glibc-dependency
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags='-s -w' -o /out/app
FROM gcr.io/distroless/static:nonroot
COPY /out/app /app
USER nonroot:nonroot
ENTRYPOINT ["/app"]Чому:
- distroless/static має лише binary, без shell, без package-manager. Attacker не може drop у shell для exploration.
USER nonroot:nonrootкрутиться як UID 65532, не root.- Multi-stage: build-toolchain лишається поза фінальним image.
-ldflags='-s -w': strip symbols, менший binary.
Run-time: повні hardening-флаги
docker run -d \
--name=api \
--user=10001:10001 \
--read-only \
--tmpfs=/tmp:size=64m,mode=1777 \
--tmpfs=/run:size=4m \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
--security-opt=no-new-privileges \
--security-opt=seccomp=/etc/docker/seccomp/default.json \
--security-opt=apparmor=docker-default \
--pids-limit=200 \
--memory=512m --memory-swap=512m \
--cpus=1.0 \
--network=app-net \
-p 8080:8080 \
-v /etc/myapp/config.yaml:/etc/myapp/config.yaml:ro \
myorg/api:1.0Лінія за лінією:
--user: explicit UID/GID (override USER в image, забезпечує non-root).--read-only: rootfs read-only; app не може модифікувати себе.--tmpfs: writable scratch-каталоги в tmpfs (RAM, lost on stop).--cap-drop=ALL: прибирає кожен Linux-capability. App не може bind на low-port, змінити час, mount тощо.--cap-add=NET_BIND_SERVICE: додає назад тільки потрібне (bind на port < 1024). Drop це, якщо bind на > 1024.--security-opt=no-new-privileges: process не може набути нові cap через setuid-binary.--security-opt=seccomp: обмежує syscall до whitelist. Дефолт OK для більшості app.--security-opt=apparmor: застосовує AppArmor-profile (дефолт ставить розумні дефолти).--pids-limit: запобігає fork-bomb DoS.--memory --cpus: cgroup-limit запобігають resource-exhaustion.--network=app-net: isolated user-defined мережа (не default bridge, не host).-v ...:ro: config mounted read-only.
Linux-capability reference
# Дивись, які cap loaded всередині
docker exec api capsh --print
# Current: =
# Bounding set =
# Ambient set =Типові cap, що залишати:
- NET_BIND_SERVICE, bind на port < 1024.
- CHOWN, SETUID, SETGID, лише якщо app fork worker-ів як різних користувачів.
- DAC_OVERRIDE, лише якщо легітимно читаєш файли, які не належать.
Ніколи не додавай:
- SYS_ADMIN (масивний scope; ~50 sub-capability).
- SYS_PTRACE (читати memory інших process).
- SYS_MODULE (load kernel-module).
- SYS_RAWIO, NET_RAW (raw-socket, packet-маніпуляція).
Seccomp-profile basics
Seccomp фільтрує специфічні syscall. Default Docker-profile блокує ~44 ризикових syscall (наприклад, mount, reboot, kexec_load, ptrace).
# Run без seccomp (НЕ рекомендовано)
docker run --security-opt=seccomp=unconfined ...
# Run з custom-profile
docker run --security-opt=seccomp=/path/to/profile.json ...Щільніший profile блокує більше. Для Go HTTP-server можна drop clone, unshare, keyctl тощо.
Image-scanning
# Trivy: open-source, зрілий
trivy image --severity HIGH,CRITICAL myorg/api:1.0
# Docker Scout (built-in)
docker scout cves myorg/api:1.0
# Snyk
snyk container test myorg/api:1.0Інтегруй у CI: fail build на будь-якому HIGH/CRITICAL з fix-доступним.
Secrets-management
Погано:
ENV DB_PASSWORD=hunter2 # Запечено в image, leak через docker historyПогано:
docker run -e DB_PASSWORD=hunter2 myorg/app # Видно в process-table, ps, /proc, kernel-audit-logКраще:
# Mount як file
echo 'hunter2' | docker secret create db-pass - # Swarm
docker run -v secret-db:/run/secrets/db-pass:ro myorg/app
# Або читати з vault на runtime
docker run -e VAULT_ROLE=app-prod myorg/app
# App fetch'ить з HashiCorp Vault на startupDocker Swarm-secrets, Kubernetes-secrets, HashiCorp Vault, AWS Secrets Manager, всі дозволяють app pull credential на runtime, ніколи не persist в image.
Daemon-hardening
У /etc/docker/daemon.json:
{
"icc": false,
"userns-remap": "default",
"no-new-privileges": true,
"log-driver": "json-file",
"log-opts": {"max-size": "100m", "max-file": "3"},
"live-restore": true,
"userland-proxy": false
}icc: false: container'и не можуть говорити через default-bridge за замовчуванням; мають використовувати user-defined-мережі.userns-remap: мапить container UID 0 на high host-UID, тож root всередині unprivileged зовні.live-restore: container'и продовжують крутитися через daemon-restart (менше downtime, менше вікно для attacker діяти на daemon-crash).
--privileged і Docker-socket: уникай
docker run --privileged ...
# Еквівалент: drop усю containerization. Дорівнює root на host.
docker run -v /var/run/docker.sock:/var/run/docker.sock ...
# Container може spawn'ити інші container, включно privileged. Дорівнює root на host.Якщо потрібен Docker-in-Docker для CI, бери rootless DinD з dedicated daemon per pipeline, ніколи не host-socket.
Реальне застосування
- PCI/HIPAA-workload: повний CIS Docker Benchmark; аудитори очікують.
- Multi-tenant cluster: user-namespace remap не обговорюється.
- Public-facing API: read-only FS + drop cap + seccomp default.
- Внутрішні сервіси: щонайменше non-root + drop cap + memory-limit.
- CI-runner: ephemeral, але все ще: limited cap, без privileged, якщо explicitly не Docker-in-Docker.
Типові помилки
Run як root всередині container
Дефолтний user nginx:alpine це root. App, що не override USER, крутиться як root. Завжди ставь USER у Dockerfile або --user на run.
Mount /var/run/docker.sock
Дати container socket дорівнює дати йому root на host. Бери rootless-DinD або sysbox, якщо потрібен container-in-container.
Класти secrets в env-var
Видно будь-кому з docker inspect-permission, leak у логи і crash-dump. Mount як file.
Skip image-scanning
node:18-image 6 місяців тому має відомі CVE. Pin і update; scan на кожен build.
Брати --privileged для зручності
Еквівалент sudo -i. Майже ніколи не потрібно; додавай specific-capability замість.
Питання для поглиблення
Q: Що робить --security-opt=no-new-privileges?
A: Ставить kernel-flag no_new_privs на container's process. Вони не можуть набути нові privilege через setuid-binary або capability. Комбінуй з non-root USER для сильної privilege-containment.
Q: AppArmor чи SELinux?
A: Бери, що дефолт твоєї distro. Ubuntu використовує AppArmor; RHEL/CentOS/Rocky використовують SELinux. Обидва додають MAC (Mandatory Access Control). Docker дає default-profile для обох. Custom-profile потужний, але maintenance-важкий.
Q: Чи rootless Docker безпечніший за звичайний?
A: Так для host (compromise дає лише unprivileged-user). Tradeoff: обмежений port-bind (< 1024 потребує cap або proxy), без --network=host-семантики, трохи повільніше (fuse-overlayfs на старих kernel).
Q: (Senior) Як reason'ити про supply-chain security для container-image?
A: Три layer: (1) provenance (підписані images через cosign або Docker Content Trust, attestation build), (2) SBOM (software bill of materials per image, scanned на CVE), (3) policy (admission-controller типу Kyverno або Gatekeeper, що enforce signing/scanning перед deploy). Для high-stakes-infra build images сам з trusted-base замість pull random Docker Hub-images.
Q: (Senior) Як user-namespace змінює security-model?
A: З userns-remap, container UID 0 мапиться на high host-UID (наприклад, 100000). Container, що escape, ще має unprivileged host-access. Tradeoff: image-layer під remapped UID не можна шарити з non-remapped-daemon; bind-mount потребує correct-ownership; деякий software детектить «не реальний root» і ламається. Варто ціни на multi-tenant-host.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.
Коментарі
Ще немає коментарів