«Permission denied» для Docker-томів ніколи не буває просто проблемою прав доступу. Це проблема невідповідності ідентичностей, замаскована під помилку файлової системи, і зазвичай вона проявляється в найгірший момент: під час деплою, міграції або «маленької зміни», яку хтось запевняв, що вона безпечна.
Якщо ви запускаєте контейнери в продакшені, ви не можете ставитися до UID/GID як до дрібниць. Контейнери пишуть файли. Хости застосовують власність. Мережеве сховище додає свою логіку. Посилення безпеки додає ще думок. Ваше завдання — узгодити ці думки.
Чому це повторюється (це ідентичність, а не магія)
У Linux права доступу контролюються числовими ідентифікаторами: UID — для власника, GID — для групи. Імена на кшталт www-data — це лише таблиця відповідностей. Коли процес у контейнері записує в змонтований каталог, файловій системі хоста видно UID і GID, а не «користувача контейнера».
Ось основна проблема: для контейнера UID 1000 може означати «appuser», а для хоста UID 1000 може означати «alice», а ваш NFS-сервер може вирішити, що це «nobody». Та ж цифра — різне значення, або те ж значення — інший номер. В будь-якому випадку хост відповідає: ні.
Docker-томи роблять це очевидним, бо вони буквально є файловими системами (bind-монти) або директоріями, якими керує хост (іменовані томи). Ваш контейнер — це не маленька ВМ з власним ядром і всесвітом прав. Це просто процеси з обмеженим поглядом на хост. Ядро — це швейцар, і воно перевіряє ідентифікатори, а не виправдання.
Ще одна гостра деталь: багато офіційних образів за замовчуванням збудовані для запуску від root. Це «працює», поки ви не змонтуєте шлях хоста, яким володіє звичайний користувач, або поки ви не посилите безпеку контейнера, щоб він працював як non-root, або поки ваш бекенд зберігання не відмовить у записі через root-squash. Тоді ви дізнаєтесь, що «легкий» шлях — це технічний борг з таймером.
Короткий жарт №1: Контейнери не «мають прав». Вони позичають ваші, псують килим і лишають вам звіт з безпеки.
Факти та історія, що пояснюють сучасний біль
- Unix-права старші за більшість вашої інфраструктури: застосування UID/GID сягає раннього Unix; модель проста, довговічна і байдужа до контейнерів.
- Ранні налаштування Docker віддавали перевагу зручності: ранні робочі процеси Docker заохочували «запуск від root», бо це уникало тертя; пізніше безпека в продакшені зробила цей вибір дорогим.
- Bind-монти існували задовго до контейнерів: ядру байдуже, що монтування походить від Docker; воно застосовує ті самі правила власності.
- Користувацькі простори (user namespaces) існували давно: user namespaces з’явилися роки тому, але їх впровадження відставало, бо вони потужні й легко помилково налаштовуються.
- NFS root squash — навмисна міна-пастка: багато NFS-експортів відображають віддаленого root на
nobodyза дизайном, щоб запобігти тому, щоб контейнерний root став root сховища. - SELinux/AppArmor змінили гру: сучасні дистрибутиви можуть забороняти доступ навіть коли UID/GID виглядають правильними, бо MAC (mandatory access control) стоїть вище за класичні DAC-права.
- Overlay-файлові системи додають заплутаних симптомів: overlay2 може створити враження, що права «випадково змінилися», коли ви фактично бачите об’єднані шари та непрозорі директорії.
- Kubernetes нормалізував non-root навантаження: коли кластери почали застосовувати «runAsNonRoot», індустрія перестала уникати акуратних припущень про UID/GID.
Є причина, чому це не «виправив» кращий рантайм для контейнерів: ядро робить саме те, що має робити.
Цитата з операцій, яку варто мати на столі: «Надія — це не стратегія.»
— General Gordon R. Sullivan
Швидкий план діагностики (перший/другий/третій)
Перший: визначте, ким процес вважає себе
Якщо ви не знаєте ефективного UID/GID у контейнері, ви лише здогадуєтесь. Отримайте числа. Підтвердіть, під яким користувачем фактично працює процес додатка, а не те, що натякає Dockerfile.
Другий: з’ясуйте, який шлях на хості змонтовано і хто ним володіє
«Том» може означати bind-монт, іменований том, NFS, CIFS або плагін-монтування. Кожен має іншу поведінку власності та ACL. Визначте реальний бекап-путь, його власність, біти режиму, ACL та будь-які MAC-мітки.
Третій: перевірте політики вище POSIX-прав
Коли біти режиму виглядають нормально, але все одно не працює, зазвичай це SELinux, AppArmor або root squash на мережевому сховищі. Не витрачайте годину на chmod, щоб потрапити в неправильну канаву.
Швидке дерево рішень
- Працює без тому, не працює з томом: невідповідність ідентичностей або політика сховища.
- Працює як root, не працює як non-root: власність директорії/біти режиму неправильні для очікуваного UID/GID.
- Працює на локальному диску, не працює на NFS/CIFS: root squash, idmapping або серверні дозволи/ACL.
- Проблема лише на Fedora/RHEL: спочатку підозрівайте SELinux-маркування.
Практичні завдання: команди, виводи та рішення (12+)
Це команди, які я справді виконую під час інцидентів. Кожне завдання включає: команду, що означає вивід, і рішення, яке ви приймаєте.
Завдання 1: підтвердити користувача контейнера (ефективний UID/GID)
cr0x@server:~$ docker exec -it app sh -lc 'id; umask'
uid=10001(app) gid=10001(app) groups=10001(app),10002(shared)
0022
Значення: Додаток пише як UID 10001/GID 10001, стандартний umask 0022 (файли 644, директорії 755, якщо не змінено).
Рішення: Змонтована директорія має бути записуваною для UID 10001 або групи, до якої належить процес (наприклад, GID 10002), або через ACL.
Завдання 2: підтвердити, що і де змонтовано (всередині контейнера)
cr0x@server:~$ docker exec -it app sh -lc 'mount | sed -n "1,5p"; mount | grep -E "/data|/var/lib"'
overlay on / type overlay (rw,relatime,lowerdir=...,upperdir=...,workdir=...)
proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)
tmpfs on /dev type tmpfs (rw,nosuid,size=65536k,mode=755,inode64)
devpts on /dev/pts type devpts (rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=666)
sysfs on /sys type sysfs (ro,nosuid,nodev,noexec,relatime)
/dev/sdb1 on /data type ext4 (rw,relatime)
Значення: /data — це реальний mount, ext4, а не внутрішній overlay шлях.
Рішення: Розслідування має включати власність/біти режиму файлової системи хоста для точки монтування ext4.
Завдання 3: переглянути конфігурацію монтування Docker (вид з хоста)
cr0x@server:~$ docker inspect app --format '{{json .Mounts}}'
[{"Type":"bind","Source":"/srv/app/data","Destination":"/data","Mode":"rw","RW":true,"Propagation":"rprivate"}]
Значення: Це bind-монт з /srv/app/data. Питання в дозволах цієї директорії, а не в драйвері томів Docker.
Рішення: Виправити власність/біти/ACL для /srv/app/data, або змінити runtime UID/GID контейнера.
Завдання 4: перевірити власність, права та ACL директорії на хості
cr0x@server:~$ sudo ls -ldn /srv/app/data
drwxr-x--- 2 0 0 4096 Jan 2 10:11 /srv/app/data
cr0x@server:~$ sudo getfacl -p /srv/app/data
# file: /srv/app/data
# owner: root
# group: root
user::rwx
group::r-x
other::---
Значення: Власник root, група root, і «other» не має доступу. UID 10001 не може писати або навіть читати.
Рішення: Або chown/chgrp підходящим UID/GID, або надати доступ через членство в групі/ACL. Не ставте chmod 777, якщо ви любите післяінцидентні звіти.
Завдання 5: відтворити помилку тестом запису (всередині контейнера)
cr0x@server:~$ docker exec -it app sh -lc 'touch /data/.permtest && echo OK'
touch: cannot touch '/data/.permtest': Permission denied
Значення: Це не ваш додаток. Це базовий збій запису.
Рішення: Виправте доступ до файлової системи спочатку; не перенаcтройуйте додаток, поки простий touch не пройде.
Завдання 6: перевірити, чи SELinux відмовляє (хост)
cr0x@server:~$ getenforce
Enforcing
cr0x@server:~$ sudo ls -ldZ /srv/app/data
drwxr-x---. 2 root root unconfined_u:object_r:default_t:s0 4096 Jan 2 10:11 /srv/app/data
Значення: SELinux у режимі Enforcing, і директорія має загальну мітку (default_t), яку контейнери можуть не мати права читати.
Рішення: Додайте відповідні опції монтування SELinux (:Z або :z) для bind-монтів, або перемаркуйте шлях під хостом у тип, дружній до контейнера.
Завдання 7: перевірити AppArmor профіль (хост)
cr0x@server:~$ docker inspect app --format '{{.AppArmorProfile}}'
docker-default
Значення: Застосовано стандартний профіль AppArmor. Зазвичай це нормально, але кастомні профілі можуть блокувати монти або шляхи.
Рішення: Якщо права виглядають правильними і SELinux вимкнено, аудитуйте журнали AppArmor і правила профілю.
Завдання 8: визначити бекап-путь іменованого тому (якщо це не bind-монт)
cr0x@server:~$ docker volume inspect appdata --format '{{.Mountpoint}}'
/var/lib/docker/volumes/appdata/_data
cr0x@server:~$ sudo ls -ldn /var/lib/docker/volumes/appdata/_data
drwxr-xr-x 2 0 0 4096 Jan 2 09:50 /var/lib/docker/volumes/appdata/_data
Значення: Іменовані томи за замовчуванням належать root на хості, якщо образ або крок ініціалізації не змінюють цього.
Рішення: Створіть контрольований крок ініціалізації для встановлення власності один раз, або запускайте сервіс з відповідним UID/GID.
Завдання 9: перевірити, чи ви на NFS і чи root squash вас кусає
cr0x@server:~$ mount | grep -E ' nfs| nfs4'
10.0.2.10:/exports/app on /srv/app/data type nfs4 (rw,relatime,vers=4.1,proto=tcp,clientaddr=10.0.2.21,local_lock=none,sec=sys)
cr0x@server:~$ sudo touch /srv/app/data/.hosttest
touch: cannot touch '/srv/app/data/.hosttest': Permission denied
Значення: Навіть root хоста не може записати. Це класичний root squash або серверні дозволи/ACL.
Рішення: Перестаньте вважати це проблемою Docker. Виправте експорт NFS та відображення ідентичностей на сервері, або використайте виділений сервісний UID, який існує послідовно на всіх клієнтах.
Завдання 10: перевірити числове відображення власності між хостом і контейнером
cr0x@server:~$ getent passwd 10001
appuser:x:10001:10001::/nonexistent:/usr/sbin/nologin
cr0x@server:~$ docker exec -it app sh -lc 'getent passwd 10001 || true; cat /etc/passwd | tail -n 3'
app:x:10001:10001:app:/home/app:/bin/sh
messagebus:x:100:102::/nonexistent:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
Значення: UID 10001 існує на хості і в контейнері; гарний знак. Якщо хост показує іншого користувача для 10001, історія «збіг UID» — це фантазія.
Рішення: Якщо можна, стандартизуйте UID між системами для stateful сервісів. Якщо не можна — використайте ACL або idmapped mounts (де доступно) замість наївних очікувань.
Завдання 11: протестувати доступ через групу (здоровий середній шлях)
cr0x@server:~$ sudo groupadd -g 10002 shared 2>/dev/null || true
cr0x@server:~$ sudo chgrp -R 10002 /srv/app/data
cr0x@server:~$ sudo chmod -R g+rwX /srv/app/data
cr0x@server:~$ sudo chmod g+s /srv/app/data
cr0x@server:~$ sudo ls -ldn /srv/app/data
drwxrws--- 2 0 10002 4096 Jan 2 10:15 /srv/app/data
Значення: Директорія належить групі 10002 і встановлено setgid, тож нові файли успадковують групу 10002.
Рішення: Зробіть процес контейнера членом GID 10002 (через образ або runtime). Це уникає chown всього під один UID і краще підходить для спільного доступу.
Завдання 12: додати ACL для конкретного UID (коли груп недостатньо)
cr0x@server:~$ sudo setfacl -m u:10001:rwx /srv/app/data
cr0x@server:~$ sudo setfacl -m d:u:10001:rwx /srv/app/data
cr0x@server:~$ sudo getfacl -p /srv/app/data | sed -n '1,12p'
# file: /srv/app/data
# owner: root
# group: shared
user::rwx
user:10001:rwx
group::rwx
mask::rwx
other::---
default:user::rwx
default:user:10001:rwx
default:group::rwx
Значення: UID 10001 має явний доступ, і нові файли успадковують його через default ACL.
Рішення: Використовуйте ACL, коли потрібно кілька записувачів з різними UID, особливо між хостами, без вдавання до 777.
Завдання 13: перевірити атрибути immutable (момент «чому chmod не допомагає»)
cr0x@server:~$ sudo lsattr -d /srv/app/data
-------------e-- /srv/app/data
Значення: Немає immutable-флагу. Якщо бачите i, зміни будуть невдалими мовчки або з EPERM.
Рішення: Якщо immutable встановлено, видаліть його усвідомлено (chattr -i) і задокументуйте, навіщо він був.
Завдання 14: перевірити реальний шлях запису і помилки через strace (точково, не щодня)
cr0x@server:~$ docker exec -it app sh -lc 'strace -f -e trace=file -o /tmp/trace.log sh -lc "touch /data/x" || true; tail -n 5 /tmp/trace.log'
openat(AT_FDCWD, "/data/x", O_WRONLY|O_CREAT|O_NOCTTY|O_NONBLOCK, 0666) = -1 EACCES (Permission denied)
write(2, "touch: cannot touch '/data/x': Permission denied\n", 52) = 52
exit_group(1) = ?
+++ exited with 1 +++
Значення: Ядро повернуло EACCES при створенні. Це класичне відхилення DAC/MAC, не «файл вже існує», не «диск заповнений».
Рішення: Поверніться до власності/ACL/SELinux. Не витрачайте час на налаштування на рівні додатка.
Стратегії UID/GID, що реально працюють
Стратегія 1: Збігайте власність хоста (запускайте контейнер із тим самим UID/GID)
Це найчистіший підхід для bind-монтів на одному хості: оберіть сервісний UID/GID на хості й запускайте процес контейнера під тим самим числовим ідентифікатором.
У Docker Compose це зазвичай виглядає так:
cr0x@server:~$ cat docker-compose.yml
services:
app:
image: yourorg/app:1.2.3
user: "10001:10001"
volumes:
- /srv/app/data:/data:rw
Коли застосовувати: однохостові stateful сервіси, прості деплої, локальні диски, передбачувані UID.
Не застосовувати коли: ви ділите той самий том між багатьма хостами з різною алокацією UID, або залежите від додаткових груп, які різняться між середовищами.
Стратегія 2: Доступ через групи + setgid директорії (кращий дефолт для спільного тому)
Якщо більше ніж один сервіс потребує доступу, або оператори торкаються файлів, доступ через групу — ваш друг. Створіть спільний GID, змініть групову власність директорії, встановіть setgid і забезпечте, щоб користувачі контейнера приєднувалися до цієї групи.
На стороні контейнера можна додати supplementary groups:
cr0x@server:~$ cat docker-compose.yml
services:
app:
image: yourorg/app:1.2.3
user: "10001:10001"
group_add:
- "10002"
volumes:
- /srv/app/data:/data:rw
Чому це працює: масштабується краще, ніж власність по UID, а setgid запобігає «дрейфу», коли деякі файли належать випадковим групам.
Стратегія 3: ACL для змішаних записувачів (точний інструмент, не кувалда)
POSIX ACL дозволяють надати доступ кільком UID/GID без зміни основного власника. Вони ідеальні, коли директорією керує одна команда, але записують кілька сервісів з різними числовими ідентичностями.
Правило: якщо ви використовуєте ACL, застосовуйте також default ACL, інакше ви виправите сьогоднішній інцидент і зламаєте створення файлів завтра.
Стратегія 4: Одноразова ініціалізація (chown один раз, не при кожному старті)
Багато образів роблять chown -R під час старту. Це прийнятно тільки коли директорія з даними мала і локальна. У продакшені з великими даними рекурсивний chown при кожному запуску — самоподібна відмова у доступі.
Краще: явна init job, що виконується один раз для нового тому, встановлює власність і ніколи не запускається знову, якщо ви навмисно не ротуєте сховище.
Приклад: тимчасовий контейнер для ініціалізації іменованого тому:
cr0x@server:~$ docker run --rm -u 0:0 -v appdata:/data busybox sh -lc 'mkdir -p /data && chown -R 10001:10001 /data && ls -ldn /data'
drwxr-xr-x 2 10001 10001 4096 Jan 2 10:20 /data
Рішення: Використовуйте це для іменованих томів і першого запуску. Не вписуйте рекурсивний chown у основний старт сервісу, якщо вам не подобаються повільні рестарти і довгі простої.
Стратегія 5: Не боріться з образом — обирайте образи з підтримкою non-root
Деякі образи мають чітко визначеного non-root користувача і підтримують PUID/PGID як змінні середовища, або коректно приймають runtime --user. Інші розраховані на root і потім креативно ламаються, коли ви позбавляєте їх root.
Ваше рішення: якщо образ stateful сервісу не може працювати як non-root без хаків, трактуйте його як ризик. Виправте Dockerfile всередині або змініть образ. «Але воно працює як root» — це не позиція з безпеки.
Стратегія 6: Відображення user namespace (сильна ізоляція, додаткова складність)
User namespaces дозволяють відображати root контейнера (UID 0) на неповноважений діапазон UID на хості. Це зменшує зону ураження при втечі контейнера. Але це також ускладнює права томів, якщо ви не сплануєте це.
Коли увімкнено, файл, створений «root у контейнері», може показатися як UID 165536 на хості, бо це відображений хост-UID. Це правильна поведінка. Просто дивує, якщо ви не підписались на це.
Коли використовувати: потрібний сильніший захист хоста і ви можете стандартизувати відображення по вузлах.
Коли уникати: коли ви сильно залежите від bind-монтів до директорій, якими керують люди, і не можете терпіти складність відображення.
Стратегія 7: Rootless Docker (добра базова безпека, все одно потребує планування)
Rootless Docker запускає демон і контейнери без привілеїв root. Це значно зменшує інциденти «root контейнера став root хоста». Але томи все ще мають правильну власність під домашнім користувачем rootless та підпорядкованими діапазонами UID/GID.
Ключовий момент: rootless змінює місце зберігання і як відображаються ідентичності. Це не просто заміна для «всього під /srv».
Стратегія 8: Kubernetes: runAsUser + fsGroup (якщо ви в тому світі)
У Kubernetes еквівалент «стратегії UID/GID» зазвичай — securityContext з runAsUser, runAsGroup і fsGroup. fsGroup допомагає, бо kubelet може змінювати групову власність/права на змонтованих томах, щоб груповий запис працював.
Але не вважайте fsGroup магією. Для деяких типів томів потрібна рекурсивна зміна прав, що може бути болісно повільно на великих даних. Плануйте це.
Короткий жарт №2: «Просто chmod 777» — це еквівалент «просто перезавантажити» для сховища: іноді ефективно, завжди підозріло.
Специфічні для сховища режими відмов (ext4, NFS, CIFS, ZFS, overlay)
Локальні файлові системи (ext4/xfs): передбачувані, але їх легко зіпсувати
На локальному диску UID/GID та біти режиму зазвичай пояснюють усе. Якщо щось не так — це через людей (або init-скрипти), які це зіпсували. Поширені шаблони саботажу:
- Шлях на хості створено як root під час провізії і ніколи не chown-или.
- Директорія групово записувана, але файли не успадковують групу, бо setgid не встановлено.
- Umask надто строгий (наприклад, 0077) і файли стають приватними за замовчуванням.
- ACL mask обмежує доступ, навіть якщо ACL-записи існують.
NFS: ідентичність — це політика
Проблеми з дозволами NFS часто пов’язані з відображенням ідентичностей. Якщо ви використовуєте AUTH_SYS (класичний «sec=sys»), сервер довіряє клієнту надсилати UID. Це означає: послідовність числових ідентифікаторів між машинами не є опціональною; це вся модель безпеки.
Якщо увімкнено root squash (часто за замовчуванням), UID 0 на клієнті відображається на неповноваженого користувача на сервері. В контейнерному світі це означає, що «запуск від root» — не та кнопка, якою ви думали.
Практична порада: оберіть сервісні акаунти з фіксованими UID по всіх вузлах, керуйте ними централізовано і не сподівайтеся, що мапінг імен вас врятує. NFS не дбає, як ви називаєте користувача.
CIFS/SMB: права можна підробити на боці клієнта
SMB-монти на Linux можуть показувати вигляд прав, що частково синтетичний. Опції монтування як uid=, gid=, file_mode= і dir_mode= можуть зробити все виглядати записуваним, поки сервер не скаже інакше через власні ACL.
Режим відмов: Ви думаєте, що виправили це зміною опцій монтування, але серверний ACL все одно забороняє запис. Або ви «виправляєте» це, примусово встановлюючи власність, і тоді ламаєте аудит, бо всі файли належать одному локальному UID.
ZFS: чудовий для даних, суворий щодо власності
ZFS не особливий у плані POSIX-прав; він просто послідовний. Така послідовність може бути жорсткою, коли ви очікуєте, що «Docker впорається». Якщо ви робите snapshot і rollback, зміни власності теж відкотяться. Це і фіча, і пастка під час інциденту.
overlay2: ілюзія записуваних шарів
Root-файлова система контейнера зазвичай overlay2: об’єднання read-only шарів образу і writable upper layer. Томні монти обходять це. Тому якщо додаток записує в /tmp, але не може в /data, це очікувано: /tmp у writable-шарі контейнера, а /data — це те, що контролює хостова файловa система.
SELinux: права можуть бути правильні і все одно неправильні
На системах з SELinux важлива мітка. Bind-монтована директорія з міткою default_t може бути недоступною контейнерам навіть якщо UID/GID ідеальні. Docker підтримує перемаркування через прапорці монтування:
:Zдля приватної мітки (ексклюзивно для одного контейнера):zдля розділеної мітки (декілька контейнерів)
Обирайте свідомо. Якщо ви ділите той самий шлях хоста між контейнерами і використовуєте :Z, ви перемаркуєте його у глухий кут.
Три корпоративні міні-історії з передової
Міні-історія 1: Інцидент через хибне припущення («root може писати будь-куди»)
Середня компанія запускала контейнеризований ETL-заданий, який писав parquet-файли в спільний NFS-експорт. Це «працювало» місяцями, що в основному означало, що ніхто його не чіпав. Потім служба безпеки попросила перестати запускати контейнери як root, і команда погодилась, встановивши user: "10001:10001" у Compose.
Наступної ночі ETL відразу впав: Permission denied у вихідному каталозі. Інженер on-call спробував класичне: запустити знову як root. Та ж помилка. От тоді і померло припущення: «root у контейнері = root у сховищі». Ні. NFS root squash відобразив клієнтський root на неповноваженого користувача на сервері.
Вони потім спробували іншу класичну «лікувальну» дію: chmod каталогу на 777 на клієнтському монті. Нічого не змінилося. Бо серверний експорт мав ACL, що все одно забороняли запис, і chmod на клієнті не змінював серверні ACL так, як вони уявляли.
Справжнє нудне виправлення: створити виділений сервісний акаунт з фіксованим UID/GID, забезпечити наявність цього акаунту з тими ж числами на всіх ETL-вузлах і встановити серверну власність відповідно. Також додали просту перевірку: створити тимчасовий файл у цільовому каталозі перед запуском дорогого завдання.
Постмортем також був нудний: оновили runbook, щоб трактувати NFS як окрему доменну зону безпеки з власними правилами. Той один абзац врятував наступних on-call від повторення того самого танцю.
Міні-історія 2: Оптимізація, яка обернулась проти (рекурсивний chown на старті)
Інша команда запускала stateful сервіс у контейнерах з іменованим томом. Інженер помітив випадкові помилки прав після міграції й вирішив «закріпити» старт: кожний старт контейнера виконував chown -R app:app /var/lib/app перед запуском демона.
У деві це було чудово. Новий том, мало даних, швидкий chown, і більше проблем з правами немає. У продакшені dataset був великий і на повільніших дисках. Під час звичайного деплою сервіс стартував повільно, потім ще повільніше, потім виглядав мертвим. Health check впав. Оркестратор перезапускав його. Це спричинило чергу рестартів з рекурсивним chown. Тепер у них був рестарт-луп, що робив важкі операції метаданих на тій самій деревоподібній структурі.
Графіки сховища показали правду: висока IOPS, здебільшого операції метаданих, і спайки латентності, що впливали на інші сервіси. Аутейдж не спричинив додаток. Його спричинила «оптимізація», що перетворила кожний рестарт на тест на навантаження сховища.
Виправили, прибравши chown зі старту і замінивши його одноразовим init job, що виконується лише при створенні нового тому. Також змінили стратегію деплою: не оновлюйте всі інстанси одразу і не перезапускайте у циклі на повільних стартах без backoff.
Урок: виправлення прав, що лінійно масштабується з розміром даних, рано чи пізно призведе до Pager-а.
Міні-історія 3: Нудна, але правильна практика, що врятувала день (стандартизовані UID і контракт прав)
Велика команда платформи мала правило: кожен stateful контейнер працює під виділеним сервісним UID, з централізовано керованою алокацією UID/GID. Розробники нарікали, що це бюрократія. Вони хотіли «просто використовувати 1000». Команда платформи ввічливо відмовляла, знову й знову.
Місяць потому сталася проблема: потрібно було швидко перебудувати вузол, і сервіс пересів на свіжу машину. Сервіс піднявся, приєднав персистентний том і почав писати одразу. Ніяких помилок прав, ніякого ручного chown, ніякої паніки.
Чому? Відображення UID/GID було послідовним по флоту. Власність тому відповідала сервісному акаунту скрізь. Runtime-користувач контейнера був зафіксований. Дерево директорій мало setgid і default ACL там, де потрібен був спільний доступ. Нічого хитрого, нічого захопливого. Просто контракт.
На пост-інцидентному огляді хтось спитав, чи це був випадок. Відповідь SRE-ліда була по суті: удача — для лотерей; у нас є runbook-и.
У тієї команди все одно були аутейджі. У всіх вони бувають. Але у них не було цього класу аутейджів — і це вже велика вигода від кількох політичних рішень на ранніх стадіях.
Поширені помилки: симптом → коренева причина → виправлення
1) Симптом: працює в контейнері без тому, не працює з bind-монтом
Коренева причина: шлях на хості має власність/біти режиму, що не дозволяють записувати контейнерному UID.
Виправлення: chown/chgrp директорії на хості під контейнерний UID/GID, або використати спільну групу + setgid + group_add, або застосувати ACL.
2) Симптом: працює як root, не працює як non-root
Коренева причина: образ спроектований під root, або хост-директорія належить root і не записувана для очікуваного UID.
Виправлення: оберіть відомий UID/GID і встановіть власність відповідно; віддавайте перевагу образам з підтримкою non-root; уникайте «sudo всередині контейнера» хаків.
3) Симптом: права виглядають коректними, але все одно «Permission denied» на Fedora/RHEL
Коренева причина: невідповідність SELinux-мітки для bind-монту.
Виправлення: монтуйте з :Z/:z або перемаркуйте шлях хоста; підтвердіть через ls -Z і журнали аудиту.
4) Симптом: не можу писати в NFS навіть як root на хості
Коренева причина: NFS root squash або серверні ACL забороняють.
Виправлення: виправте серверні правила експорту/ACL; використовуйте виділений сервісний UID, послідовний на клієнтах; не намагайтесь «chmod» пройти через політику NFS.
5) Симптом: переривчасті збої після деплою; інколи фіксується рестартом
Коренева причина: гонка між init-скриптами і стартом додатка, або кілька контейнерів ініціалізують той самий том по-різному.
Виправлення: ізолюйте ініціалізацію (одноразова робота), забезпечте детерміновану власність і запобігайте одночасній ініціалізації в кількох репліках.
6) Симптом: директорія тому записувана, але нові файли мають неправильну групу
Коренева причина: відсутній setgid на директоріях або відсутній default ACL; umask надто обмежує.
Виправлення: встановіть setgid на спільних директоріях; застосуйте default ACL; перевірте umask і поведінку додатка.
7) Симптом: зміни прав на хості не змінюють те, що бачить контейнер
Коренева причина: ви не редагуєте реальний бекап-путь (іменований том vs bind-монт), або ви на SMB/NFS з серверною політикою.
Виправлення: docker inspect джерело монтування; для іменованих томів використайте docker volume inspect; для мережевого сховища змінюйте правила на сервері.
8) Симптом: після увімкнення userns-remap все «стало» UID 165536
Коренева причина: відображення user namespace працює; ваші інструменти та очікування не готові.
Виправлення: плануйте діапазони відображення UID; забезпечте, щоб хост-шляхи і автоматика розуміли відображену власність; уникайте bind-монтів людських директорій у remapped-контейнерах.
Чеклисти / покроковий план
Чеклист A: зупиніть кровотечу під час інциденту (10–15 хвилин)
- Доведіть, що це права:
touchу змонтованій директорії зсередини контейнера. Якщо не вдається — продовжуйте; якщо вдається — проблема в додатку. - Отримайте ефективний UID/GID:
idв контейнері, а не з Dockerfile. - Підтвердіть тип монтування:
docker inspect→ bind чи іменований том? - Перевірте власність/режим/ACL на хості:
ls -ldnіgetfaclна джерельному шляху. - Перевірте SELinux:
getenforceіls -Zна шляху хоста. - Перевірте мережеве сховище:
mountна хості; якщо NFS/CIFS — підозрівайте серверні правила. - Оберіть найменш ризикове виправлення: віддавайте перевагу груповому запису або таргетованому ACL; уникайте 777; уникайте рекурсивного chown на великих деревах.
Чеклист B: Щоб більше не траплялось (контракт для продакшену)
- Стандартизуйте сервісні UID/GID: виділяйте фіксовані числові ідентифікатори для кожного stateful сервісу у всіх середовищах.
- Документуйте контракт тому: «Цей шлях має бути записуваним для UID X та/або GID Y, з setgid і default ACL.» Покладіть це в репозиторій.
- Використовуйте групу + setgid для спільних шляхів: це найпростіша масштабована модель для кількох записувачів.
- Обробляйте ініціалізацію явно: одноразова робота для встановлення власності/прав на нові томи.
- Визначте політику SELinux: забезпечте правильне маркування в Compose/Kubernetes маніфестах.
- Тестуйте префлайтом: CI або entrypoint-перевірка, що перевіряє
test -wна потрібних каталогах і аварійно завершує з зрозумілим повідомленням. - Тримайте людей поза шляхом даних: якщо операторам доводиться торкатися файлів, дайте їм груповий доступ; не «sudo edit» файли так, щоб непередбачувано змінювалась власність.
Чеклист C: Якщо мусите використовувати мережеве сховище
- NFS: забезпечте послідовність UID/GID між вузлами; навмисно вирішіть питання root squash; керуйте серверними дозволами.
- CIFS: будьте явні з опціями монтування; розумійте, чи права застосовуються на сервері; не прикидайтеся, що біти режиму реальні, якщо вони ні.
- Латентність і витрати на метадані: уникайте рекурсивних змін прав на величезних деревах; плануйте поведінку старту відповідно.
FAQ
1) Чому ім’я користувача в контейнері не має значення для прав тому?
Бо ядро застосовує права за числовими UID/GID. Імена — це записи в /etc/passwd (або NSS). Хост бачить числа.
2) Чи варто запускати контейнери як root, щоб уникнути проблем з правами?
Ні. Це переносить ризик з «permission denied» в «компрометація хоста» і все одно не працює з NFS root squash та SELinux-політикою. Вирішіть відображення ідентичностей замість цього.
3) Який найкращий дефолт для спільних записуваних директорій?
Створіть спільний GID, змініть групову власність директорії, встановіть g+rwX, встановіть setgid і забезпечте приєднання контейнерів до цього GID. Додайте default ACL при необхідності.
4) Чи коли-небудь прийнятний chmod 777?
Як короткочасна діагностика, можливо. Як виправлення — це недбало і часто непотрібно. Використовуйте груповий запис або ACL.
5) Чому це відбувається лише на одному хості?
Зазвичай через невідповідність UID/GID (різна алокація користувачів), різний SELinux-режим/мітки або тому, що «той самий шлях» насправді вказує на різне сховище (локально проти NFS).
6) У чому різниця між іменованими томами і bind-монтами для прав?
Bind-монти використовують існуючий шлях на хості (ваша відповідальність). Іменовані томи керуються піддиректорією Docker і часто спочатку належать root на хості, якщо їх не ініціалізувати.
7) Як безпечно виправити права для іменованого тому?
Запустіть одноразовий init-контейнер (або тимчасовий docker run) як root, щоб встановити власність на том, а потім запускайте основний сервіс як non-root.
8) Чому я бачу UID 165536 (або подібний) на файлах хоста?
Ймовірно, ви увімкнули user namespace remapping або rootless режим. UID контейнера відображається у підпорядкований діапазон UID на хості. Це очікувана поведінка.
9) Чому права виглядають правильними, але записи все одно відмовляються?
SELinux/AppArmor можуть відмовити в доступі навіть якщо POSIX-права дозволяють. На системах з SELinux мітки — часта причина для bind-монтів.
10) Чи можна «виправити» це, додавши користувачів у /etc/passwd всередині контейнера?
Додавання імені допомагає для логів і інструментів, але не змінює числовий UID. Виправлення — це: узгодити UID, використовувати групи/ACL або налаштувати відображення.
Висновок: наступні кроки, які ви можете зробити сьогодні
Проблеми з правами Docker-томів передбачувані. Це хороша новина. Вони трапляються, коли числові ідентичності та політики не узгоджені. Ваше завдання — не «пробувати chmod, поки не запрацює». Ваше завдання — визначити контракт прав і послідовно його дотримуватися.
Зробіть наступне:
- Оберіть стратегію UID/GID для кожного сервісу: збіг UID для простих випадків, спільна група + setgid для спільного доступу, ACL для змішаних записувачів.
- Приберіть рекурсивний chown зі звичайного старту: замініть його одноразовим init-кроком, прив’язаним до створення тому.
- Зробіть діагностику швидкою: вбудуйте префлайт-перевірки (
id,test -w) і задокументуйте очікувані UID/GID в репозиторії. - Свідомо обробляйте SELinux і мережеве сховище: мітки і серверна політика — не «крайні випадки». Це продакшен.
Коли ви включите UID/GID у специфікацію деплойменту, «Permission denied» перестане бути таємницею і стане тестом, який ви забули написати.