Docker на хостах з SELinux/AppArmor: помилки дозволів, які ніхто не пояснює

Було корисно?

Ви виконали ритуал: chmod -R 777, можливо ще й chown для порядку, перезапустили контейнер,
а він усе одно каже «permission denied». Шлях на хості записуваний. UID співпадає. Файлова система — не тільки‑для‑читання.
І все одно ваш контейнер поводиться так, ніби його не впускають у власний дім.

Тут інженери починають звинувачувати Docker, потім Linux, потім «безпеку», потім одне одного. Правда нудна: вас блокує
система обов’язкового контролю доступу (SELinux або AppArmor), і вона робить саме те, для чого створена. Повідомлення про помилку
просто не вважає за потрібне це пояснювати.

Чому «permission denied» переживає chmod і chown

Unix‑права (власник/група/режим) — це дискреційний контроль доступу. Вони локальні й змінювані. Якщо ви власник
файлу, ви можете надати доступ. Якщо ви root, зазвичай можна пройти через усе. Контейнери додають простори і можливості,
але в підсумку, коли процес намагається відкрити файл, вирішує ядро.

SELinux і AppArmor — це обов’язкові системи контролю доступу (MAC). Вони — не «файлові права». Це додатковий шар політики,
який може заборонити доступ, навіть коли біт‑режими дозволяють. Ось ключ: ядро може повернути EACCES
з причин, які не мають нічого спільного з ls -l.

Ось типовий патерн:

  • Класична проблема з правами: невірний UID/GID, неправильний режим, невірні ACL. Виправляється через chown/chmod/setfacl.
  • Проблема SELinux: мітки (контексти) не дозволяють домену контейнера торкатися цього шляху. Виправлення — маркування, boolean‑параметри або політика.
  • Проблема AppArmor: профіль контейнера забороняє шлях, монтування, можливість або системний виклик. Виправлення — змінити профіль або переключити його.

Ви впізнаєте проблеми MAC за тим, що ваш контейнер поводиться як чемний злодій: у нього є ключі (UID/режим),
але сигналізація все одно викликає поліцію (SELinux/AppArmor).

Жарт №1: Якби chmod 777 усе вирішував, безпека була б одним Bash‑аліасом і ми всі були б безробітні.

SELinux vs AppArmor: різні релігії, ті самі вигнання

SELinux в одному прагматичному абзаці

SELinux працює на основі міток. Усьому присвоєно контекст (user:role:type:level). Політика вирішує, які «домени»
(типи процесів) можуть звертатись до яких «типів» (міток об’єктів) з якими дозволами. Контейнери зазвичай працюють
у обмеженому домені, як‑от container_t, і файли на хості повинні мати такі мітки, щоб цьому домену було дозволено доступ.
Якщо ви підтягуєте випадкові шляхи хоста в контейнер без правильних міток, SELinux заблокує доступ, навіть коли файлова система каже «будь ласка».

AppArmor в одному прагматичному абзаці

AppArmor працює на основі шляхів. Профілі визначають, до яких шляхів процес може читати/писати/виконувати, а також які можливості й
інтерфейси ядра він може використовувати. Docker часто застосовує профіль за замовчуванням (зазвичай docker-default), якщо
ви не перевизначите його. Якщо вашому контейнеру потрібно змонтувати щось, отримати доступ до /sys специфічним способом або торкнутися
шляху хоста, не передбаченого профілем, AppArmor може це заборонити.

Чому ви відчуваєте плутанину

Вони дають збій однаково на рівні застосунку. Ваш додаток бачить «permission denied». У логах стек‑трейс. Команда свариться про власність файлів.
Тим часом реальна відмова в ядрі фіксується в аудиторських логах, а ваш контейнер не отримує більш дружнього повідомлення, бо шар VFS не проводить психотерапію.

Ваше завдання — з’ясувати, який механізм політики працює, потім читати відповідні логи і зробити найменшу безпечну зміну. Решта посібника — саме цей шлях, без духовних метафор.

Цікаві факти та коротка історія (щоб дивні речі мали сенс)

  • SELinux починався як науково‑дослідна робота (спочатку з NSA) і був створений для примусу політики навіть проти root. Тому «але я root» його не вражає.
  • AppArmor народився як SubDomain і став AppArmor після поглинання та ребрендингу; він набув популярності, бо з ним легше працювати, коли думати в термінах шляхів файлів.
  • Docker не винайшов обмеження; він успадкував те, що вже було в ядрі: namespaces, cgroups, LSM хуки, seccomp. Docker здебільшого їх зв’язує і отримує за це докори.
  • Маркування контейнерів у SELinux суттєво еволюціонувало з появою контейнерів; ранні підходи були грубіші, а сучасні дистрибутиви дають кращі значення за замовчуванням для container_t та споріднених типів.
  • Параметри монтування :z і :Z існують, бо bind‑mounts порушують припущення про «мітки відповідають наміру»; ці прапори перемаркують вміст так, щоб контейнер міг його використовувати.
  • Аудит‑логи — це не «debug» логи; це події безпеки. Коли команди централізують їх коректно, проблеми SELinux перестають бути таємницею і стають рутинними.
  • Шляхова модель AppArmor може бути обдурена іграми з перейменуваннями/линками, якщо профілі не дуже ретельні; модель міток SELinux уникає частини цього, але додає оперційну вагу маркування.
  • Overlay‑файлові системи змінили історію збереження контейнерів; SELinux мав навчитися правильно маркувати шари overlay, і неправильні мітки можуть створювати помилки, що виглядають як баги Docker.

