Skip to main content

Як реалізувати 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 групує перевірки:

  1. Host-конфігурація (Docker-installation, audit, partition для /var/lib/docker)
  2. Daemon-конфігурація (TLS, audit, log-level, ulimit)
  3. Daemon-файли (perms на docker.sock, daemon.json)
  4. Image і Build-файли (USER, без setuid, без dockerd в image)
  5. Container-runtime (cap-drop, seccomp, без privileged, ulimit)
  6. Operations (image-lifecycle, secrets, registries)

Бери docker-bench-security (open-source-tool від Docker Inc) для автоматичного scanning.

Приклади

Build-time: hardened Dockerfile

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 --from=build /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-флаги

bash
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

bash
# Дивись, які 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).

bash
# 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

bash
# 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

Погано:

dockerfile
ENV DB_PASSWORD=hunter2 # Запечено в image, leak через docker history

Погано:

bash
docker run -e DB_PASSWORD=hunter2 myorg/app # Видно в process-table, ps, /proc, kernel-audit-log

Краще:

bash
# 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 на startup

Docker Swarm-secrets, Kubernetes-secrets, HashiCorp Vault, AWS Secrets Manager, всі дозволяють app pull credential на runtime, ніколи не persist в image.

Daemon-hardening

У /etc/docker/daemon.json:

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: уникай

bash
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.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Коментарі

Ще немає коментарів