AppArmor в Linux: песочница для сервисов в 50 строк профиля
Семёнов Евгений Сергеевич, директор АйТи Фреш. Когда на домашнем сервере можно игнорировать LSM-подсистему ядра, то на продакшене — уже нет. У нас на практике AppArmor закрывает сервисы клиентов от последствий RCE в PHP-библиотеке, от shell-инъекций в webhook-скриптах и от «убежавших» через volume контейнеров Docker. В этой статье — как мы пишем и сопровождаем AppArmor-профили, не превращая сервер в неотлаживаемую крепость.
Mandatory Access Control простыми словами
Классические права Linux (rwx+owner+group) — это DAC, дискреционное управление. Владелец решает, кому можно. MAC устроен иначе: политику назначает система, и даже root не может её обойти. AppArmor — одна из реализаций LSM-хуков, которая говорит ядру: «процесс nginx может читать /etc/nginx/, писать только в /var/log/nginx/, открывать только 80/443 — остальное блокировать». Если злоумышленник пролез через CVE в PHP-FPM, AppArmor не даст ему прочитать /etc/shadow, даже если пользователь www-data получит временный root.
Где AppArmor уже работает и где его надо включать
В Ubuntu Server и Debian 11/12 AppArmor установлен по умолчанию и включен для nginx, mysql, cups, tcpdump и пары десятков других пакетов. Проверить состояние:
sudo aa-status
# 34 profiles are loaded.
# 22 profiles are in enforce mode.
# /usr/sbin/cupsd
# /usr/sbin/nscd
# ...
# 12 profiles are in complain mode.
# 8 processes have profiles defined.
На RHEL-семействе (AlmaLinux, Rocky) стоит SELinux, и AppArmor туда ставить не рекомендую — две MAC-подсистемы в одном ядре жить не умеют. Выбираем, что нам роднее.
Анатомия профиля
Профиль — текстовый файл в /etc/apparmor.d/. Читается легко, даже если не знаешь синтаксиса. Вот пример для самописного Python-демона invoicer:
# /etc/apparmor.d/opt.invoicer.bin.invoicer
#include <tunables/global>
@{INVOICER_HOME} = /opt/invoicer
@{INVOICER_LOG} = /var/log/invoicer
/opt/invoicer/bin/invoicer {
#include <abstractions/base>
#include <abstractions/python>
#include <abstractions/nameservice>
capability net_bind_service,
network tcp,
network udp,
@{INVOICER_HOME}/** r,
@{INVOICER_HOME}/bin/** mrix,
@{INVOICER_HOME}/var/** rwk,
@{INVOICER_LOG}/** rw,
/etc/invoicer/config.yaml r,
/etc/ssl/certs/** r,
/proc/*/stat r,
/proc/*/status r,
/sys/kernel/mm/transparent_hugepage/enabled r,
deny @{HOME}/** rwkx,
deny /etc/shadow r,
deny /root/** rwkx,
}
Суть: демону разрешено делать только то, что явно перечислено. Всё остальное — запрет. Правила deny здесь декоративные (итак неявно запрещено), но я их пишу специально — чтобы в audit-логе видеть попытки, даже если кто-то потом случайно расширит профиль.
aa-genprof: автогенерация под реальную нагрузку
Писать профиль с нуля — путь долгий. Я делаю так: запускаю приложение в learning-режиме, выполняю типовые сценарии (старт, обработка запроса, бэкап), и утилита aa-genprof сама строит правила.
sudo apt install apparmor-utils
sudo aa-genprof /opt/invoicer/bin/invoicer
# Параллельно в другом окне — запускаем демон и прогоняем тесты
# После каждой операции — возвращаемся в aa-genprof и нажимаем S (scan)
# Утилита предлагает варианты: Abstraction? Include? Glob? Allow/Deny?
# Когда нагрузочный прогон пройден — F (Finish), профиль сохраняется
Я всегда перед запуском в enforce держу профиль в complain mode минимум неделю. За это время нормально отрабатывают редкие сценарии: ротация логов, еженедельные cron-задачи, разовые операции из админки. Всё, что система научилась разрешать — возвращаем обратно правилами.
| Режим | Что делает | Когда использовать |
|---|---|---|
| enforce | Блокирует запрещённое, пишет в лог | Продакшен после обкатки |
| complain | Разрешает всё, пишет в лог | Обучение, отладка, недельный прогон |
| disable | Профиль выгружен из ядра | Диагностика конкретной проблемы |
| audit | Enforce + логирование разрешённых операций | Форензика, расследование |
Реальный кейс: взлом Bitrix и последствия
В ноябре 2024 года к нам прибежал клиент — оптовая компания в Подольске, на их Битриксе 24.0 нашли свежий webshell. Атакующий сидел уже четыре дня, прочитал базу, успел залить nested-эксплойт в backup-папку. Что его остановило — у нас на этом сервере был AppArmor-профиль для PHP-FPM, который запрещал запись куда-либо, кроме /var/www/site/upload/ и /tmp. Попытки сделать exec shell-скрипта из /tmp аккуратно ломали закладки злоумышленника — в audit.log мы позже разобрали 47 таких попыток.
Без AppArmor ущерб был бы в разы больше: читать почту из /var/spool/mail, подложить SSH-ключ в /root/.ssh/authorized_keys, установить крон — все эти сценарии упёрлись в MAC. Выложить пост-инцидентный разбор клиент не разрешил, но в ключевом месте там одна строчка: «Ущерб: утечка данных клиентской базы (1220 записей), простой сайта 4 часа. Стоимость реагирования — 92 000 руб». Без AppArmor было бы тысяч 400 и две недели работ.
AppArmor + systemd: изящная интеграция
systemd умеет требовать AppArmor-профиль для юнита — это страховка, если кто-то выгрузит профиль, а сервис запустит заново.
# /etc/systemd/system/invoicer.service
[Service]
ExecStart=/opt/invoicer/bin/invoicer
AppArmorProfile=opt.invoicer.bin.invoicer
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
Если профиль не загружен — systemd откажется стартовать сервис. То что надо для параноидального продакшена.
AppArmor в Docker и LXC
Docker по умолчанию применяет профиль docker-default. Если нужен свой — генерируете, загружаете и указываете в запуске:
sudo apparmor_parser -r -W /etc/apparmor.d/docker-nginx
docker run --security-opt apparmor=docker-nginx -p 80:80 nginx:stable
Для LXC правило ещё проще — одна строчка в конфиге контейнера /var/lib/lxc/webhost/config:
lxc.apparmor.profile = lxc-container-restricted
Отладка: разбор audit-логов
Когда профиль в enforce и что-то сломалось, смотрим сюда:
sudo dmesg | grep DENIED | tail
sudo journalctl -k | grep apparmor="DENIED"
# или парсим аккуратно:
sudo aa-logprof
Утилита aa-logprof предложит добавить правила для каждого зафиксированного DENY — остаётся только подтвердить или отклонить. Я всегда делаю так: в staging-среде прогнали тест-набор, просмотрели денаи, сформировали финальный профиль, выкатили в прод.
Чек-лист внедрения AppArmor на новом сервере
- Включить
apparmorв параметрах GRUB (apparmor=1 security=apparmor) — на Debian иногда забывают. - Проверить
aa-status, убедиться, что стандартные профили загружены. - Для каждого самописного сервиса — сгенерировать профиль через
aa-genprof, недельный complain, потом enforce. - В systemd-юнитах всех собственных демонов прописать
AppArmorProfile=. - Настроить logrotate для
/var/log/audit/и alert в Zabbix/LibreNMS на слово DENIED в последние 5 минут. - В репозитории Ansible/Salt хранить все профили — восстановление сервера должно возвращать и политики безопасности.
Закроем Linux-сервисы по правилам безопасности
Внедрим AppArmor или SELinux на рабочие сервера — под ключ, с отладкой и полгода гарантии стабильной работы профилей. Поможем с форензикой, если на сервере уже инцидент: разберём audit-логи, найдём каналы и точки входа.
Телефон: +7 903 729-62-41
Telegram: @ITfresh_Boss
Семёнов Евгений Сергеевич, директор АйТи Фреш
FAQ — AppArmor на практике
- AppArmor или SELinux — что выбрать?
- AppArmor использует пути файлов, SELinux — метки inode. AppArmor проще читать и править, его профили похожи на fstab. SELinux мощнее, но входной порог выше. В Ubuntu и Debian по умолчанию AppArmor, в RHEL/CentOS — SELinux.
- Насколько AppArmor замедляет систему?
- На типичных веб-нагрузках разница меньше 2% — в пределах погрешности. На I/O-интенсивных приложениях вроде Ceph OSD лучше замерить до и после.
- Как быстро потушить AppArmor при инциденте?
- Правильно: перевести конкретный профиль в complain mode через aa-complain. Не трогать всю подсистему.
- AppArmor работает с Docker и LXC?
- Да. Docker идёт со встроенным профилем docker-default, LXC — с lxc-container-default. Оба можно переопределить.
- Что делать, если приложение пишет в непредсказуемые пути?
- Использовать tunables и glob-шаблоны. Объявляете @{MYAPP_DATA}=/srv/myapp и в профиле пишете @{MYAPP_DATA}/** rw.