Короткий ответ
На Ubuntu 22.04/24.04 AppArmor — основной MAC. DENIED читают из journalctl -k / dmesg, профиль правят (aa-logprof или ручной include), затем enforce. aa-complain — окно отладки с датой возврата. aa-disable и apparmor=0 — не решение. Не чините DENIED через chmod 777 и не путайте с SELinux (setenforce): SELinux. Хост host.example (10.0.20.10), пользователь admin.
Симптомы и как отличить
- В логе приложения Permission denied, unix-mode нормальные;
apparmor="DENIED" operation="open" comm="mysqld" name="/var/lib/mysql-custom/";- после
aa-complain usr.sbin.mysqld«всё заработало» и так оставили.
| Слой | Как отличить |
|---|---|
| unix 640 | stat, пользователь сервиса |
| UFW | timeout с сети, локально ок |
| AppArmor | DENIED в kernel log |
| seccomp/Docker | контейнер, --security-opt |
Возможные причины
- Данные переехали в
/srv, профиль знает только/var/lib. - Обновление пакета ужесточило профиль.
- Свой бинарь без профиля работает unconfined — наоборот, не confined.
- Overlay в контейнере vs хостовый aa.
- Кто-то
aa-disableв прошлом инциденте. - Tunable не включён (
@{PROC}и т.п. — не выдумывайте, смотрите/etc/apparmor.d/tunables).
Диагностика
sudo aa-status
sudo systemctl is-enabled apparmor
sudo dmesg --ctime | grep -i DENIED | tail -n 30
sudo journalctl -k --since '1 hour ago' | grep -i apparmorКакой профиль у процесса:
ps -eZ | grep -E 'nginx|mysql|myservice'
sudo aa-status | grep -A2 'enforce mode'Файлы профилей: /etc/apparmor.d/. Отключённые лежат в disable/ симлинками.
ls /etc/apparmor.d/disable/ 2>/dev/null
sudo apparmor_parser -Q /etc/apparmor.d/usr.sbin.nginx 2>/dev/null || trueПрава unix всё равно проверьте: 777. Сервис от root маскирует часть проблем: User=.
Решение
Сценарий A. Недостающий путь в профиле пакета
Временное окно:
sudo aa-complain /usr/sbin/mysqld
# воспроизвести ошибку
sudo dmesg | grep DENIED | tail
sudo aa-logprof
sudo aa-enforce /usr/sbin/mysqld
sudo aa-status | grep mysqldaa-logprof интерактивен: принимайте только нужные path, не «allow all». Локальные дельты лучше в /etc/apparmor.d/local/usr.sbin.mysqld, чтобы пакет не затёр.
Пример идеи local (путь свой):
# /etc/apparmor.d/local/usr.sbin.mysqld
/srv/mysql/data/** rwk,sudo apparmor_parser -r /etc/apparmor.d/usr.sbin.mysqldСценарий B. Свой сервис без профиля
Создайте профиль aa-genprof /opt/myservice/bin/app на копии/staging, прогоните сценарии, enforce. Не оставляйте unconfined навсегда «потому что сложно». Минимальный профиль лучше отсутствия, если вы его прочитали.
Сценарий C. Кто-то выключил профиль
sudo rm /etc/apparmor.d/disable/usr.sbin.nginx
sudo apparmor_parser -r /etc/apparmor.d/usr.sbin.nginx
sudo aa-enforce /usr/sbin/nginxПроверьте, что сайт жив, затем оставьте enforce.
Сценарий D. Docker
Хостовый AppArmor может вешать профиль на контейнер (docker --security-opt apparmor=...). apparmor=unconfined в compose — понижение защиты. Чините профиль, не unconfined навсегда. Сокет хоста: docker.sock.
Разбор строки DENIED. Поля profile=, operation=, name= (путь), requested_mask= (r/w/k/m), comm=. Часто винят nginx, а comm="php-fpm8.3" — другой профиль (www-data vs /usr/sbin/php-fpm8.3). Правите тот файл в /etc/apparmor.d/, который совпал с profile=.
r vs w vs k (lock) vs m (mmap/exec). Для лог-файла нужен w, для библиотек mr. Не ставьте rwlkix на / «чтобы заработало».
Пакетные профили после apt-get upgrade могут перезаписать /etc/apparmor.d/usr.sbin.mysqld, но не должны затирать /etc/apparmor.d/local/*, если вы туда положили дельту. Проверьте, что include <local/usr.sbin.mysqld> есть в основном профиле (в штатных пакетах Ubuntu обычно да). Если вы правили основной файл — при апдейте потеряете и получите новые DENIED или наоборот дыру.
aa-complain на время отладки запишите в тикет с at/systemd-run --on-active=2h напоминанием вернуть enforce. Без таймера complain живёт до следующего инцидента.
Snap vs classic: snap-сервисы несут свои профили. DENIED с snap.foo не лечится правкой /etc/apparmor.d/usr.sbin.nginx. Смотрите snap connections.
Отладка без отключения MAC: aa-notify -s 1 (пакет apparmor-notify) на staging. На проде — journal. Не apparmor=0. Параллельный SELinux не нужен.
Если DENIED на unix сокет между nginx и php-fpm — разрешите конкретный путь сокета в обоих профилях, не unix, без границ если можете избежать.
После parser -r существующие процессы могут держать старый профиль до restart. Перезапустите unit в окно, не только parser.
Профили в /etc/apparmor.d/abstractions/ не копируйте в свой сервис целиком без чтения: abstractions/base нужен, abstractions/user-tmp может открыть /tmp/** шире политики. Лучше явные пути данных.
aa-status «processes are unconfined» для bash админа — норма. Смотрите mysqld, nginx, containerd. После внедрения нового бинаря в /opt добавьте профиль в тот же sprint, не «потом». В git храните local-include, не надейтесь, что live-сервер — источник истины: следующий apt затрёт ручные правки основного файла.
Если DENIED только при systemctl start и нет при ручном запуске от того же User — отличаются environment и systemd sandbox (ProtectHome, PrivateTmp). Сверяйте systemd-analyze security myservice как подсказку, но не включайте все 50 галочек сразу вместе с новым AppArmor.
Типичные ложные «выключим AppArmor» кейсы. PHP не пишет в wp-content/uploads — сначала uid пула и unix-права, DENIED будет с name= этого пути. MySQL data на отдельном LV /srv/mysql — штатный профиль знает /var/lib/mysql; local include обязателен. Nginx alias вне /var/www — то же. Docker storage driver overlayfs иногда даёт DENIED на хостовом runc; это профиль docker, не «сломался mysql».
aa-status «0 processes are in complain mode» — цель после отладки. Ненулевой complain без тикета = регрессия.
На 24.04 пакет apparmor-utils нужен для aa-complain/aa-logprof; на минимальном образе его может не быть. Поставьте utils, не отключайте MAC из-за отсутствия команды.
Профиль unconfined для /opt/myservice/bin/app означает, что baseline MAC на ваш бинарь не действует. Запланируйте genprof на staging, не считайте «нет DENIED» победой.
Не путайте systemctl stop apparmor (снимет профили до reboot в части сценариев) с лечением. Unit должен быть enabled. Политика: любой disable apparmor в тикете отклоняется, как и ufw disable.
Как проверить, что проблема устранена
sudo aa-status
sudo journalctl -k --since '10 min ago' | grep -i DENIED || echo 'no denied'
systemctl is-active apparmor
curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1/Профиль сервиса в enforce. Нет свежих DENIED при рабочей нагрузке. apparmor.service enabled. Firewall/SELinux не «выключены» как побочный эффект.
Если не помогло
- DENIED на capabilities: в профиле
capability, не User=root. - DENIED
ptraceот сканера — не allow всем ptrace. - Профиль не грузится: синтаксис
apparmor_parser -r -v. - Контейнер snap vs classic — разные профили.
- Это был UFW: timeout, нет DENIED.
Профилактика
- Local includes в git.
- После apt смотреть новые DENIED сутки.
- Запрет disable профилей в политике.
- Staging с тем же aa-status.
FAQ
complain vs disable
complain пишет события и пускает. disable не ограничивает. Оба не цель. Цель enforce.
aa-unconfined показывает bash
Интерактивные оболочки часто unconfined. Смотрите долгоживущие сервисы.
Нужен ли SELinux параллельно?
На Ubuntu — нет. Не включайте второй MAC без проекта.
logprof предлагает /proc/**
Не принимайте слепые broad rules. Сверяйте comm= в DENIED.
После parser -r сервис всё равно DENIED
Не тот профиль (имя бинаря vs profile name), или кэш контейнера, или unix-права.