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

Bind /var/run/docker.sock внутрь контейнера даёт API демона = root на host.example. Watchtower/Portainer/«маленький helper» с sock — тот же класс, что privileged. Уберите volume из compose проекта app. Если нужен API: вынесите панель на jump-хост, используйте rootless Docker (DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock) в своём namespace, или CI по SSH с узким sudo. Не chmod 666 на сокете. Не публикуйте 2375 без TLS.

Это не статья про группу docker у SSH-пользователя (соседний раздел linux-security); здесь именно проброс в контейнер.

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

  • YAML: - /var/run/docker.sock:/var/run/docker.sock;
  • docker inspect app-watch-1 --format '{{json .Mounts}}' содержит docker.sock;
  • из контейнера docker ps показывает хостовые контейнеры, включая app-db-1;
  • ss -tlnp | grep 2375.
ДоступЭквивалент
sock в контейнереroot хоста
TCP 2375root по сети
TCP 2376 mTLSAPI с крипто, всё ещё полный контроль демона
rootless user sockroot только в user ns

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

  1. Watchtower «обновлять самому».
  2. CI runner в контейнере на том же хосте, что прод app_data.
  3. Админ-панель «удобно с логином».
  4. Логи/метрики через сокет.
  5. chmod 666 после Permission denied.
  6. Редко: socket activation эксперименты.

Диагностика

cd /opt/app
grep -n docker.sock compose.yaml
docker ps -a --format '{{.Names}}' | while read n; do
  docker inspect "$n" --format '{{.Name}} {{range .Mounts}}{{.Source}}->{{.Destination}} {{end}}'
done | grep sock || echo 'no sock mounts'
sudo ss -tlnp | grep -E '2375|2376'
ls -l /var/run/docker.sock

Проверьте unit:

grep -r 2375 /etc/docker/daemon.json /etc/systemd/system/docker.service.d/ 2>/dev/null

"hosts": ["tcp://0.0.0.0:2375"] — критично.

Решение

Сценарий A. Watchtower/панель на проде app

Удалите сервис из compose. Обновления — процедура без потери данных из CI/руками.

docker compose stop watchtower
# правьте YAML, уберите сервис и volume sock
docker compose up -d

Не down -v.

Сценарий B. Нужен UI управления

Поставьте на отдельный admin-хост без app_data. Доступ по VPN. Предпочтительнее SSH + compose, не sock с прода.

Если оставляете Portainer — не на том же VM, что бухгалтерия; sock всё равно = root той VM.

Сценарий C. Rootless API вместо хостового sock

На Ubuntu rootless: пользователь deploy, dockerd-rootless.sh, DOCKER_HOST=unix:///run/user/UID/docker.sock. Контейнеры не те, что systemd docker.service. Не монтируйте /var/run/docker.sock rootful в rootless и наоборот.

Для CI: runner под deploy, без группы docker на rootful.

Сценарий D. TCP API

Закройте 2375:

# уберите tcp:// из daemon.json hosts, оставьте fd:// или unix sock systemd
sudo python3 -m json.tool /etc/docker/daemon.json
sudo systemctl restart docker

2376 только с клиентскими сертификатами и ACL, не в интернет.

Сценарий E. chmod 666 уже сделали

sudo chmod 660 /var/run/docker.sock
sudo chown root:docker /var/run/docker.sock

Уберите лишних из группы docker. Сокет пересоздаст daemon при restart с правильными правами unit.

С сокетом контейнер делает docker run --privileged -v /:/mnt и получает хост независимо от USER внутри helper-а. :ro на bind sock не переводит API в read-only: протокол один, демон пишет по просьбе клиента. TCP 2375 без TLS — то же API по сети; боты сканируют его годами. 2376 с mTLS лучше, но клиентский сертификат = полный контроль демона, храните как секрет админа, не в образе app.

Watchtower с sock на проде обновляет контейнеры без вашего окна и без бэкапа app_data. Это противоречит процедуре обновления. Замените на CI job с compose pull && up -d и проверкой health. Portainer на той же VM, что БД, даёт веб-root: украденная сессия панели = хост. Вынесите UI.

Rootless: systemctl --user status docker у deploy, сокет в /run/user/UID/docker.sock. Контейнеры не видят rootful app_data в /var/lib/docker/volumes — это другой граф. Не монтируйте rootful sock «чтобы rootless видел прод». Для CI собирайте и пушьте в registry.example:5000, прод только pull.

Права 666 на /var/run/docker.sock после «чтобы jenkins мог» — любой локальный пользователь = root. Верните 660, уберите jenkins из группы docker, дайте отдельный hop.

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

grep -R docker.sock /opt/app || echo 'no sock in project'
docker inspect app-web-1 --format '{{json .Mounts}}'
sudo ss -tlnp | grep 2375 || echo 'no 2375'
ls -l /var/run/docker.sock

Нет bind sock у контейнеров app. Нет 2375. Сокет root:docker 660. Приложение отвечает как раньше (ему sock не был нужен для HTTP).

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

  • Sidecar в другом compose в /opt/monitor — ищите по всему хосту grep -R docker.sock /opt.
  • Kubernetes unix:///var/run/docker.sock в pod — это уже не этот хост-compose, но тот же антипаттерн.
  • Rootless сокет пробросили в контейнер rootless — ограниченнее, всё ещё полный контроль того демона. Для app не нужно.
  • После снятия «не обновляется» — это цель; настройте CI.

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

  • CI grep docker.sock.
  • Запрет 2375 в baseline.
  • Группа docker только у операторов, не у приложения.
  • Отдельные хосты: prod app vs cicd.
  • Hardening.

Поиск по хосту: grep -R docker.sock /opt /etc/systemd /home --include='*.yml' --include='*.yaml' --include='*.service'. Unit вне compose легко забыть. После снятия перезапустите стек app и убедитесь, что HTTP жив без helper-а.

FAQ

Можно ли sock read-only (:ro)?

API всё равно принимает команды через тот же протокол. :ro не делает sock безопасным.

Portainer без sock?

Только если управляет другим API (агент, k8s). Агент с полным доступом — снова корень той ноды.

Rootless полностью заменяет rootful на этом сервере?

Для выделенного dev — да. Для прода с чужими volume в /var/lib/docker миграция отдельная. Не смешивайте два демона неявно.

Нужен ли privileged вместе с sock?

Часто ставят оба. Снимите оба.

docker.sock для логов fluentbit

Используйте драйвер логов/journal, не полный API. Если плагину нужен sock — вынесите сбор логов в агент на хосте, не в контейнер приложения.