Швидкий план діагностики (що перевіряти спочатку/друге/третє)

Перше: визначте, яка система MAC активна (і для Docker що має значення)

  • Якщо SELinux у режимі enforcing і Docker це використовує — починайте з AVC‑відмов.
  • Якщо AppArmor увімкнено і контейнер працює під профілем — починайте з AppArmor‑відмов.
  • Якщо нічого з цього не активно — повертайтеся до класичних прав і user namespaces.

Друге: підтвердіть операцію, що зазнає невдачі, і точний шлях/пристрій на хості

  • Чи це шлях bind mount? Іменований том? Сокет (наприклад Docker socket)? Нода пристрою?
  • Відмова при читанні, записі, виконанні, створенні, перемаркуванні, монтуванні чи ще якось?
  • Відбувається помилка тільки на одному хості? Якщо так, підозрюйте відмінності політик, а не ваш YAML.

Третє: дістаньте докази з ядра/аудиту, а не робіть здогадки

  • SELinux: шукайте type=AVC в audit логах, потім інтерпретуйте контексти й запитувані дозволи.
  • AppArmor: шукайте apparmor="DENIED" і імя профілю, потім зіставте з шляхами/можливостями.

Четверте: оберіть найменше зміну, що зберігає обмеження

  • SELinux: використовуйте правильні мітки (:z/:Z, chcon або semanage fcontext), уникайте відключення SELinux.
  • AppArmor: підправте або створіть профіль, або переключіться на інший профіль тільки коли розумієте зону ураження.

П’яте: перевірте повторювано та лишіть «хлібні крихти»

  • Відтворіть проблему мінімальним контейнером з одним монтуванням.
  • Документуйте очікування щодо міток/профілів у Compose/Kubernetes маніфестах і в провізії хоста.
  • Зробіть так, щоб це пережило перезавантаження і перемаркування.

Практичні завдання: команди, виводи, та рішення (12+)

Це команди, до яких я реально звертаюся, коли контейнер не може читати/писати монтування або його блокує при виконанні
очевидно дозволених дій. Кожне завдання включає: команду, приклад виводу, що це означає і рішення.

Завдання 1: SELinux увімкнено та в режимі enforcing?

cr0x@server:~$ getenforce
Enforcing

Значення: Політика SELinux активно забороняє недозволені дії (не тільки логування).
Рішення: Розглядайте «permission denied» як потенційно SELinux‑проблему, поки не доведете протилежне. Ідіть шукати AVC.

Завдання 2: Швидка перевірка стану SELinux

cr0x@server:~$ sestatus
SELinux status:                 enabled
SELinuxfs mount:                /sys/fs/selinux
SELinux root directory:         /etc/selinux
Loaded policy name:             targeted
Current mode:                   enforcing
Mode from config file:          enforcing
Policy MLS status:              enabled
Policy deny_unknown status:     allowed
Max kernel policy version:      33

Значення: SELinux працює в targeted режимі (поширено на Fedora/RHEL‑похідних).
Рішення: Переконайтеся, що пакети політик для Docker/контейнерів встановлені і маркування хоста коректне.

Завдання 3: AppArmor увімкнено?

cr0x@server:~$ aa-status
apparmor module is loaded.
24 profiles are loaded.
22 profiles are in enforce mode.
2 profiles are in complain mode.
0 processes are unconfined but have a profile defined.

Значення: AppArmor активно застосовує профілі.
Рішення: Якщо хост на Ubuntu/Debian‑подібному, AppArmor — один із головних підозрюваних. Далі: перевірте профіль контейнера.

Завдання 4: Який AppArmor профіль Docker застосував до контейнера

cr0x@server:~$ docker inspect --format '{{.Name}} -> AppArmor={{.AppArmorProfile}}' web-1
/web-1 -> AppArmor=docker-default

Значення: Контейнер обмежений профілем docker-default.
Рішення: Якщо бачите AppArmor‑відмови, доведеться редагувати/замінювати цей профіль або перевизначити його на рівні контейнера.

