Короткий ответ
Baseline Docker на host.example — набор команд с ожидаемым выводом, не «поставили UFW». Минимум для проекта app: демон без TCP 2375, сокет 660 root:docker, в контейнерах нет bind docker.sock и нет privileged, процесс не uid 0 (или задокументированный drop), cap_drop: ALL + точечный add, секреты не в образе и не в docker history, named volume app_data не world-writable, есть memory/cpus/pids, json-file rotation, образы только с registry.example:5000. Не отключайте UFW/AppArmor «чтобы compose прошёл».
Детали узлов: privileged, sock, root, секреты, лимиты.
Симптомы и как отличить
Нет baseline, если после инцидента нельзя показать артефакты inspect Privileged/User/Mounts. Отличие от hardening Ubuntu хоста: здесь объекты Engine и compose, не sshd. Хостовый CIS всё равно нужен (2375, группа docker).
| Проверка | Провал |
|---|---|
| sock в Mounts | контейнер = root хоста |
| Privileged true | почти хост |
| history с PASSWORD | инцидент секрета |
| Memory 0 | риск OOM хоста |
Возможные причины
- Compose из gist 2018 с privileged и sock.
- Секреты в ENV образа для «проще k8s потом».
- Группа docker у разработчика на проде.
- Нет шаблона YAML.
insecure-registriesнавсегда.- Read-write корень + uid 0 + bind
/.
Диагностика
mkdir -p /home/admin/baseline-docker && cd /home/admin/baseline-docker
date -Is | tee 00-meta.txt
hostnamectl >> 00-meta.txt
docker info --format '{{.ServerVersion}} {{.CgroupDriver}} {{.SecurityOptions}}' | tee 01-engine.txt
sudo ss -tlnp | grep -E '2375|2376' | tee 02-tcp-api.txt
ls -l /var/run/docker.sock | tee 03-sock.txt
getent group docker | tee 04-group.txtСтек:
cd /opt/app
docker compose ps | tee /home/admin/baseline-docker/05-ps.txt
for n in $(docker compose ps -q); do
docker inspect "$n" --format '{{.Name}} priv={{.HostConfig.Privileged}} user={{.Config.User}} mem={{.HostConfig.Memory}} pids={{.HostConfig.PidsLimit}} caps={{json .HostConfig.CapAdd}} drop={{json .HostConfig.CapDrop}}'
done | tee /home/admin/baseline-docker/06-inspect.txt
docker inspect $(docker compose ps -q) --format '{{.Name}} {{range .Mounts}}{{.Source}}->{{.Destination}} {{end}}' | tee /home/admin/baseline-docker/07-mounts.txtСекреты в образе (осторожно с выводом):
docker history --no-trunc registry.example:5000/app/web:1.2.3 2>/dev/null | grep -iE 'pass|secret|token|key' | tee /home/admin/baseline-docker/08-history-grep.txt || trueОжидания: пустой 02, sock 660, в 06 priv=false и непустой user/memory, в 07 нет docker.sock, в 08 пусто.
Решение
A. Демон и API
Нет tcp://0.0.0.0:2375 в daemon.json. Сокет только unix. Группа docker без прикладных учёток. live-restore по политике. log-opts max-size. userns-remap не включать в этом baseline без отдельного проекта (ломает uid томов).
sudo python3 -m json.tool /etc/docker/daemon.jsonRestart docker — окно.
B. Compose: изоляция процесса
Для web (подставьте uid образа):
user: "10001:10001"
read_only: true
tmpfs: ["/tmp", "/var/run"]
cap_drop: [ALL]
security_opt:
- no-new-privileges:true
privileged: false
mem_limit: 512m
cpus: "1.0"
pids_limit: 256Нет volume sock. Нет :ro на sock как «компромисс». Публикация портов с bind на LAN IP, не WAN. Реестр registry.example:5000/....
C. Секреты
Compose secrets: с файлов 600 в /opt/app/secrets. Не ENV в образе. После истории с паролем — ротация. .dockerignore на .env.
D. Данные
app_data named с name: app_data. Бэкап есть. Не 777. UID согласован.
E. Выкладка
cd /opt/app
docker compose config >/dev/null
docker compose up -dПовтор 06-inspect до зелёного. Если сервис падает — чините USER/тома/caps, не privileged.
AppArmor: профиль docker-default, не apparmor:unconfined. UFW active, правила на publish осознанные.
Повторяйте съёмку после каждого изменения compose, не раз в год. CI grep не заменяет inspect на живом host.example: override-файл и ручной docker run обходят YAML. Ищите в docker ps -a контейнеры вне проекта app.
Исключения (SYS_ADMIN, root, sock) храните таблицей: сервис, причина, дата пересмотра, владелец. Без даты исключение вечно. Baseline не требует CIS всех 100+ пунктов: он требует, чтобы privileged/sock/uid0/секреты-в-слое/2375/безлимит RAM были закрыты или явно приняты.
Связка с хостовым hardening: группа docker, AppArmor enforce, UFW active, нет 2375. Этот материал не заменяет патчи ядра и SSH. После прохождения чеклиста не запускайте prune -a «для красоты df» и не rm app_data.
Образы только из registry.example:5000, не Docker Hub напрямую с прода, если политика зеркала/скана иначе. Digest pin — следующий шаг после тегов.
Как проверить, что проблема устранена
Повторите съёмку. Критерии приёмки:
ssбез 2375;Privileged falseу всех сервисовapp;- нет
docker.sockв Mounts; idвweb≠ 0;CapDropсодержит ALL или эквивалентный узкий набор, нет SYS_ADMIN без тикета;MemoryиPidsLimitненулевые;docker historyбез паролей;curlhealth с ожидаемого адреса;ufw statusactive.
docker compose -f /opt/app/compose.yaml exec web id
docker inspect app-web-1 --format '{{.HostConfig.Privileged}} {{.HostConfig.Memory}}'
curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/healthЕсли не помогло
- Вендорский контейнер требует root: изолируйте сеть, read_only где можно, тикет на замену, не privileged «ну они так сказали» без разбора.
- GPU/FUSE: отдельный стенд, не этот baseline на БД.
- Rootless Engine — следующий уровень, не блокирует пункты USER/caps на текущем rootful.
- CI всё ещё пушит ENV — чините pipeline, хостовый inspect снова покраснеет.
Профилактика
- Шаблон compose + CI grep (
privileged,docker.sock,2375,USERпустой). - Повтор baseline после каждого major тега.
- Разделение prod и build.
- Мониторинг inspect полей (можно cadvisor/cgroup).
- Журнал исключений (кому SYS_ADMIN и до когда).
FAQ
Это CIS Docker Benchmark целиком?
Нет, это операционный минимум, который админ проверяет за час. CIS шире (audit, tls между демонами). Начните отсюда.
Нужен ли user namespaces?
Сильная мера, ломает uid на app_data. Не включайте в ночь без стенда.
Достаточно ли non-root без cap_drop?
Нет. Root+cap_drop лучше, чем root+all, но цель — оба: user и drop.
Можно ли Portainer в этом baseline?
Только вне prod-VM с данными, без 2375 в интернет. Sock на prod-app — провал пункта socket.
Кто владелец исключений?
Именной тикет, не комментарий # TODO security в YAML.