Короткий ответ

UNIX-сокет /var/run/docker.sock с группой docker даёт полный root на хосте: можно запустить --privileged и смонтировать /. Пользователь admin для SSH/sudo — не то же самое, что прикладной deploy или разработчик на прод-сервере. Уберите лишних из группы docker, запретите chmod 666 на сокет, закройте TCP 2375. Деплой — CI по SSH с узким sudo или rootless Docker / Podman в своём namespace. Хост host.example (10.0.20.10).

Симптомы и как отличить

  • groups deploydocker;
  • ls -l /var/run/docker.socksrw-rw---- root docker или хуже srw-rw-rw-;
  • ss -tlnp | grep 2375;
  • в wiki «добавь себя в docker чтобы не вводить пароль».
ДоступЭквивалент
группа dockerroot
sudo docker ALLroot, хотя бы с паролем sudo
2375 без TLSroot по сети
rootless sock в XDGне хостовый root

Отличие от широкого sudo: тот же итог, другой файл. sudoers. Публикация портов: лишние порты.

Возможные причины

  1. Постинструкция Docker Official: usermod -aG docker $USER на проде.
  2. Панель Portainer с сокетом и паролем admin/admin.
  3. DOCKER_HOST=tcp://0.0.0.0:2375 «на час».
  4. Jenkins-агент под jenkins в группе docker.
  5. snap docker vs docker.io — два сокета, забыли один.
  6. 666 «чтобы php запускал контейнеры».

Диагностика

getent group docker
awk -F: '$1=="docker" {print}' /etc/group
ls -l /var/run/docker.sock /run/docker.sock 2>/dev/null
ss -tlnp | grep -E '2375|2376'
systemctl status docker --no-pager | head

Кто уже пользуется:

ps -ef | grep -E 'docker|containerd' | grep -v grep
sudo grep -R docker /etc/sudoers /etc/sudoers.d/ 2>/dev/null

Проверка, что сокет = root (на изолированной копии, не как «эксплойт-инструкция»; здесь только факт API):

# от пользователя в группе docker
docker info >/dev/null && echo 'this user can control docker engine'

Если docker info работает без sudo — считайте uid 0.

TCP:

curl -sS --max-time 2 http://127.0.0.1:2375/version || true

Ответ JSON с WAN — инцидент. Закрывать bind, не «оставить для IDE».

Решение

Сценарий A. Забрать группу у людей и приложений

sudo gpasswd -d deploy docker
sudo gpasswd -d www-data docker
getent group docker

Оставьте минимум: учётки, которым политика явно даёт админ Docker (часто только admin и то лучше через sudo). Сессии group membership: пользователю перелогиниться.

Права сокета должны остаться 660 root:docker (пакет). Не:

# НЕ делать
# chmod 666 /var/run/docker.sock

Сценарий B. Админу нужен docker CLI

Узкий sudo вместо группы:

admin ALL=(root) NOPASSWD: /usr/bin/docker

Всё ещё root-эквивалент, но есть след sudo и нет постоянного gid. Ещё лучше: без NOPASSWD. Ещё лучше: админить Docker с jump-хоста, на app-сервере socket только root.

Сценарий C. Закрыть 2375

В /etc/docker/daemon.json не должно быть "hosts": ["tcp://0.0.0.0:2375"]. Override systemd docker.service с -H fd:// только unix. systemctl cat docker. После правки:

sudo systemctl daemon-reload
sudo systemctl restart docker
ss -tlnp | grep docker

2376+TLS — отдельный проект с клиентскими сертификатами, не «почти 2375». Firewall не выключать.

Сценарий D. CI

Варианты без хостового сокета приложению: отдельный runner-хост; Kaniko/buildah в k8s; rootless Docker (dockerd-rootless) под пользователем CI, без группы docker хоста. Не монтировать хостовый sock в job-контейнер.

Секреты из docker inspect: секреты. Процесс демона root: ожидаемо для классического Docker; не плодите ещё User=root приложения на хосте: сервис.

Инвентаризация членов группы после чистки. getent group docker должен содержать только согласованные админские UID. Автоматизация: ansible assert docker_users == ['admin'] или пусто. Cloud-init groups: [docker] в user-data — уберите для прикладных user.

TCP API и systemd override: некоторые инструкции предлагают /etc/systemd/system/docker.service.d/override.conf с ExecStart= пустым и новым -H tcp://. Ищите такие drop-in:

systemctl cat docker.service
ls /etc/systemd/system/docker.service.d/

Пустой ExecStart= обязателен при переопределении, иначе два -H. Цель — не держать tcp://0.0.0.0 без TLS.

Portainer, Yacht, Dockge с bind-монтом сокета: UI = docker root. Ограничьте listen 127.0.0.1 + SSH tunnel, SSO, не публикуйте :9000 в мир. Пароль по умолчанию смените в день установки.

Rootless: dockerd-rootless-setuptool.sh под пользователем CI, DOCKER_HOST=unix:///run/user/UID/docker.sock. Не добавляйте этого пользователя в группу docker хоста. Проверьте, что он не может ls /root.

Секреты: любой, кто говорит с сокетом, делает docker exec и читает env. После изъятия gid ротируйте секреты контейнеров, если gid был у посторонних.

AppArmor docker-default не отменяет gid. Не aa-complain docker-default чтобы «собрать образ».

Как проверить, что проблема устранена

getent group docker
sudo -u deploy docker info 2>&1 | head
ls -l /var/run/docker.sock
ss -tlnp | grep -E '2375|2376' || echo 'no docker tcp'

deploy получает permission denied. Сокет не 666. TCP API нет. Контейнеры приложения живы. admin администрирует согласованным способом (sudo/консоль).

Если не помогло

  • Сокет снова 666: unit или скрипт в cron. Найдите chmod.*docker.sock.
  • snap docker: /var/snap/docker/common/run/docker.sock.
  • Пользователь в lxd тоже root-подобный — инвентаризация групп.
  • Portainer с сокетом: ограничьте UI CIDR, сильный пароль, всё равно = root.
  • AppArmor docker-default не замена изъятию группы.

Профилактика

  • Запрет usermod -aG docker на прод в регламенте.
  • Мониторинг getent group docker и listen 2375.
  • Compose без sock mount.
  • Baseline.

FAQ

Почему официальный док предлагает группу docker?

Для рабочей станции разработчика. Прод-сервер — не рабочая станция.

rootless полностью безопасен?

Нет, но это не хостовый uid 0. Уменьшает удар. Нужны cgroup/uid map. Всё равно патчи и firewall.

Podman + sudo

Тот же класс задач. Сокет/socket activation смотрите отдельно, не копируйте docker.sock 666.

Нужен ли docker для одного nginx

Часто нет. Бинарник пакета + User=www-data проще для MAC и сокета.

Может ли AppArmor закрыть сокет?

Профиль может ограничить контейнер, не отменяет gid docker на хосте. Не aa-disable.