· 14 мин чтения

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Профиль выгружен из ядраДиагностика конкретной проблемы
auditEnforce + логирование разрешённых операцийФорензика, расследование

Реальный кейс: взлом 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 на новом сервере

Закроем 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.

Подпишитесь на рассылку ITfresh

Раз в неделю — практические гайды для руководителя IT и сисадмина: безопасность, 1С, миграции, резервные копии, лайфхаки из реальных проектов.

Реквизиты оператора персональных данных

ООО «АЙТИ-ФРЕШ», ИНН 7719418495, КПП 771901001. Юридический адрес: 105523, г. Москва, Щёлковское шоссе, д. 92, корп. 7. Контакт: info@itfresh.ru, +7 903 729-62-41. Оператор обрабатывает e-mail подписчика в целях рассылки информационных и рекламных материалов до момента отзыва согласия.