Завдання 5: Підтвердіть точне монтування і де Docker думає, що воно є

cr0x@server:~$ docker inspect --format '{{range .Mounts}}{{.Type}} {{.Source}} -> {{.Destination}}{{"\n"}}{{end}}' web-1
bind /srv/web/data -> /var/lib/app/data
volume web-cache -> /var/cache/app

Значення: У вас і bind mount, і іменований том. SELinux/AppArmor поводяться по‑різному для них.
Рішення: Зосередьте увагу на bind mount спочатку; саме там зазвичай кусає маркування/шляхова політика.

Завдання 6: Відтворіть мінімальний тест на запис всередині контейнера

cr0x@server:~$ docker exec -it web-1 sh -lc 'id; touch /var/lib/app/data/.probe && echo ok'
uid=1000(app) gid=1000(app) groups=1000(app)
touch: cannot touch '/var/lib/app/data/.probe': Permission denied

Значення: Користувач додатка не може створити файл у замонтованому каталозі.
Рішення: Якщо права на хості в порядку, переходьте одразу до доказів MAC (AVC/AppArmor).

Завдання 7: В будь‑якому разі перевірте класичні права на хості (бо люди)

cr0x@server:~$ ls -ldn /srv/web/data
drwxrwx--- 5 1000 1000 4096 Jan  3 10:12 /srv/web/data

Значення: Режим і власник збігаються з користувачем у контейнері (UID/GID 1000).
Рішення: Перестаньте сперечатися про chown. Це майже точно SELinux/AppArmor (або userns/shiftfs, але почніть з MAC).

Завдання 8: На SELinux‑хостах перегляньте контекст каталогу на хості

cr0x@server:~$ ls -ldZ /srv/web/data
drwxrwx---. 5 1000 1000 unconfined_u:object_r:default_t:s0 /srv/web/data

Значення: Каталог промаркований як default_t — «не знаю, що це за мітка». Контейнери зазвичай не можуть її торкати.
Рішення: Перемаркуйте каталог для доступу контейнера (переважно стійко). Не вимикайте SELinux.

Завдання 9: Знайдіть відмову SELinux в audit логах (курячий дим)

cr0x@server:~$ sudo ausearch -m avc -ts recent | tail -n 5
type=AVC msg=audit(1704286512.911:412): avc:  denied  { create } for  pid=23841 comm="touch" name=".probe" scontext=system_u:system_r:container_t:s0:c123,c456 tcontext=unconfined_u:object_r:default_t:s0 tclass=file permissive=0

Значення: Домен процесу — container_t; ціль — default_t; заборонене право — create.
Рішення: Виправляйте маркування /srv/web/data (контекст цілі), а не UID контейнера.

Завдання 10: Швидке тимчасове перемаркування через Docker прапори монтування (:z vs :Z)

cr0x@server:~$ docker run --rm -v /srv/web/data:/data:Z alpine sh -lc 'touch /data/ok && ls -l /data/ok'
-rw-r--r--    1 root     root             0 Jan  3 10:21 /data/ok

Значення: Перемаркування спрацювало; контейнер тепер може записувати.
Рішення: Вирішіть між :Z (приватна мітка для одного контейнера) і :z (спільна мітка) залежно від того, чи потрібно декільком контейнерам доступ до одного шляху.

Завдання 11: Зробіть SELinux‑маркування стійким через semanage fcontext

cr0x@server:~$ sudo semanage fcontext -a -t container_file_t "/srv/web/data(/.*)?"
cr0x@server:~$ sudo restorecon -Rv /srv/web/data
restorecon reset /srv/web/data context unconfined_u:object_r:default_t:s0->unconfined_u:object_r:container_file_t:s0
restorecon reset /srv/web/data/ok context unconfined_u:object_r:default_t:s0->unconfined_u:object_r:container_file_t:s0

Значення: Ви оголосили очікуване відображення міток і застосували його. Воно переживе операції перемаркування.
Рішення: Віддавайте перевагу цьому на керованих серверах. chcon — тільки для швидких тестів.

Завдання 12: Підтвердіть нову SELinux‑мітку

cr0x@server:~$ ls -ldZ /srv/web/data
drwxrwx---. 5 1000 1000 unconfined_u:object_r:container_file_t:s0 /srv/web/data

Значення: Каталог тепер промаркований так, щоб бути доступним контейнерам.
Рішення: Перевірте запис з контейнера. Якщо ще відмовляє — дивіться інші відмови: може підшлях, сокет або інший клас об’єкта.

Завдання 13: На AppArmor‑хостах знайдіть відмову в логах ядра

