Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Як реалізувати security hardening Docker контейнерів?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)Застосовуй defense-in-depth на **build, run, і host** рівнях. ```dockerfile # Build-time FROM gcr.io/distroless/static:nonroot USER nonroot:nonroot ``` ```bash # Runtime docker run \ --read-only \ --tmpfs /tmp \ --cap-drop=ALL --cap-add=NET_BIND_SERVICE \ --security-opt=no-new-privileges \ --security-opt seccomp=default.json \ --user 1001:1001 \ --pids-limit=200 --memory=512m --cpus=1 \ myorg/app ``` **Топ-айтеми:** non-root USER, read-only FS, drop capabilities, seccomp + AppArmor-profile, scan images, mount secrets через volumes (не env), без `--privileged`. CIS Docker Benchmark це канонічний checklist.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**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.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.