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 состоит из миллионов декларативных правил, явно разрешающих конкретные действия между типами:

Text
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.

Рассмотрим типичный профиль безопасности для веб-приложения:

Text
/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-системами:

  1. Дрейф путей запуска в AppArmor: Если путь к бинарнику динамически меняется (например, сервис запускается из временного каталога CI/CD или хэшированного пути /app/releases/v1.2/myapp), жесткий профиль AppArmor может проигнорировать исполнение или заблокировать дочерние процессы. Решения:
    • Явная фиксация путей запуска в профиле: profile /usr/bin/myapp { ... }.
    • Использование профилирования по криптографическому хэшу бинарника (profile /usr/bin/myapp sha256:abc123 { ... } или хэширование через sha256).
  2. Метки SELinux при развертывании: Если вы скопируете новый Go-бинарник на RedHat/CentOS через scp или соберете его локально через go build, файл получит контекст пользователя (user_home_t). Попытка службы systemd запустить такой сервис завершится мгновенным падением с ошибкой permission denied. Необходимо выполнить перемаркировку меток с помощью утилит semanage fcontext и restorecon.
  3. Минимизация привилегий через Capabilities: Вместо запуска сервиса от имени всемогущего root, современная архитектура требует дробить полномочия процесса через CAPABILITIES (см. 57. ACL, capabilities и привилегии процессов).

В языке Go низкоуровневое управление привилегиями и флагами ядра реализуется через официальный пакет расширений golang.org/x/sys/unix:

Go
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.
  • Диагностика 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: Отчет о блокировке с точным указанием пути и флагов
  

Итог

  1. MAC против DAC: В отличие от избирательной модели DAC (chmod), системы принудительного контроля доступа (SELinux и AppArmor) действуют на уровне ядра через хуки LSM, изолируя даже процессы с полномочиями суперпользователя root.
  2. SELinux: Опирается на жесткую типизацию (Type Enforcement), контексты безопасности формата user:role:type:level и расширенные атрибуты инодов (security.selinux), обеспечивая максимальный уровень защиты enterprise-систем.
  3. AppArmor: Использует путь-ориентированный подход (Path-based MAC), компилирует профили из /etc/apparmor.d/ в детерминированные автоматы (DFA) и является стандартом де-факто в контейнерных экосистемах Kubernetes и Ubuntu.
  4. Статические бинарники Go: Требуют аккуратной настройки политик (фиксация путей запуска, хэширование через sha256, перемаркировка меток через restorecon и semanage fcontext).
  5. Контейнерная безопасность: Стандартный профиль docker-default блокирует вызовы ptrace и доступ к /proc/sys. При необходимости тонкой отладки pprof следует расширять возможности через --cap-add=SYS_PTRACE или кастомные профили.
  6. Наблюдаемость и аудит: Блокировки MAC проявляются в ошибке EACCES и отслеживаются с помощью системных утилит auditd, dmesg, ausearch -m avc -ts recent, aa-status и aa-logprof.

Мы подробно изучили, как ядро операционной системы осуществляет многослойную фильтрацию и блокировку нелегитимных действий процессов. Но что происходит, когда процесс все же терпит крах — будь то из-за критической ошибки сегментации (SIGSEGV), аппаратного сбоя памяти или неисправности драйвера? В следующей статье мы исследуем механизмы посмертной диагностики: разберем генерацию дампов памяти и научимся находить причину падения сервиса в 59. Crash Dump, Core Dump и анализ падений.