cr0x@server:~$ sudo journalctl -k -g 'apparmor="DENIED"' -n 5
Jan 03 10:24:11 server kernel: audit: type=1400 audit(1704287051.112:96): apparmor="DENIED" operation="open" profile="docker-default" name="/srv/web/data/.probe" pid=24910 comm="touch" requested_mask="wc" denied_mask="wc" fsuid=1000 ouid=1000

Значення: Профіль AppArmor docker-default заборонив запис/створення (wc) у шлях хоста.
Рішення: Або змінити профіль, щоб дозволити цей шлях, або обрати інший підхід (іменований том, інший шлях або перевизначення профілю).

Завдання 14: Визначте мітку init‑процесу контейнера (SELinux) або профіль (AppArmor) зсередини

cr0x@server:~$ docker exec -it web-1 sh -lc 'cat /proc/1/attr/current 2>/dev/null || true; cat /proc/1/attr/apparmor/current 2>/dev/null || true'
system_u:system_r:container_t:s0:c123,c456
docker-default (enforce)

Значення: Ви бачите метадані обмеження від ядра. (На деяких хостах будете бачити одне або інше.)
Рішення: Якщо SELinux показує container_t — фокусуйтеся на мітках. Якщо AppArmor показує профіль — працюйте з правилами профілю.

Завдання 15: Перевірте опції безпеки демона Docker (SELinux/AppArmor/seccomp)

cr0x@server:~$ docker info --format '{{json .SecurityOptions}}'
["name=seccomp,profile=builtin","name=selinux","name=apparmor"]

Значення: Docker знає про SELinux і AppArmor; обидві системи присутні на цьому хості.
Рішення: Не припускайте «ми — AppArmor‑шоп» або «ми — SELinux‑шоп». У вашому кластері може бути змішана конфігурація.

Завдання 16: Перевірте, чи іменований том уникає проблеми bind mount

cr0x@server:~$ docker run --rm -v web-cache:/cache alpine sh -lc 'touch /cache/ok && ls -l /cache/ok'
-rw-r--r--    1 root     root             0 Jan  3 10:28 /cache/ok

Значення: Іменований том працює, бо Docker розміщає його в директорії з правильними мітками/дозволами для контейнерів.
Рішення: Віддавайте перевагу іменованим томам, якщо вам не потрібні семантика хост‑шляху (резервні копії, аудит, спільні хости інструментів).

Завдання 17: Перевірте опції монтування і тип файлової системи (NFS і подібні можуть ускладнювати)

cr0x@server:~$ findmnt -T /srv/web/data
TARGET SOURCE              FSTYPE OPTIONS
/      /dev/mapper/rootvg ext4   rw,relatime,seclabel

Значення: Файлова система підтримує SELinux‑мітки (seclabel). Добре.
Рішення: Якщо ви не бачите seclabel (або ви на NFS/CIFS), плануйте спеціальну обробку і ретельно тестуйте.

Завдання 18: Для дивностей overlay2 перевірте мітку merged‑каталогу контейнера (на SELinux‑хостах)

