Нічого так не псує спокійну зміну на чергуванні, як контейнер, який нормально стартує, коректно логить, а потім з валідною помилкою засинає на одній стрічці: permission denied. Це завжди той сокет, який ви примонтували «як завжди», або нода пристрою, якій ви «точно надали доступ». Команда розробників клянеться, що на їхньому ноутбуці все працювало. Ви клянетеся, що це не може бути проблема прав, бо процес запускали від root. Обидва ви по-своєму помиляєтесь.
Ось практичний, промисловий підхід до налагодження помилок доступу Docker на UNIX‑сокетах і в /dev—без застосування --privileged, як ніби це вогнегасник для нудьги.
Ментальна модель: чому навіть root може отримати відмову
Коли контейнер каже «permission denied», ваш мозок хоче, щоб це були класичні UNIX‑права: користувач, група, біти режиму. Іноді так воно і є. Частіше — ні. У світі контейнерів доступ — це переговори між кількома незалежними «швейцарами»:
- Файлові права (UID/GID/мод) на файловій системі хоста (включно з bind‑монтажами).
- Мапінг неймспейсів (user namespaces, rootless Docker, remapped root), що змінює, що означає UID/GID в контейнері назовні.
- Linux capabilities (тонкі «root‑права», наприклад
CAP_NET_ADMIN). - Seccomp (фільтрація системних викликів), яка може блокувати операції з обманливим повідомленням.
- LSM‑и (SELinux/AppArmor), які відхиляють дії незалежно від бітів режиму.
- cgroups device controller (дозвільні/заборонні списки пристроїв), що може заблокувати
open()для нод пристроїв.
Ці шари відмовляють по‑різному, і виправлення не взаємозамінні. «Запустити як root» вирішує тільки один шар (UID/GID) і інколи навіть не цей, якщо задіяні user namespaces. «Використати --privileged» зносить кілька шарів одночасно й робить усе працюючим доти, доки це не спричинить іншу катастрофу.
Ось правило, яке хочу, щоб ви запам’ятали: privileged — це не право; це зміна середовища. Воно відключає або послаблює кілька засобів захисту. Тому воно «працює». І саме тому воно зазвичай неправильне рішення.
Цитата, яка добре з роками звучить у операціях: Надія — це не стратегія.
— Джеймс Кемерон
Жарт №1: Якщо --privileged — ваш план налагодження, ви не налагоджуєте — ви домовляєтесь з ядром за допомогою мегафона.
Цікаві факти та невелика історія (корисно, не тривіально)
- Capabilities — це не новинка. Linux capabilities розділили «root» на дискретні повноваження наприкінці 1990‑х; контейнери просто зробили їх видимими для звичайних людей.
- Docker починався як клей для LXC. Ранні версії Docker покладалися на LXC; пізніше екосистема стандартизувалась навколо OCI runtime specs.
docker.sockфактично еквівалентний root. Доступ до Docker API зазвичай дає контроль над хостом, бо можна змонтувати файлову систему хоста або запустити привілейовані контейнери.- Фільтрація пристроїв у cgroups старша за Docker. Контролер devices існував задовго до хайпу з контейнерами; Docker просто використовує його, щоб уникнути пригод з
/dev/mem. - За замовчуванням seccomp у Docker консервативний. Docker постачає дефолтний seccomp‑профіль, який блокує низку системних викликів; багато повідомлень «permission denied» насправді — це відмови syscall.
- Rootless Docker змінив модель загроз. Rootless‑режим уникає демона root, але також змінює поведінку доступу до пристроїв та привілейованих операцій. Багато речей просто не працюють.
- SELinux робить «permission denied» буквальним. На системах з SELinux DAC (біти режиму) може сказати «дозволити», а SELinux — «ні». Обидва — «права», але від різних систем.
- Прив’язування сокету перетинає кордони довіри. Контейнер, що підключається до сокету хоста, наслідує все, що цей сокет дозволяє (systemd, containerd, Docker, власні демони адміністрування).
Швидкий план діагностики
Ви обмежені в часі. Не тикати випадково. Робіть це в порядку; кожен крок швидко звужує клас помилки.
1) Підтвердіть, який шлях падає та де він знаходиться
- Це bind‑монтаж з хоста? Сокет створено всередині контейнера? Нода пристрою передана?
- Чи помилка виникає при
open(),connect()або на якомусь вищому рівні бібліотеки?
2) Визначте ідентичність: UID/GID всередині проти назовні
- Перевірте UID/GID процесу в контейнері.
- Перевірте власника/мод файлу на хості й вимоги групи (часто для сокетів).
- Якщо задіяний rootless або userns‑remap, вважайте, що проблема з мапінгом UID, доки не доведено протилежне.
3) Перевірте «політичні шари»: LSM і seccomp
- Якщо SELinux/AppArmor активні — шукайте AVC/AppArmor відмови.
- Якщо відмова трапляється при чомусь «привілейованому» (mount, setns, perf, bpf, raw sockets), підозрюйте seccomp/capabilities.
4) Для пристроїв перевірте права cgroup device
- Якщо ви бачите
/dev/..., але не можете його відкрити — часто це фільтр cgroups device або відсутня capability.
5) Лише потім думайте про додавання capabilities або --device
- Віддавайте перевагу
--cap-add,--deviceі явним виправленням груп. - Залишайте
--privilegedдля рідкісних випадків і використовуйте його як тимчасовий діагностичний крок, а не рішення.
Практичні завдання: команди, вивід, рішення (12+)
Ось ходи, якими я реально користуюся. Кожне завдання включає, що означає вивід і яке рішення з цього випливає. Запускайте їх на хості, якщо не вказано інше.
Завдання 1: Відтворіть помилку з максимальним контекстом
cr0x@server:~$ docker logs --tail=50 myapp
...snip...
Error: connect unix /var/run/docker.sock: permission denied
...snip...
Що це означає: У вас є конкретний шлях, що падає: /var/run/docker.sock. Це UNIX‑сокет. Операція — connect(), а не просто open().
Рішення: Зосередьтеся на власності сокета, групі, SELinux/AppArmor‑мітках та на тому, чи належить користувач контейнера до потрібної групи. Не починайте з capabilities.
Завдання 2: Визначте effective user контейнера
cr0x@server:~$ docker exec myapp id
uid=10001(app) gid=10001(app) groups=10001(app)
Що це означає: Процес не root. У нього немає додаткових груп. Якщо сокет вимагає групи (зазвичай так), це не спрацює.
Рішення: Запустіть цей процес з групою, що відповідає сокету, або спроєктуйте по‑новому, щоб контейнер не потребував доступу до Docker хоста.
Завдання 3: Перевірте права сокета на хості
cr0x@server:~$ ls -l /var/run/docker.sock
srw-rw---- 1 root docker 0 Jan 3 10:12 /var/run/docker.sock
Що це означає: Лише root і члени групи docker можуть підключитись. Режим 660. Це типово.
Рішення: Якщо ви наполягаєте на монтуванні цього сокета, вашому користувачу в контейнері потрібна група docker (важлива відповідність GID), або потрібен проксі з вужчим API.
Завдання 4: Підтвердіть GID групи docker (несумісність GID — класика)
cr0x@server:~$ getent group docker
docker:x:998:cr0x
Що це означає: На хості група docker має GID 998. Всередині контейнера імена груп можуть не збігатися, якщо ви їх не вирівняєте.
Рішення: Передайте групу в контейнер (--group-add 998) або збудуйте образ так, щоб у контейнері була група з тим самим числовим GID, і процес її використовував.
Завдання 5: Запустіть одноразовий контейнер з явним group‑add для валідації
cr0x@server:~$ docker run --rm -it \
-v /var/run/docker.sock:/var/run/docker.sock \
--group-add 998 \
alpine:3.20 sh -lc 'id && apk add --no-cache docker-cli >/dev/null && docker ps >/dev/null && echo OK'
uid=0(root) gid=0(root) groups=0(root),998
OK
Що це означає: З групою 998 контейнер може спілкуватись з Docker API.
Рішення: Якщо це «виправлення» прийнятне (часто ні), впровадьте мапінг груп правильно. Інакше розгляньте будь‑яке монтування docker.sock як виняток безпеки, який потребує огляду.
Завдання 6: Діагностуйте відмови SELinux (якщо SELinux увімкнено)
cr0x@server:~$ getenforce
Enforcing
Що це означає: SELinux політика активна. Простий bind‑монтаж може бути заблокований правилами міток.
Рішення: Перед тим як чіпати права або capabilities, перевірте журнали аудиту на AVC‑відмови.
Завдання 7: Знайдіть останні AVC, що відповідають вашому контейнеру
cr0x@server:~$ sudo ausearch -m avc -ts recent | tail -n 5
type=AVC msg=audit(1735902901.123:812): avc: denied { connectto } for pid=21456 comm="myapp" path="/var/run/docker.sock" scontext=system_u:system_r:container_t:s0:c123,c456 tcontext=system_u:object_r:docker_var_run_t:s0 tclass=unix_stream_socket permissive=0
...snip...
Що це означає: SELinux явно заборонив connectto на сокеті. Навіть правильні UNIX‑права не допоможуть.
Рішення: Виправте мітки/опції монтування (:Z/:z) або політику. Не робіть chmod‑а сокета з розпачу.
Завдання 8: Перевірте AppArmor‑профіль (поширено на Ubuntu)
cr0x@server:~$ docker inspect --format '{{.AppArmorProfile}}' myapp
docker-default
Що це означає: Застосовано дефолтний AppArmor‑профіль. Він може відмовляти в деяких операціях (рідше для сокетів, частіше для монтів, perf, ptrace).
Рішення: Якщо бачите AppArmor‑відмови в dmesg, відрегулюйте профіль або тестуйте з --security-opt apparmor=unconfined лише як діагностику.
Завдання 9: Перевірте режим seccomp
cr0x@server:~$ docker inspect --format '{{.HostConfig.SecurityOpt}}' myapp
[]
Що це означає: Немає кастомних security opts; в дії дефолтний seccomp профіль Docker.
Рішення: Якщо відмова — denial системного виклику (часто показує Operation not permitted), протестуйте з --security-opt seccomp=unconfined, щоб підтвердити, а тоді додайте лише потрібні syscalls/capabilities.
Завдання 10: Для доступу до пристрою перевірте, чи нода пристрою існує всередині контейнера
cr0x@server:~$ docker exec myvpn ls -l /dev/net/tun
crw-rw-rw- 1 root root 10, 200 Jan 3 10:12 /dev/net/tun
Що це означає: Нода пристрою присутня. Якщо додаток все ще не може її використати, проблема не в «відсутньому файлі». Швидше за все це фільтр cgroups device або відсутня capability (часто CAP_NET_ADMIN).
Рішення: Перевірте правила дозволу пристроїв і необхідні capabilities, а не chmod.
Завдання 11: Підтвердіть, що контейнер стартував з передачею пристрою
cr0x@server:~$ docker inspect --format '{{json .HostConfig.Devices}}' myvpn
[{"PathOnHost":"/dev/net/tun","PathInContainer":"/dev/net/tun","CgroupPermissions":"rwm"}]
Що це означає: Docker налаштував проброс пристрою та права cgroup для читання/запису/створення ноди.
Рішення: Якщо права все ще падають, ймовірно потрібна capability (CAP_NET_ADMIN) або ви блокуєтесь LSM/seccomp.
Завдання 12: Проінспектуйте capabilities контейнера (ефективний набір)
cr0x@server:~$ docker exec myvpn sh -lc 'apk add --no-cache libcap >/dev/null 2>&1; capsh --print | sed -n "1,8p"'
Current: cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap=ep
Bounding set =cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap
...snip...
Що це означає: Немає cap_net_admin. Багато VPN/TUN‑воркфлоу потребують його для налаштування інтерфейсів або маршрутизації.
Рішення: Додайте --cap-add NET_ADMIN (і, можливо, NET_RAW, залежно від потреб) замість --privileged.
Завдання 13: Перевірте, запустивши з мінімально доданою capability
cr0x@server:~$ docker run --rm -it \
--device /dev/net/tun \
--cap-add NET_ADMIN \
alpine:3.20 sh -lc 'ip link show >/dev/null 2>&1 || apk add --no-cache iproute2 >/dev/null; ip tuntap add dev tun0 mode tun && echo OK'
OK
Що це означає: З NET_ADMIN плюс пристрій операція вдається.
Рішення: Закріпіть ці вимоги в конфігурації запуску (Compose/Kubernetes securityContext еквівалент). Не ескалюйте далі, якщо тільки щось інше не блокує.
Завдання 14: Перевірте журнали ядра/аудиту на підказки «denied» (LSM/seccomp)
cr0x@server:~$ dmesg | tail -n 8
[1735902910.441] audit: type=1400 audit(1735902910.441:813): apparmor="DENIED" operation="mount" info="failed flags match" error=-13 profile="docker-default" name="/sys/fs/cgroup/" pid=21999 comm="runc"
...snip...
Що це означає: AppArmor заборонив mount. Помилка — -13 (EACCES), що виглядає як проблема прав на рівні користувача.
Рішення: Ви не вирішите це chmod‑ом. Потрібна зміна AppArmor, інша стратегія монтування або уникати виконання такого монтування всередині контейнера.
Завдання 15: Підтвердіть, чи ви в режимі rootless (і перестаньте чекати дива)
cr0x@server:~$ docker info --format '{{.SecurityOptions}}'
[name=seccomp,profile=default name=rootless]
Що це означає: Rootless увімкнено. Багато доступів до пристроїв і низькорівневих функцій не працюватимуть або працюватимуть інакше.
Рішення: Якщо вам потрібен прямий доступ до пристроїв або функцій мережі ядра, rootless може не бути підходящою платформою. Оберіть інші варіанти.
Capabilities vs privileged: що вам справді потрібно
--privileged — це кувалда: усі capabilities, доступ до всіх пристроїв та послаблення багатьох обмежень безпеки. Це чудово, щоб довести точку, і жахливо для продакшену. Capabilities — це скальпель: додаєте саме те, що потрібно процесу, і зберігаєте решту сандбоксу цілою.
Що насправді змінює --privileged
Точна поведінка залежить від версії Docker і ядра, але на практиці привілейований режим зазвичай робить наступне:
- Додає усі capabilities до контейнера (або майже всі), розширюючи effective і bounding множини.
- Вимикає фільтр device cgroup: контейнер отримує широкий доступ до пристроїв хоста.
- Послаблює деякі LSM‑обмеження залежно від конфігурації (не універсальний обхід, але змінює гру).
- Полегшує виконання mount‑ів, маніпуляції зі стеком мережі, завантаження модулів ядра (часто все ще потрібна співпраця хоста) і роботу з неймспейсами.
Ось чому воно «виправляє» permission denied. І одночасно це перетворює скомпрометований додаток на компроміс хоста з дуже малою кількістю додаткових кроків.
Типові потреби в capabilities за симптомом
- Потрібно створити TUN/TAP, налаштувати маршрути, iptables:
CAP_NET_ADMIN(і інодіCAP_NET_RAW). - Потрібно біндитись на порти <1024:
CAP_NET_BIND_SERVICE(Docker у багатьох випадках надає це за замовчуванням). - Потрібно змінювати час чи налаштування годинника:
CAP_SYS_TIME(намагайтесь уникати). - Потрібно монтувати файлові системи:
CAP_SYS_ADMIN(це «загальний» capability; уникайте за будь‑яку ціну). - Потрібно використовувати perf events: часто блокується налаштуваннями ядра і вимагає
CAP_SYS_ADMINабо змін sysctl; також часто блокується seccomp. - Потрібно змінювати власність на bind‑монтажі: можливо знадобиться
CAP_CHOWN, але зазвичай краще виправити власність на хості або використати правильний UID‑мапінг.
Нудні настанови, що працюють
Якщо вас тягне до --privileged, спитайте спочатку:
- Чи можна вирішити це вирівнюванням UID/GID або членством у групі?
- Чи можна вирішити це додаванням однієї capability і, можливо, одного device mapping?
- Чи можна вирішити це переміщенням привілейованої дії в sidecar/агент з вузьким API?
- Чи справді ми намагаємось робити адміністрування хоста зсередини прикладного контейнера?
UNIX‑сокети: docker.sock, рантайми контейнерів і ваші власні сокети
UNIX‑домені сокети — це файли, але вони не звичайні файли. «Права» застосовуються під час підключення, а сервіс за сокетом вирішує, що робити після з’єднання. Це означає два різні площини контролю:
- Файлова система права контролює, хто може підключитися.
- Авторизація сервісу (якщо є) контролює, що ви можете робити після підключення.
docker.sock: вогнегас із прекрасно поданим маркетингом
Монтування /var/run/docker.sock у контейнер — поширена практика. Це також руйнування межі безпеки. Якщо зловмисник отримає виконання коду в такому контейнері, він часто може створити новий контейнер з змонтованою файловою системою хоста і назвати це «debugging».
Отже, коли ви «виправляєте» permission denied на docker.sock, ви не просто вирішуєте технічну проблему. Ви надаєте контроль над хостом. Ставтесь до цього як до продакшен‑SSH ключів: строго, з аудитом і рідко.
Група і GID: генератор «працює на моєму ноуті»
Сокет docker зазвичай належить групі docker. Всередині контейнера ім’я «docker» не має значення; має значення числовий GID. Якщо сокет належить root:docker з GID 998, процес у контейнері має бути в GID 998, щоб підключитися.
Є три розумні опції:
- Використати
--group-addз GID хоста. Добре для швидких перевірок, але документуйте причину. - Створити групу в образі з тим самим числовим GID і запускати процес з нею. Краще для відтворюваності.
- Не монтувати сокет. Використовуйте вузький проксі або перепроєктуйте. Найбезпечніший варіант.
Ваші власні сервісні сокети (Postgres, Redis, системні демони)
Та сама схема: файл сокета на хості має owner/group/mode. Клієнтський процес у контейнері має відповідати цим правам з точки зору хоста. Якщо ви bind‑монтуєте /run/postgresql/.s.PGSQL.5432 у контейнер, клієнт всередині має права на підключення згідно з модом цього сокета.
Також: багато сервісів розміщують сокети під /run, який є tmpfs і відновлюється при перезавантаженні. Якщо ви «виправили» біти моду раз — ви нічого не закріпили. Ви залишили запис для майбутнього розчарування.
Пристрої: /dev, cgroups і чому mknod вам не друг
Ноди пристроїв у /dev — це спеціальні файли, що представляють інтерфейси ядра. Доступ контролюється:
- UNIX‑правами на ноді пристрою (власник/група/мод).
- Дозвольною таблицею контролера device cgroup (major/minor + r/w/m).
- Capabilities, потрібними для виконання операції (мережевий адміністратор, сирі ввід/вивід тощо).
- LSM‑ами і seccomp для певних чутливих операцій.
Найпоширеніші відмови доступу до пристроїв
/dev/net/tun: Ви передали пристрій, але забулиCAP_NET_ADMIN, або запускаєте rootless і чекали дива./dev/fuse: Ви передали пристрій, але забули, що модуль ядра fuse відсутній, або рантайм забороняє його без додаткових налаштувань.- GPU‑пристрої (
/dev/nvidia*,/dev/dri): членство в групі (частоvideoабоrender) і runtime‑хоки важать так само, як і ноди пристроїв. - Блок‑пристрої: якщо ви намагаєтесь монтувати або форматувати диски зсередини контейнера — зупиніться і подумайте про зону ураження. Потім подумайте ще раз.
--device — це точково; користуйтесь ним
Для доступу до пристроїв надавайте перевагу:
--device /dev/net/tun(або конкретному пристрою)--cap-addдля capability, що авторизує пов’язану операцію ядра- Явній груповій власності (наприклад, додати до
renderдля DRM‑пристроїв) коли це застосовно
Не кидайте --privileged заради одного відсутнього пристрою. Якщо ваш додаток потребує одного пристрою — дайте йому один. Якщо потрібно двадцять — це, можливо, не додаток, а агент, і агенти варті іншого розгляду.
Жарт №2: Ядру байдуже, що ваш контейнер «лише намагається допомогти». Воно як відділ кадрів: політика перш за все, емоції потім.
SELinux, AppArmor, seccomp: шар за шаром «виглядає як права»
Біти режиму — це лише зовнішня кірка. Наповнення — це політика.
SELinux: коли chmod — це перформанс‑арт
На системах з SELinux відмова часто логуються як AVC. У процесу є контекст безпеки (наприклад container_t), а в цілі — тип (наприклад docker_var_run_t). Якщо політика каже «ні», то це «ні».
Операційно, поширений фікс для bind‑монтажів — правильне позначення:
:Zдля релейбелінгу вмісту для виключного використання контейнером:zдля релейбелінгу для спільного використання
Якщо ви не релейбелите і SELinux у режимі Enforcing, ви можете бачити сокет і при цьому бути заблокованими при підключенні. Це не дивна поведінка Docker. Це SELinux виконує свою роботу.
AppArmor: тихіший, але теж гострий
AppArmor‑відмови з’являються в dmesg і можуть блокувати монт і ptrace та інші чутливі операції. Вони можуть проявлятись як permission denied або operation not permitted, залежно від виклику і способу відмови.
«Легкий» тест — --security-opt apparmor=unconfined. «Правильне» виправлення — написати профіль AppArmor, що дозволяє саме те, що вам потрібно. Якщо контейнер повинен монтувати довільні речі — це запах проблеми, а не вимога до профілю.
Seccomp: невидима рука, що повертає «EPERM»
Seccomp фільтрує системні виклики. Коли він блокує, ви часто отримуєте EPERM (Operation not permitted) і звіт про баг, який каже «права». Іноді це коректно; іноді це заблокований syscall, що не має нічого спільного з правами файлів.
Під час налагодження тимчасово запустіть з незв’язаним seccomp, щоб підтвердити діагноз, а потім або відрегулюйте профіль, або змініть підхід. Класичний приклад — робочі навантаження, що намагаються використовувати нові syscall, які дефолтний профіль не дозволяє.
Rootless Docker: інші правила, той самий біль
Rootless Docker чудово знижує ризик хоста. Це також реалії: якщо ваш додаток потребує дій, які зазвичай вимагають root на хості (пристрої, низькорівнева сітьова конфігурація, монт), rootless режим ускладнить або зробить це неможливим.
Як rootless змінює історію прав
- User namespaces не опціональні. UID/GID всередині контейнера мапляться на не‑root ID на хості.
- Доступ до пристроїв обмежений. Навіть якщо ви бачите ноди пристроїв, операції можуть блокуватись через відсутність привілеїв.
- Мережа може працювати інакше. Залежно від налаштування, ви можете використовувати slirp4netns чи подібне, що змінює сенс «мережевого адміністратора».
Rootless — це не «Docker, але безпечніший». Це «Docker з іншими обмеженнями». Якщо ви запускаєте системи збереження даних, VPN‑ендпоінти або щось, що має бути частиною ядра, rootless може бути невідповідним інструментом.
Три корпоративні історії з практики
Інцидент: неправильне припущення (root в контейнері == root на хості)
Команда розгорнула контейнеризований лог‑шипер, який мав читати ротаційні логи з шляху на хості. В стагінгу вони запускали контейнер як root і монтували /var/log. Працювало. Зміна пройшла рев’ю, бо «це тільки для читання».
У проді увімкнули user namespace remapping на Docker‑демоні. Усередині контейнера процес був UID 0. На хості він відображався на непривілейований UID‑діапазон. Раптом шипер почав падати з permission denied на деяких ротаційних файлах, а на інших — ні. Помилки були періодичні, бо власність ротаційних логів відрізнялась по сервісах і завданнях ротації.
Вони пробували звичні заклинання: chmod на хості, перезапуск контейнера, запуск як root (він уже був), додавання --privileged (що не допомогло стабільно через userns‑мапінг). Тим часом оповіщення почали пропускати логи саме від сервісів, які потрібні під час інцидентів.
Виправлення було нудним: перестати вважати, що UID 0 щось означає через межу неймспейсів. Вирівняли власність, використавши ACL на директоріях логів для промапленого UID‑діапазону, і змінили шипер, щоб він запускався з конкретним UID, що відповідав політиці хоста. Додали canary‑перевірку, яка читає відомий лог‑шлях при старті і завершує роботою при помилці, замість «best‑effort» мовчазної втрати логів.
Висновок постмортему: контейнери не «зламали права». Помилкове припущення команди — так. Неймспейси — це не атмосфера; це математика.
Оптимізація, що відкотилась: «просто примонтувати docker.sock, щоб не розгортати агенти»
Платформна команда хотіла пришвидшити CI‑джоби. Ідея: запускати білд‑контейнери, що спілкуються з демоном Docker хоста через /var/run/docker.sock. Ніякої вкладеної віртуалізації, ніяких окремих білдерів, менше витрат. Усі раділи, бо швидко й дешево.
Потім розробник додав залежність, яка виконувала post‑install скрипт із стороннього пакету. Скрипт не мав злого наміру; він був неакуратний і припускав, що може інспектувати середовище. Він звернувся до Docker API, помітив, що має контроль, і запустив допоміжний контейнер. Той контейнер змонтував шляхи хоста, щоб «кешувати» речі. Кеш містив креденшіали і конфіги, які не повинні були бути видимі для білду.
Безпека помітила це під час рев’ю, бо логи білду почали містити дивні деталі середовища. Нічого не було експлуатовано, але це був близький випадок. Проблема в тому, що монтування docker.sock еквівалентне наданню контейнеру адміністративного API хоста.
Ремедіація — контрольований сервіс билдера з вузьким API: «збілдь цей репозиторій на цьому коміті з такою конфігурацією». Ніякого загального Docker API. Біди стали трохи повільніші. Організація спала спокійніше. І команда перестала сприймати docker.sock як зручну фічу.
Урок: якщо ваша «оптимізація» скорочує межу довіри, це не оптимізація. Це борговий інструмент з плаваючою ставкою відсотків.
Нудна, але правильна практика, що врятувала день: явні capabilities і preflight‑перевірки
Група з продукту мережевого програмного забезпечення запускала контейнери, які налаштовували TUN‑інтерфейси і застосовували маршрути. Ранні прототипи були всі з --privileged, бо мета була показати демо, а не вражати ядро. Коли продукт пішов у продакшен, SRE наполіг задокументувати мінімальний набір: --device /dev/net/tun, --cap-add NET_ADMIN плюс кілька sysctl‑ів, що керуються поза контейнером.
Це здавалося педантичним. Але змусило команду задокументувати точні операції контейнера і де вони виконуються. Додали preflight при старті: перевірити наявність /dev/net/tun, підтвердити наявність CAP_NET_ADMIN (через невелику самоперевірку) і вивести чітку помилку, якщо чогось бракує.
Місяць потому рутинне посилення хостів змінило дефолтний seccomp профіль на підмножині нод. Декілька контейнерів почали падати на старті з помилками, схожими на доступ. Завдяки preflight, триаж був швидким: логи говорили «NET_ADMIN відсутній» на уражених нодах. Він не був відсутній; його обрізав невірно застосований політик. Вони відкотили політику, а потім виправили процес розгортання.
Ніякої героїки. Ніхто не ліз у SSH о 3‑й ранку. Система чесно повідомила помилку рано, і виправлення було очевидним. Ось та нудьга, яка потрібна в продакшені.
Поширені помилки: симптом → корінь проблеми → виправлення
1) Симптом: «permission denied» при підключенні до /var/run/docker.sock
Корінь проблеми: користувач контейнера не в групі сокета (або невідповідність GID), або SELinux забороняє connect.
Виправлення: вирівняйте GID за допомогою --group-add або створення групи в образі; для SELinux використайте правильне маркування або політику. І переосмисліть, чи потрібно взагалі монтувати docker.sock.
2) Симптом: нода пристрою існує, але open() повертає permission denied
Корінь проблеми: device cgroup забороняє, або відсутня capability для повʼязаної операції.
Виправлення: передайте пристрій через --device з rwm; додайте мінімальну capability (часто NET_ADMIN для TUN). Перевірте через docker inspect і capsh.
3) Симптом: chmod 666 все одно не допомагає (сокет або файл)
Корінь проблеми: SELinux/AppArmor відмова, або об’єкт не той, що ви думаєте (наприклад, новий сокет перезаписано під /run).
Виправлення: перевірте AVC/AppArmor журнали, позначте монтування правильно, і виправте сервіс, що створює сокет (systemd unit permissions), замість ганятися за тимчасовими файлами.
4) Симптом: працює з –privileged, падає без нього
Корінь проблеми: вам потрібен один із: device mapping, capability, seccomp‑дозвіл або права монтування.
Виправлення: робіть бісекцію: спочатку додайте --device (якщо релевантно), потім одну capability, потім тестуйте seccomp unconfined, щоб підтвердити. Замініть privileged явними вимогами.
5) Симптом: rootless контейнери не можуть отримати доступ до /dev/kmsg, /dev/net/tun або монтувати ФС
Корінь проблеми: rootless дизайнно не має привілеїв root на хості.
Виправлення: не запускайте такі навантаження в rootless. Використайте вузол з rootful для привілейованих задач або перенесіть операцію на хост через агента.
6) Симптом: «Operation not permitted» при mount або setns
Корінь проблеми: блоковано seccomp/AppArmor або бракує CAP_SYS_ADMIN.
Виправлення: уникайте виконання цього всередині контейнера. Якщо це неминуче, використайте кастомний seccomp/AppArmor профіль і обґрунтуйте потребу в capability. Розглядайте CAP_SYS_ADMIN як «privileged‑lite».
7) Симптом: контейнер бачить сокет, але підключення падає тільки на деяких хостах
Корінь проблеми: відмінності політик на хості (наприклад, SELinux у режимі Enforcing на деяких, різні GID, різні systemd socket unit права).
Виправлення: уніфікуйте конфіг хостів; додайте preflight‑перевірки, що логують власність/GID сокета при старті контейнера; сприймайте дрейф як причину інциденту.
8) Симптом: доступ до шляхів є, але не працює при bind‑монтуванні
Корінь проблеми: опції/мітки монтування (SELinux), або userns‑мапінг змінює семантику власності.
Виправлення: використайте :Z/:z для SELinux; забезпечте відповідність UID/GID; розгляньте іменовані томи з керованою власністю, якщо доречно.
Чеклісти / покроковий план
Чекліст A: Виправити «permission denied» для UNIX‑сокета без переходу в privileged
- Визначте шлях сокета із логів або strace‑подібної дебагінгової інформації в додатку.
- На хості: зробіть
ls -lна сокеті і зафіксуйте власника/групу/мод. - У контейнері: перевірте
idдля UID/GID. - Вирівняйте доступ групи за числовим GID:
- Віддавайте перевагу
--group-add <gid>як швидкий тест. - Для продакшену створіть групу в образі з тим самим GID і запускайте процес у ній.
- Віддавайте перевагу
- Якщо SELinux увімкнено: перевірте AVC‑журнали; позначте монтування коректно.
- Якщо AppArmor увімкнено: перевірте
dmesgна відмови; відрегулюйте профіль при потребі. - Документуйте ризик якщо сокет має адміністративні права (docker.sock, containerd, systemd).
Чекліст B: Виправити «permission denied» для /dev пристрою правильно
- Підтвердіть наявність ноди пристрою в контейнері (
ls -l). - Підтвердіть, що пристрій передано (
docker inspect .HostConfig.Devices). - Визначте потрібну capability для операції (TUN і маршрути:
NET_ADMIN, сирі сокети:NET_RAW). - Додавайте по одній capability і перетестовуйте.
- Перевірте журнали LSM/seccomp, якщо відмова не зникає.
- За замовчуванням відмовте у CAP_SYS_ADMIN. Якщо хтось просить його, нехай пояснить syscall і альтернативу.
Чекліст C: Безпечне використання docker.sock (якщо вже зовсім потрібно)
- Змоделюйте загрозу: припускайте, що компрометація контейнера = компрометація хоста.
- Обмежте, хто може його деплоїти і де він може працювати (виділені ноди, жорсткі мережеві політики).
- Запускайте як non‑root і додайте лише групу docker за числовим GID.
- Віддавайте перевагу проксі, що відкриває лише потрібні ендпоінти, а не весь Docker API.
- Додайте моніторинг на несподіване створення контейнерів і монтування хост‑шляхів через цей сокет.
Поширені питання (FAQ)
1) Чому контейнер, що працює як root, все одно отримує «permission denied»?
Бо «root» всередині контейнера може не бути root на хості (user namespaces), і тому LSM, seccomp та правила cgroup device можуть забороняти доступ незалежно від UID.
2) Чи коли‑небудь варто монтувати /var/run/docker.sock у контейнер?
Рідко. Розглядайте це як надання адміністративного контролю над хостом. Якщо робите так, використайте явний GID‑мапінг, суворі обмеження розгортання і задокументуйте виняток.
3) У чому різниця між --cap-add і --privileged?
--cap-add надає конкретну kernel capability. --privileged надає широкий набір capabilities, послаблює обмеження з пристроями і послаблює ізоляцію в кількох аспектах. Одне — скальпель, інше — вилочний навантажувач.
4) Моєму додатку потрібен CAP_SYS_ADMIN. Це прийнятно?
Припускайте «ні», поки не доведено протилежне. CAP_SYS_ADMIN охоплює величезний спектр операцій (включно з монтами і взаємодією з неймспейсами). Часто є альтернатива: зробіть mount на хості, використайте volume plugin або перепроєктуйте робочий процес.
5) Чому на моєму ноутбуці працює, а в проді — ні?
Різні політики хоста: SELinux у проді в режимі Enforcing, відмінні AppArmor профілі, різні GID групи docker, увімкнено userns‑remap або різні версії ядра/seccomp. Контейнери портабельні; політики хостів — ні.
6) Як дізнатися, чи SELinux — причина проблеми?
Якщо getenforce повертає Enforcing і ви бачите AVC‑відмови в журналах аудиту, що посилаються на контекст вашого контейнера і цільовий об’єкт, SELinux — причина. Виправте мітки або політику; не намагайтеся вирішити chmod‑ом.
7) Який найбезпечніший спосіб надати контейнеру доступ до UNIX‑сокета хоста?
Переконайтесь, що сокет обслуговує неадміністративний API; встановіть режим сокета з вимогою виділеної групи; додайте у контейнер лише цей числовий GID; уникайте запуску всього контейнера як root. Якщо це адміністративний сокет — серйозно перегляньте рішення.
8) Чим права на пристрої відрізняються від звичайних файлових прав?
Ноди пристроїв також проходять через allow‑list контролера device cgroup і часто вимагають capabilities для повʼязаних привілейованих операцій ядра. Наявність /dev/net/tun не означає, що ви зможете його використовувати.
9) Чи rootless Docker вирішує ці проблеми з правами?
Він знижує ризики, але також прибирає можливість виконувати багато привілейованих дій. Якщо ваше навантаження потребує пристроїв або конфігурації мережі ядра, rootless швидше ускладнить завдання, ніж допоможе.
Наступні кроки, які ви реально можете зробити
Якщо ви зараз дивитесь на «permission denied», перестаньте гадати і почніть класифікувати:
- Це сокет чи пристрій? Цей вибір визначає найшвидший шлях.
- Перевірте ідентичність (UID/GID і мапінг) перед тим як чіпати capabilities.
- Перевірте SELinux/AppArmor/seccomp журнали, коли chmod не змінює результат.
- Замініть privileged явними вимогами: один
--device, одна--cap-add, один--group-add, плюс маркування при потребі. - Впишіть preflight‑перевірки в контейнери, що залежать від інтеграції з хостом, щоб відмови були голосні й точні.
І якщо хтось просить «просто примонтувати docker.sock», спитайте, яку проблему вони насправді вирішують. Здебільшого правильне виправлення — не в правах. Воно в архітектурі.