Короткий ответ
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 2375 | root по сети |
| TCP 2376 mTLS | API с крипто, всё ещё полный контроль демона |
| rootless user sock | root только в user ns |
Возможные причины
- Watchtower «обновлять самому».
- CI runner в контейнере на том же хосте, что прод
app_data. - Админ-панель «удобно с логином».
- Логи/метрики через сокет.
chmod 666после Permission denied.- Редко: 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 docker2376 только с клиентскими сертификатами и 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 — вынесите сбор логов в агент на хосте, не в контейнер приложения.