cr0x@server:~$ cid=$(docker inspect -f '{{.Id}}' web-1)
cr0x@server:~$ sudo ls -ldZ /var/lib/docker/overlay2/*/merged 2>/dev/null | head -n 2
drwxr-xr-x. 1 root root system_u:object_r:container_file_t:s0:c87,c912 4096 Jan  3 10:30 /var/lib/docker/overlay2/3a3d.../merged
drwxr-xr-x. 1 root root system_u:object_r:container_file_t:s0:c21,c333 4096 Jan  3 10:30 /var/lib/docker/overlay2/9b71.../merged

Значення: Merged‑директорії overlay мають мітки контейнера з MCS‑категоріями. Це очікувано на SELinux‑хостах.
Рішення: Якщо ці мітки неправильні або відсутні — ви у полі «несумісність демона/драйвера зберігання/політики», а не «chmod це».

Bind mounts, томи та маркування: справжні механіки

Чому bind mounts — це місце, куди йдуть добрі наміри, щоб загинути

Іменований Docker‑том створюється в керованих Docker директоріях з правильним контекстом безпеки (SELinux) або з шляхами, які вже дозволені
(AppArmor), залежно від налаштувань дистро. Bind mount — це сирий шлях хоста, який ви обрали. Ядру все одно, що ви обрали його «для контейнера».
Це просто шлях на хості з тими мітками/політиками, що вже існують.

Це центральна оперативна відмінність: томи зазвичай «просто працюють», а bind mounts часто падають у заплутаний спосіб,
бо вони перетинають домени безпеки.

SELinux: мітка — це дозвіл, а не режим

У SELinux процес контейнера працює в домені типу container_t плюс набір MCS‑категорій. Файли під /var/lib/docker
(або корінь зберігання контейнерів) промарковані так, щоб відповідати цьому домену і часто містять відповідні категорії. Ваш випадковий шлях
на хості під /srv може бути промаркований як default_t або var_t чи якимось іншим типом. Політика може заборонити доступ контейнера.

Виправлення — не «зробіть світ записуваним». Виправлення — «застосуйте SELinux‑тип, який домен контейнера може використовувати».
Поширені підходи:

  • Використовуйте Docker‑прапори монтування: :z для спільного вмісту, :Z для приватного вмісту.
  • Задайте постійні мітки: semanage fcontext + restorecon, використовуючи container_file_t.
  • Використовуйте інструменти container‑selinux (залежать від дистро) щоб тримати політику в узгодженості з поведінкою Docker.

Що насправді роблять :z і :Z (і чому людей це дивує)

Параметри :z/:Z наказують Docker перемаркувати вихідний шлях так, щоб контейнер міг його використовувати.
:Z зазвичай дає вмісту приватну мітку (включно з категоріями) для одного контейнера. :z
робить мітку придатною для спільного доступу між контейнерами. Якщо ви повісите :Z на шлях, який використовують кілька контейнерів,
один із них може раптово втратити доступ. Це не примха Docker; це ви просите приватні мітки для спільного вмісту.

Ще одна гостра деталь: перемаркування каталогу на хості може мати побічні ефекти для процесів, що не працюють у контейнерах.
SELinux‑мітки — це істина для всього системного простору, а не примха окремого контейнера. Якщо каталог використовує інший сервіс з жорсткими
очікуваннями, ви можете його поламати.

AppArmor: шляхи і можливості — поле бою

Відмови AppArmor зазвичай виглядають так:

  • Не вдається записати в змонтований шлях (профіль не дозволяє).
  • Не можна виконати mount всередині контейнера (відмовлено в можливості або правилі на mount).
  • Не можна отримати доступ до /proc або /sys, як цього очікує додаток.
  • Не можна використовувати привілейовані операції навіть коли ви root у контейнері (фільтрація capabilities).

Виправлення зазвичай — підправити профіль (дозволити конкретний шлях) або запускати з іншим профілем. Ліниве виправлення —
«unconfined», що іноді прийнятно для контрольованого вікна діагностики, але майже ніколи не прийнятно як постійний стан у продакшені.

Примітка щодо NFS, CIFS, FUSE та інших мережевих ускладнень

Мережеві файлові системи ускладнюють справу:

  • Деякі монтування не підтримують SELinux‑мітки як локальні файлові системи, або їм потрібні явні опції монтування.
  • Root‑squash на NFS може перетворити «root контейнера» на «nobody», і тоді ви гонитиметеся за невірним винуватцем.
  • AppArmor все одно може забороняти шляхи незалежно від файлової системи під ним.

Коли контейнери не можуть писати в NFS — перевіряйте і MAC, і правила експорту NFS. Рідко це лише одна річ.

Профілі AppArmor: що робить Docker, що робить дистро

docker-default — компроміс, а не обіцянка

Профіль AppArmor за замовчуванням від Docker покликаний блокувати деякі очевидно небезпечні речі, водночас дозволяючи більшості контейнерів
працювати. Він не підлаштований під ваш додаток, ваші монтування або режим комплаєнсу. Це загальний пасок безпеки.
Корисний, але не індивідуальний.

Коли ви зіштовхуєтесь з AppArmor‑відмовою, у вас є вибір:

  • Змінити робоче навантаження: надавайте перевагу іменованим томам; уникайте дивних монтувань; зменшіть привілейовану поведінку.
  • Змінити профіль: дозволити конкретні шляхи та операції, які потрібні.
  • Змінити конфігурацію контейнера: перевизначити профіль для цього контейнера.

Діагностика AppArmor: що каже відмова

Рядок відмови зазвичай містить:

  • profile= який профіль заблокував
  • operation= open/mount/ptrace/і т.д.
  • name= шлях
  • requested_mask / denied_mask який тип доступу намагалися отримати

Ставтеся до цього як до сфокусованої головоломки. Якщо відмовляє запис у конкретний шлях хоста — або не монтуйте його, або явно дозвольте його.
Якщо відмовляє mount/possibilities, подумайте, чи дійсно це потрібно. Більшість додатків цього не потребує.

Перевизначення AppArmor: використовуйте його як скальпель

Docker підтримує перевизначення AppArmor на рівні контейнера через security opts. Це може врятувати, якщо у вас один‑разовий образ від постачальника,
що потребує певної можливості, але саме так люди випадково запускають продакшн без MAC.

Операційне правило: якщо ви перевизначаєте профілі, зафіксуйте це рішення в інфраструктурному коді і внесіть у модель загроз. Не дозволяйте цьому стати племінною
тимчасовою хитрістю, яка живе в якихось забутих нотатках.

Жарт №2: AppArmor як корпоративна політика відряджень — поїздка «схвалена», допоки ви не спробуєте відшкодувати таксі.

Три корпоративні міні‑історії з полів

Інцидент 1: неправильне припущення («права — це і є права»)

Середня компанія перенесла низку сервісів з ВМ у контейнери. Вони дбали про відображення UID/GID і навіть стандартизували запуск додатків не‑root.
Все виглядало чисто. Потім один сервіс почав падати тільки на нових RHEL‑хостах, тоді як на старій Ubuntu‑флоті працював нормально.

On‑call зробив звичне: перевірив власника, виконав chmod, redeploy. Сервіс усе одно не міг писати в каталог даних. Після двох годин хтось промовив
«можливо SELinux», на що послідувала та тиша, яку ви отримуєте, коли кажете «можливо це DNS».

Вони знайшли AVC‑відмови: bind‑монтований каталог мав мітку default_t. Домен контейнера був container_t. SELinux робив свою роботу.
Неправильне припущення було в тому, що файлові права — це вся історія, тому вони весь час крутили не той регулятор.

Виправлення було простим: визначити стійкі правила fcontext для хост‑шляхів додатка і застосувати їх через restorecon. Справжнє покращення було процедурним:
команда додала крок в провізію хостів, який перевіряє коректність міток для відомих bind‑mounts.

Пост‑інцидентний пункт дій був прямолінійним: «Припиніть використовувати chmod як стратегію налагодження». Вони надрукували це на наліпці і приклеїли на ноутбук.
Це було помірно ефективно, але мораль покращилась.

Інцидент 2: оптимізація, що відбилася боком (спільні bind mounts + :Z)

Інша організація запускала кластер контейнерів, що ділили хост‑каталог для згенерованих артефактів. Думайте про мініатюри, зібрані фронтенд‑бандли тощо.
Вони хотіли швидші релізи і менше дублювання, тому монтували один і той самий хост‑шлях в кілька контейнерів по вузлу і назвали це «ефективно».

Інженер з безпеки помітив періодичні SELinux‑відмови і вирішив «виправити це правильно», додавши :Z до монтування в Compose. У девелопі працювало:
один контейнер, один каталог — без проблем. Вони розгорнули в продакшн.

В продакшні стало дивно. Половина контейнерів могла писати, половина — ні, і які саме — змінювалося після рестартів.
Іноді деплой «вирішував» проблему, коли контейнер опинявся на іншому вузлі. Команда витратила день, підозрюючи бекенд сховища, потім Docker, потім додаток.

Корінь проблеми: :Z застосовує приватну мітку, призначену для категорій MCS одного контейнера. Коли кілька контейнерів ділять шлях, приватна мітка
відповідатиме категоріям лише одного контейнера, а інші не зможуть доступатися. Це недетерміновано при рестарті, бо категорії можуть різнитись.

Вони перейшли на :z для по-справжньому спільного вмісту, а в окремих випадках перемістилися на іменовані томи, щоб уникнути зв’язки з хост‑шляхом.
«Оптимізація» не лише відбилася боком; вона створила режим відмов, що виглядав як нестабільний I/O і викинув багато часу старших інженерів.
Урок простий: у SELinux‑країні спільний доступ — це політичне рішення, а не зручність монтування.

Історія 3: нудна, але правильна практика, що врятувала вечір (аудит‑логи + runbook)

Велика компанія мала змішаний флот: деякі хости з SELinux enforcing, деякі з AppArmor, кілька жорстко захищених машин з обома і постійний потік вендор‑образів,
що думали, ніби можуть робити що завгодно.

Вони інвестували в глибоко неефектну практику: централізували аудит‑логи і навчили інженерів їх читати. Це не був пункт у чек‑листі комплаєнсу — це була
операційна здатність. У них був runbook: «permission denied у контейнері» → «перевірити Docker mounts, потім SELinux/AppArmor‑відмови, потім UID/GID».
Усі знали ті самі кроки.

Одного вечора сервіс, пов’язаний зі сховищем, упав після рутинної хвилі патчів хостів. Логи додатка нічого корисного не показали.
Логи контейнера показали «permission denied» на шляху даних. On‑call слідував runbook, знайшов AVC‑відмови і побачив, що цільовий шлях був промаркований некоректно після міграції FS.

Оскільки вони мали стійкі правила fcontext у контрольованому конфігу, виправлення було restorecon і відкат одного невірного монтування.
Сервіс відновлено швидко. Ніяких героїв. Ніякого «тимчасового встановлення SELinux у permissive». Просто система поводилась передбачувано.

Врятувала ситуацію не геніальність. Врятувала відтворюваність. Команда не «пам’ятала» SELinux; їхні інструменти і runbook пам’ятали це за них.

Поширені помилки: симптом → корінь → виправлення

1) Симптом: контейнер не може писати в bind mount, але права на хості правильні

Корінь: SELinux контекст на каталозі хоста не доступний для контейнеру (часто default_t).

Виправлення: Використайте :z/:Z при монтуванні, або встановіть стійкі мітки через
semanage fcontext -a -t container_file_t та restorecon.

2) Симптом: працює на Ubuntu‑хості, падає на RHEL/Fedora‑хості

Корінь: SELinux enforcement на одній групі хостів, AppArmor (або нічого) на іншій. Ваші маніфести припускають неправильний базис.

Виправлення: Виявляйте і кодуйте конфігурацію безпеки хоста; додавайте SELinux‑маркування для bind‑mounts на SELinux‑хостах.

3) Симптом: тільки один з кількох контейнерів може писати у спільний каталог

Корінь: Використання :Z (приватна мітка) для спільного вмісту; MCS‑категорії не співпадають між контейнерами.

Виправлення: Використайте :z для спільних монтувань, або припиніть шарити і використайте окремі шляхи/томи.

4) Симптом: «permission denied» при доступі до UNIX socket (наприклад /var/run/…)

Корінь: SELinux‑тип сокета забороняє домену контейнера, або профіль AppArmor блокує шлях.

Виправлення: Уникайте монтування привілейованих хост‑сокетів коли можливо; інакше міткуйте/дозвольте шлях сокета спеціально. Розгляньте проксі‑сервіс замість прямого монтування сокета.

5) Симптом: контейнер падає при спробі змонтувати або використати FUSE

Корінь: AppArmor забороняє операції mount або потрібні можливості; SELinux може забороняти доступ до пристрою.

Виправлення: Переоцініть необхідність. Якщо потрібно, створіть налаштований AppArmor‑профіль і явно дозвольте потрібні операції; уникати повного unconfined.

6) Симптом: після перезавантаження або перемаркування проблема повертається

Корінь: Ви використовували chcon або тимчасове Docker‑перемаркування як ad‑hoc виправлення без стійких fcontext правил; або конфігурація провізії відтворює каталоги з дефолтними мітками.

Виправлення: Використовуйте semanage fcontext + restorecon, і забезпечте створення директорій через конфігураційне управління.

7) Симптом: ви поставили SELinux у permissive і все «запрацювало»

Корінь: Ви довели, що це SELinux, а потім зупинилися на найдорожчому рішенні.

Виправлення: Поверніть SELinux у enforcing і виправте мітки/політику. Permissive — для діагностики, не для постійного режиму.

8) Симптом: іменований том працює, bind mount не працює, однаковий шлях у контейнері

Корінь: Шлях, що управляється Docker, вже промаркований/дозволений; довільний bind mount — ні.

Виправлення: Віддавайте перевагу іменованим томам. Використовуйте bind mounts тільки коли потрібна інтеграція з хостом, і тоді маркуйте/дозвольте коректно.

Контрольні списки / покроковий план

Контрольний список A: коли контейнер не може писати в монтування

  1. Визначте тип монтування і шлях на хості: docker inspect mounts.
  2. Відтворіть за допомогою простого touch у контейнері, щоб ізолювати логіку додатка від прав.
  3. Швидко перевірте режим/власника на хості (ls -ldn), щоб виключити очевидний UID mismatch.
  4. Перевірте стан SELinux (getenforce) і мітку (ls -ldZ), якщо він у режимі enforcing.
  5. Шукайте AVC‑відмови (ausearch -m avc), що відповідають шляху і операції.
  6. Якщо AppArmor активний, шукайте в логах ядра apparmor="DENIED" і визначте профіль.
  7. Застосуйте найменше виправлення: SELinux‑маркування або зміни профілю AppArmor.
  8. Повторно протестуйте мінімальним probe на запис, потім із реальним додатком.
  9. Зробіть рішення стійким: semanage fcontext/restorecon або деплой профілю через CM.
  10. Запишіть очікування в маніфесті сервісу («цей bind mount вимагає container_file_t»).

Контрольний список B: хардінг без ламання всього

  1. Віддавайте перевагу іменованим томам для стану додатка, якщо не потрібен хост‑шлях.
  2. Стандартизуйте невелику множину коренів для bind mounts (/srv/containers/<app>) і маркуйте їх послідовно.
  3. Централізуйте аудит і логи ядра; ставте алерти на сплески AVC/AppArmor‑відмов.
  4. Тримайте SELinux у enforcing і AppArmor у enforcing; розглядайте винятки як зміни під контролем.
  5. Не копіюйте механічно --privileged. Якщо потрібна одна capability — додайте лише її.
  6. У CI запускайте «permission probe» контейнер проти очікуваних монтувань, щоб виявляти дрейф міток/профілів раніше.

Контрольний список C: день міграції (коли ви переміщаєте хости або сховище)

  1. До перенесення директорій даних збережіть контексти: ls -lZ на старому шляху.
  2. Після міграції застосуйте fcontext правила і restorecon на новому шляху.
  3. Перевірте мінімальним контейнером, що робить create/read/delete операції.
  4. Лише потім піднімайте продакшн‑трафік. Це не містика; це профілактика ночних сюрпризів.

Одне висловлювання, що добре прижилося в операціях: «Надія — не стратегія.» — генерал Gordon R. Sullivan

FAQ

1) Чому Docker показує «permission denied», а не «SELinux denied»?

Тому що ядро повертає загальну помилку доступу для системного виклику. Додаток і Docker бачать EACCES. Реальні деталі живуть
в SELinux/AppArmor логах, а не в повідомленні додатка.

2) Чи варто відключати SELinux або AppArmor, щоб контейнери працювали?

Ні, не як постійне рішення. Використовуйте permissive/complain режим коротко, щоб підтвердити підозру, а потім виправляйте мітки/правила профілів.
Вимикання MAC перетворює тонкі вектори втечі контейнера на гучні інциденти згодом.

3) У чому різниця між :z і :Z на монтуваннях Docker?

На SELinux‑хостах вони викликають перемаркування. :z призначено для спільного вмісту (декілька контейнерів).
:Z — для приватного вмісту (один контейнер). Використання :Z на спільних каталогах викликає збої між контейнерами.

4) Я використав chcon і це спрацювало. Чому потім зламалось?

chcon змінює мітки, але не робить їх стійкими під час операцій перемаркування або деяких провізійних проходів. Використовуйте semanage fcontext
для визначення правила, потім застосуйте його через restorecon.

5) Чому іменований Docker‑том працює, коли bind mount падає?

Іменовані томи живуть у директоріях, керованих Docker, і успадковують мітки/шляхи, які політики вже очікують для контейнерного використання.
Bind mount успадковує те, що вже має шлях хоста.

6) Чи можу я запускати контейнери unconfined під AppArmor?

Можете, але ставтеся до цього як до запуску без поручителів. Іноді це тимчасовий діагностичний крок або свідомий виняток для строго контрольованого хоста.
Це не має бути за замовчуванням.

7) Чому це з’явилось лише після патча хоста або оновлення ОС?

Пакети політик, профілі за замовчуванням і поведінка маркування можуть змінюватися з оновленнями. Також міграції сховища і відновлення директорій можуть скидати контексти до дефолтних.
Виправлення — закодувати мітки/профілі, а не покладатись на «що ОС робить сама».

8) Чи це та сама проблема, що й user namespace remapping або rootless Docker?

Інший шар. Userns/rootless впливає на мапінг UID/GID і межі можливостей. SELinux/AppArmor — це LSM‑шари політики. Можна мати обидві проблеми одночасно,
саме тому потрібно дивитись на докази (аудит‑логи), а не гадати.

9) Як обрати між зміною політики і зміною дизайну контейнера?

Якщо ви постійно прорізаєте діри в MAC політиці заради зручності — треба редизайнити. Віддавайте перевагу іменованим томам, уникайте хост‑сокетів,
зменшуйте привілеї. Якщо є легітимна інтеграційна потреба (наприклад спільний хост‑каталог для бекапів), зробіть точну зміну політики і документуйте її.

Висновок: практичні наступні кроки

Помилки дозволів, які ніхто не пояснює, рідко містять загадку. Вони просто погано повідомляються. SELinux і AppArmor сидять під рівнем вашого runtime
контейнерів і не ведуть переговори з вашим chmod. Вони примушують політику, а деталі — в audit‑трейлах і логах ядра.

Наступні кроки, які ви можете зробити цього тижня, без розв’язування священної війни:

  1. Навчіть ваш on‑call runbook перевіряти SELinux/AppArmor перед тим, як чіпати файлові режими.
  2. Стандартизуйте корені для bind mounts і застосуйте стійкі SELinux fcontext правила там, де це релевантно.
  3. Віддавайте перевагу іменованим томам, якщо не потрібна семантика хост‑шляху.
  4. Централізуйте аудити і логи ядра, щоб «permission denied» став двоххвилинною діагностикою, а не двогодинним суперечками.
  5. Коли мусите перевизначити обмеження, робіть це навмисно, мінімально і в коді — ніколи як аварійний фрагмент, що живе вічно.

Контейнери й так складні. Не робіть їх моторошними. Зробіть їх видимими, промаркованими правильно і нудними.
Саме нудне — те, що тримається в роботі.

← Попередня
Форматування SPF-запису: лапки та пробіли, що тихо ламають пошту
Наступна →
Імпорт VM з ESXi в Proxmox: Windows/Linux, драйвери VirtIO, маппінг NIC, виправлення завантаження

Залишити коментар