58. SELinux и AppArmor
Mandatory Access Control: Почему chmod больше не достаточно
В предыдущих разделах (56. Безопасность ОС. Права доступа, users, groups, 57. ACL, capabilities и привилегии процессов) мы изучили механизмы, управляющие правами доступа в UNIX-системах. Все они опираются на классическую модель DAC (Discretionary Access Control — избирательное управление доступом). В парадигме DAC владелец ресурса сам решает («по своему усмотрению»), кому доверить чтение или модификацию своих файлов через команду chmod или списки ACL.
Увы, в суровых реалиях современного бэкенда модель DAC обладает фатальной уязвимостью. Представьте популярный веб-сервер nginx или микросервис на Go, обрабатывающий входящий HTTP-трафик от имени непривилегированного пользователя www-data. Если в коде сторонней библиотеки или парсера протокола обнаружится уязвимость удаленного исполнения кода (RCE), злоумышленник захватит контроль над процессом. И в этот момент он получит абсолютно все полномочия пользователя www-data! Злоумышленник сможет прочитать ключи сессий, модифицировать исходные файлы приложения, внедрить бэкдор в базу SQLite или запустить криптомайнер во временном каталоге /tmp. В модели DAC операционная система даже не шелохнется, ведь с точки зрения chmod процесс делает то, на что у него есть формальные файловые права.
Чтобы разорвать эту порочную связь между скомпрометированным процессом и доступными ему ресурсами, в компьютерных науках была создана концепция MAC (Mandatory Access Control — принудительное управление доступом). В системе MAC ни владелец файла, ни даже всемогущий суперпользователь root не могут произвольно раздавать права. Безопасность регулируется централизованной политикой операционной системы: даже если процесс запущен от root, ядро жестко запретит ему открывать сокеты, писать в системные каталоги или читать закрытые файлы, если такое действие не прописано в утвержденном правиле безопасности.
В мире Linux главными бастионами принудительного контроля доступа являются системы SELinux и AppArmor (58. SELinux и AppArmor), построенные поверх архитектурного каркаса LSM (Linux Security Modules).
flowchart TD
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
subgraph UserSpace ["Пространство пользователя (User Space)"]
Attacker["Атакующий (RCE через уязвимый Go-код)"]
CompromisedProcess["Скомпрометированный сервис (UID=1000)"]
end
subgraph KernelSpace ["Пространство ядра (Kernel Space)"]
SyscallFilter["Обработка системного вызова (sys_openat)"]
DAC_Check{"Проверка DAC (chmod / ACL)"}
LSM_Check{"Проверка MAC (LSM: SELinux / AppArmor)"}
TargetFile["Целевой файл (/etc/shadow или /var/data)"]
end
Attacker -->|"Управляет"| CompromisedProcess
CompromisedProcess -->|"Попытка чтения"| SyscallFilter
SyscallFilter --> DAC_Check
DAC_Check -->|"Разрешено владельцем"| LSM_Check
LSM_Check -->|"Блокировка политикой!"| DenyAlert["Отказ: EACCES / Лог в auditd"]
LSM_Check -->|"Разрешено политикой"| TargetFile
Для Go-разработчика понимание MAC — не абстрактная теория, а насущная производственная необходимость: именно правила AppArmor и SELinux диктуют, какие системные вызовы может выполнять Docker-контейнер, почему вызов os.Open неожиданно падает с ошибкой EACCES при корректных файловых правах, и какие capabilities требуются сервису для нормальной работы.
LSM: Архитектура безопасности ядра
До начала 2000-х годов попытки внедрить принудительный контроль доступа в Linux приводили к появлению несовместимых между собой патчей ядра. Архитектурное спасение пришло с созданием каркаса LSM (Linux Security Modules). LSM не реализует конкретных политик безопасности, а предоставляет универсальную систему перехвата операций в ядре.
flowchart LR
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
subgraph Syscalls ["Системные вызовы"]
VFS_Op["Файлы: openat, read, write"]:::process
Net_Op["Сеть: socket, bind, connect"]:::network
Task_Op["Процессы: execve, fork, ptrace"]:::process
IPCOp["IPC: shmget, msgsnd"]:::process
end
subgraph KernelCore ["Подсистемы ядра Linux"]
VFS["VFS Subsystem"]:::process
NetStack["Сетевой стек"]:::process
SchedCore["Планировщик (sched)"]
IPCCore["Подсистема IPC (ipc)"]
end
subgraph LSMFramework ["Каркас LSM (security_hook_heads)"]
HookChain["Цепочка зарегистрированных хуков"]:::process
SELinuxMod["Модуль SELinux"]:::process
AppArmorMod["Модуль AppArmor"]:::process
BPFMod["Модуль BPF-LSM"]:::process
end
VFS_Op --> VFS --> HookChain
Net_Op --> NetStack --> HookChain
Task_Op --> SchedCore --> HookChain
IPCOp --> IPCCore --> HookChain
HookChain --> SELinuxMod
HookChain --> AppArmorMod
HookChain --> BPFMod
Принцип работы предельно прост и надежен: ядро сначала выполняет стандартную проверку DAC (права пользователя, группы и биты rwx). Если DAC запрещает операцию, ядро сразу возвращает ошибку, экономя такты процессора. Но если DAC дает добро, управление передается LSM-хуку. LSM вызывает функции зарегистрированных модулей безопасности, и только при единогласном одобрении системный вызов получает доступ к «железу».
SELinux: Контексты, xattrs и политика
Разработанный Агентством национальной безопасности США (NSA), SELinux (Security-Enhanced Linux) представляет собой реализацию механизмов Type Enforcement (управление типами), RBAC (ролевой доступ) и MLS/MCS (многоуровневая безопасность).
В парадигме SELinux все элементы операционной системы делятся на субъекты (процессы) и объекты (файлы, сокеты, каталоги, дескрипторы). Каждому субъекту и объекту присваивается обязательная метка безопасности — контекст безопасности:
user:role:type:level
user— логическая личность SELinux (не путать с Linux UID).role— роль, определяющая допустимый набор типов (например,system_r).type(или домен для процессов) — сердце Type Enforcement. Определяет функциональное назначение объекта (например, процессhttpd_tили файлыvar_log_t).level— диапазон секретности и категории (MLS/MCS), активно используемый в защищенных кластерах и контейнерах (s0:c1,c2).
Политика SELinux состоит из миллионов декларативных правил, явно разрешающих конкретные действия между типами:
allow httpd_t var_log_t :file { write };
Это правило гласит: процесс, работающий в домене типа httpd_t, имеет право производить операцию записи (write) в файлы, помеченные типом var_log_t. Если процесс попытается прочитать /etc/shadow, для которого нет разрешающего правила allow, ядро заблокирует операцию с ошибкой доступа, даже если процесс запущен от привилегированного пользователя root!
flowchart LR
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
subgraph Subject ["Субъект (Процесс)"]
Process["Go-бинарник / Nginx
Контекст: system_u:system_r:httpd_t:s0"]
end
subgraph AVC_Engine ["SELinux Engine & AVC Cache"]
AVC{"Access Vector Cache (AVC)"}
PolicyRule["Политика: allow httpd_t var_log_t :file { write };"]
end
subgraph Object ["Объект (Файл)"]
LogFile["Файл лога: /var/log/app.log
Контекст: system_u:object_r:var_log_t:s0"]
ShadowFile["Файл паролей: /etc/shadow
Контекст: system_u:object_r:shadow_t:s0"]
end
Process -->|"write()"| AVC
AVC -->|"Проверка правила"| PolicyRule
PolicyRule -->|"Разрешено"| LogFile
Process -.->|"Попытка read()"| ShadowFile
AVC -.->|"Правило отсутствует: BLOCK!"| ShadowFile
Как это работает на уровне файловой системы:
SELinux хранит контексты безопасности файлов в расширенных атрибутах (xattrs) инодов под системным именем security.selinux. При каждом обращении к файлу VFS вызывает функцию ядра getxattr(), извлекает контекст и сверяет его с правилами через кэш решений AVC (Access Vector Cache). Это добавляет накладные расходы на каждый syscall доступа к файлу, поэтому политики тщательно оптимизируются, а AVC кэширует результаты проверок в оперативной памяти процессора.
Режимы работы SELinux:
Enforcing— штатный режим: все запрещенные политикой действия жестко блокируются ядром, а факты нарушений протоколируются в журнал аудита.Permissive— режим отладки: ядро не блокирует подозрительные операции, но заносит подробные предупреждения в системный лог. Идеально подходит для профилирования и разработки новых правил без риска сломать прод.Disabled— полное отключение подсистемы SELinux и деактивация соответствующих LSM-хуков.
AppArmor: Профильно-ориентированный подход
Если SELinux ориентирован на глобальные метки файловой системы, то AppArmor (Application Armor) предлагает более интуитивный и прагматичный подход, основанный на пути к исполняемому файлу (Path-based MAC).
В AppArmor безопасность настраивается через профили, которые привязываются не к абстрактным инодам или типам, а к конкретным абсолютным путям бинарных файлов на диске или профилям в подсистеме 53. Контейнеры под капотом. namespaces и cgroups.
Рассмотрим типичный профиль безопасности для веб-приложения:
/usr/bin/myapp {
#include <abstractions/base>
/etc/app/config r,
/var/log/myapp w,
network inet tcp,
deny /etc/shadow r,
}
В этом профиле декларативно зафиксировано: бинарнику /usr/bin/myapp разрешено читать конфигурационный файл /etc/app/config, писать в директорию логов /var/log/myapp, открывать сетевые TCP-сокеты (network inet tcp), а любая попытка прочитать системную базу паролей /etc/shadow принудительно пресекается правилом deny.
flowchart TD
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
AppBinary["/usr/bin/myapp (Go Executable)"]:::system --> KernelLoader["Ядро: execve()"]
KernelLoader --> AppArmorParser["AppArmor: Загрузка профиля из /etc/apparmor.d/"]
AppArmorParser --> CompiledDFA["Компиляция в детерминированный автомат (DFA)"]
CompiledDFA --> SyscallMonitor{"Перехват системных вызовов"}
SyscallMonitor -->|"/etc/app/config r"| AllowRead["Разрешить чтение (fd)"]
SyscallMonitor -->|"/etc/shadow r"| DenyRead["Запретить: EACCES / Log audit"]
AppArmor хранит профили в каталоге /etc/apparmor.d/. Перед загрузкой в ядро утилита apparmor_parser компилирует текстовый профиль в компактный двоичный детерминированный конечный автомат (DFA — Deterministic Finite Automaton). Это позволяет ядру молниеносно проверять пути файлов при системных вызовах без накладных расходов на чтение расширенных дисковых атрибутов xattr. Именно поэтому AppArmor стал стандартом по умолчанию в дистрибутивах семейства Debian/Ubuntu и средах оркестрации контейнеров вроде Kubernetes.
Влияние на Go-разработку и контейнеризацию
По умолчанию компилятор Go создает статически скомпонованные исполняемые бинарники (без зависимостей от внешних .so-библиотек C). Это создает специфические нюансы при работе с MAC-системами:
- Дрейф путей запуска в AppArmor: Если путь к бинарнику динамически меняется (например, сервис запускается из временного каталога CI/CD или хэшированного пути
/app/releases/v1.2/myapp), жесткий профиль AppArmor может проигнорировать исполнение или заблокировать дочерние процессы. Решения:- Явная фиксация путей запуска в профиле:
profile /usr/bin/myapp { ... }. - Использование профилирования по криптографическому хэшу бинарника (
profile /usr/bin/myapp sha256:abc123 { ... }или хэширование черезsha256).
- Явная фиксация путей запуска в профиле:
- Метки SELinux при развертывании: Если вы скопируете новый Go-бинарник на RedHat/CentOS через
scpили соберете его локально черезgo build, файл получит контекст пользователя (user_home_t). Попытка службыsystemdзапустить такой сервис завершится мгновенным падением с ошибкой permission denied. Необходимо выполнить перемаркировку меток с помощью утилитsemanage fcontextиrestorecon. - Минимизация привилегий через Capabilities: Вместо запуска сервиса от имени всемогущего
root, современная архитектура требует дробить полномочия процесса черезCAPABILITIES(см. 57. ACL, capabilities и привилегии процессов).
В языке Go низкоуровневое управление привилегиями и флагами ядра реализуется через официальный пакет расширений golang.org/x/sys/unix:
package main
import (
"fmt"
"log"
"golang.org/x/sys/unix"
)
func dropPrivileges() error {
// При LINUX_CAPABILITY_VERSION_3 ядро ожидает массив из ДВУХ структур CapUserData
// (capability 0–31 и 32–63), а не одну. Типы называются CapUserHeader и CapUserData.
header := unix.CapUserHeader{Version: unix.LINUX_CAPABILITY_VERSION_3}
var data [2]unix.CapUserData
// 1. Оставляем только CAP_NET_BIND_SERVICE для прослушивания <1024 портов
// (расширять набор после сброса уже нельзя, поэтому задаём нужный набор сразу)
data[0].Permitted = 1 << unix.CAP_NET_BIND_SERVICE
data[0].Effective = 1 << unix.CAP_NET_BIND_SERVICE
if err := unix.Capset(&header, &data[0]); err != nil {
return fmt.Errorf("capset: %w", err)
}
// Внимание: unix.Capset действует только на ТЕКУЩИЙ поток ОС, а не на весь процесс
// (в отличие от syscall.Setuid). Остальные потоки Go сохранят прежние capabilities.
// 2. Смена UID процесса делается через syscall.Setuid (os.Chown меняет владельца файла, а не UID).
return nil
}
func main() {
if err := dropPrivileges(); err != nil {
log.Fatal(err)
}
// Инициализация HTTP-сервера и т.д.
}
Отладка и мониторинг MAC
Когда подсистема безопасности ядра блокирует операцию вашего приложения, это может выглядеть как необъяснимый сбой в прикладном коде: функция возвращает стандартную ошибку EACCES («Permission Denied»), хотя утилита ls -l подтверждает полные права 777 на файл.
Для диагностики необходимо обращаться к журналу аудита операционной системы и буферу ядра dmesg или демону auditd:
- Диагностика SELinux:
- Поиск недавних отказов в доступе (AVC denials):
ausearch -m avc -ts recent. - Анализ журнала и генерация рекомендаций на человеческом языке:
sealert -a /var/log/audit/audit.log. - Автоматическая генерация недостающих разрешающих правил на основе зафиксированных инцидентов:
audit2allow.
- Поиск недавних отказов в доступе (AVC denials):
- Диагностика AppArmor:
- Просмотр системного буфера на предмет блокировок:
dmesg | grep audit. - Проверка статуса загруженных профилей и активных режимов:
aa-status. - Интерактивный анализ журнала аудита для дообучения профилей приложения:
aa-logprof.
- Просмотр системного буфера на предмет блокировок:
В устойчивых Go-приложениях крайне важно реализовывать корректное завершение (graceful shutdown) с обработкой сигналов SIGTERM и SIGKILL, поскольку MAC-блокировки не влияют на маршрутизацию сигналов от диспетчера процессов systemd. Однако если во время сброса буферов на диск сервис попытается сохранить аварийный дамп в файл, заблокированный политикой безопасности, системный вызов os.WriteFile вернет ошибку EACCES. Если в коде отсутствует паттерн if err != nil { log and handle }, приложение упадет с паникой, потеряв несохраненные транзакции пользователей.
sequenceDiagram
autonumber
participant App as Go-приложение
participant Kernel as Ядро (LSM/AppArmor)
participant AuditLog as auditd / dmesg
participant Dev as Инженер (CLI)
App->>Kernel: os.WriteFile("/var/log/app.dump", data)
Kernel->>Kernel: Проверка профиля AppArmor (DFA)
Note over Kernel: Политикой запись в данный путь запрещена!
Kernel->>AuditLog: AVC log: apparmor="DENIED" comm="myapp" requested_mask="w"
Kernel-->>App: errno = EACCES (Permission Denied)
App->>App: if err != nil { log and handle }
Dev->>AuditLog: dmesg | grep audit / aa-logprof
AuditLog-->>Dev: Отчет о блокировке с точным указанием пути и флагов
Итог
- MAC против DAC: В отличие от избирательной модели DAC (
chmod), системы принудительного контроля доступа (SELinux и AppArmor) действуют на уровне ядра через хуки LSM, изолируя даже процессы с полномочиями суперпользователяroot. - SELinux: Опирается на жесткую типизацию (Type Enforcement), контексты безопасности формата
user:role:type:levelи расширенные атрибуты инодов (security.selinux), обеспечивая максимальный уровень защиты enterprise-систем. - AppArmor: Использует путь-ориентированный подход (Path-based MAC), компилирует профили из
/etc/apparmor.d/в детерминированные автоматы (DFA) и является стандартом де-факто в контейнерных экосистемах Kubernetes и Ubuntu. - Статические бинарники Go: Требуют аккуратной настройки политик (фиксация путей запуска, хэширование через
sha256, перемаркировка меток черезrestoreconиsemanage fcontext). - Контейнерная безопасность: Стандартный профиль
docker-defaultблокирует вызовыptraceи доступ к/proc/sys. При необходимости тонкой отладки pprof следует расширять возможности через--cap-add=SYS_PTRACEили кастомные профили. - Наблюдаемость и аудит: Блокировки MAC проявляются в ошибке
EACCESи отслеживаются с помощью системных утилитauditd,dmesg,ausearch -m avc -ts recent,aa-statusиaa-logprof.
Мы подробно изучили, как ядро операционной системы осуществляет многослойную фильтрацию и блокировку нелегитимных действий процессов. Но что происходит, когда процесс все же терпит крах — будь то из-за критической ошибки сегментации (SIGSEGV), аппаратного сбоя памяти или неисправности драйвера? В следующей статье мы исследуем механизмы посмертной диагностики: разберем генерацию дампов памяти и научимся находить причину падения сервиса в 59. Crash Dump, Core Dump и анализ падений.