57. ACL, capabilities и привилегии процессов

Введение: Почему классических rwx недостаточно

В предыдущей главе (56. Безопасность ОС. Права доступа, users, groups) мы детально исследовали классическую модель прав доступа UNIX: разделение на категории owner, group, others и девять битов rwx. Эта элегантная схема, рожденная на заре вычислительной эпохи в стенах Bell Labs, великолепно справлялась со своими задачами в мире мэйнфреймов и однопоточных утилит. Однако с развитием сложных многопользовательских систем, контейнеризации и распределенных микросервисов ее простота превратилась в фундаментальное архитектурное ограничение.

Классическая модель страдает от двух критических изъянов:

  1. Катастрофическая нехватка гранулярности. В рамках модели rwx невозможно предоставить право на чтение (read) и выполнение (execute) конкретному сервисному пользователю (например, аудитору или сборщику метрик Prometheus), запретив ему запись (write), если этот пользователь не входит в основную группу файла. Единственный обходной путь в классическом UNIX — создавать под каждую комбинацию прав отдельную группу пользователей в /etc/group и постоянно жонглировать ими, что превращает администрирование в логистический кошмар.
  2. Тотальная бинарность власти root (All-or-Nothing). В традиционной парадигме процесс либо абсолютно бесправен (обычный пользователь), либо всемогущ (UID=0). Если веб-серверу на Go требовалось всего лишь занять привилегированный сетевой порт 443 или демону синхронизации времени требовалось скорректировать часы ядра, администратор был вынужден либо запускать весь сервис целиком под суперпользователем root, либо навешивать на бинарник опаснейший бит setuid. Это уподобляет архитектуру замку, где ключнику отдают связку от всех золотых хранилищ и оружейных палат только ради того, чтобы он мог открыть метлу в чулане. Любая уязвимость в коде сервиса автоматически означала полную компрометацию хоста.

Чтобы преодолеть эти барьеры и реализовать подлинный принцип наименьших привилегий (Principle of Least Privilege), ядро Linux внедрило две мощные, гармонично дополняющие друг друга технологии: POSIX ACL (Access Control Lists) для точечного разграничения прав на файловые объекты и Linux Capabilities для дробления абсолютной монополии root на десятки независимых привилегий.

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 Problem ["Проблема классического UNIX DAC"]
        direction TB
        BinaryRoot["Бинарный UID=0 (Root): Абсолютная власть или ничего"]
        RigidRWX["Жесткая триада rwx: Только Owner / Group / Others"]:::process
    end

    subgraph Solution ["Современный арсенал ядра Linux"]
        direction TB
        Capabilities["Linux Capabilities: Дробление root на 41+ независимый флаг"]:::system
        ACL["POSIX ACL: Произвольные списки доступа через xattr"]:::process
    end

    BinaryRoot ==> Capabilities
    RigidRWX ==> ACL

    Capabilities --> SafeServices["Безопасные Go-сервисы: cap_net_bind_service без root"]
    ACL --> GranularFiles["Гранулярный доступ: права отдельным пользователям и группам"]
  

POSIX ACL: Гранулярный контроль доступа

POSIX Access Control List (ACL) — это расширенная структура прав, которая позволяет задавать rwx не только для владельца и группы, но и для произвольных пользователей или групп, а также задавать маски и обязательные права.

Как это работает

Вместо трех бит rwx, файловая система хранит список записей. Каждая запись имеет тип (USER, GROUP, MASK, OTHER) и идентификатор.

Рассмотрим реальный вывод утилиты getfacl для конфигурационного файла сервиса:

Bash
# Пример вывода getfacl
user:ivan:rwx
group:devs:r-x
mask::rwx
other::r--

Обратите внимание на специальную запись mask::rwx. Маска (MASK) определяет максимально возможные права для всех именованных пользователей и групп. Даже если пользователю ivan явно назначены права rwx, но маска установлена в r--, ядро вычислит эффективные права через побитовое AND (rwx & r-- = r--), мгновенно запретив запись и исполнение. Это позволяет администраторам безопасно ограничивать расширенные права одним действием.

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 ACLEntries ["Записи POSIX ACL для файла"]
        direction TB
        OwnerEntry["user::rw- (Владелец файла)"]
        IvanEntry["user:ivan:rwx (Именованный пользователь)"]
        DevsEntry["group:devs:r-x (Именованная группа)"]
        MaskEntry["mask::r-- (Фильтрующая маска)"]
        OtherEntry["other::r-- (Все остальные)"]
    end

    IvanEntry --> BitwiseAnd{"Побитовое И с MASK"}
    MaskEntry --> BitwiseAnd
    BitwiseAnd --> EffectiveIvan["Эффективные права ivan: r-- (только чтение!)"]
  

Под капотом

POSIX ACL не хранится в стандартных inode-битах. Linux использует Extended Attributes (xattr) — механизм хранения метаданных, привязанных к файлу.

  • ACL сохраняется в расширенном атрибуте с именем system.posix_acl_access.
  • При каждом обращении к файлу VFS (Virtual File System) вызывает posix_acl_permission(), который парсит этот xattr и накладывает его поверх базовых rwx.
  • Влияние на производительность: Парсинг ACL происходит в ядре при каждом системном вызове openat, access или chmod. Если атрибут большой, это вызывает дополнительные кэши-промахи в inode-кэше ядра. В высоконагруженных сервисах (например, веб-серверы, обрабатывающие миллионы запросов в секунду) избыточное использование ACL может стать узким местом. В Go это ощущается через syscall.Stat или os.Stat, которые возвращают Errno или fs.FileInfo с задержкой из-за кэширования VFS.

Linux Capabilities: Дробление абсолютной власти

Capabilities — это механизм разделения полномочий root на мелкие, независимые блоки. Вместо того чтобы давать процессу полный UID=0, мы даем ему ровно те права, которые ему нужны.

Основные возможности

  • CAP_NET_BIND_SERVICE — привязка к портам < 1024.
  • CAP_DAC_OVERRIDE — обход проверки прав доступа к файлам (аналог chmod для root).
  • CAP_SYS_TIME — изменение системных часов.
  • CAP_NET_ADMIN — настройка сетевых интерфейсов, iptables.
  • CAP_SYS_PTRACE — отладка и трассировка процессов через ptrace.
  • CAP_SYS_ADMIN — всеобъемлющее системное администрирование (аналог полного root).

Иерархия и хранение

Каждый процесс в Linux имеет три набора capabilities:

  1. Permitted — максимальный набор, который процесс может иметь.
  2. Inheritable — набор, который передается дочерним процессам через execve.
  3. Effective — текущий активный набор. Если бит установлен, процесс действует как root для этой операции.

Кроме того, ядро поддерживает bounding set (глобальный ограничивающий набор) и ambient set (набор окружающих возможностей, введенный в Linux 4.3).

Capabilities хранятся в расширенном атрибуте security.capability. Они привязаны к исполняемому файлу, а не к пользователю. Когда вы запускаете бинарник, ядро читает этот атрибут и формирует начальные наборы прав.

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
    A["Запуск бинарника: вызов execve()"]:::entry --> B["Ядро читает security.capability"]
    B --> C["Формируются permitted и inheritable наборы"]
    C --> D["execve вызывает security_capable"]
    D --> E["Проверка через LSM"]
    E --> F["Операция разрешена или сброшена"]
  

Под капотом: VFS, extended attributes и проверка прав

Когда Go-приложение делает os.Chmod() или os.Open(), происходит следующий конвейер:

  1. User Space: Go вызывает syscall.Openat() или syscall.Chmod().
  2. Kernel Space: Ядро находит inode в dentry-кэше.
  3. VFS Layer: В зависимости от операции, VFS вызывает хуки безопасности.
    • Для файлов: posix_acl_permission() проверяет ACL.
    • Для системных операций: security_capable() проверяет capabilities текущего процесса.
  4. LSM (Linux Security Modules): Если в системе включены 58. SELinux и AppArmor, ядро передает контекст в LSM-модуль. LSM накладывает свои политики поверх нативных проверок.
  5. Результат: Возврат 0 (success) или -EPERM/-EACCES.
sequenceDiagram
    autonumber
    participant GoApp as Go-сервис (User Space)
    participant Syscall as Системный вызов (Ring 0)
    participant VFS as Слой VFS (Virtual File System)
    participant ACL as Диспетчер POSIX ACL
    participant LSM as LSM (SELinux / AppArmor)

    GoApp->>Syscall: syscall.Openat() / syscall.Chmod()
    Syscall->>VFS: Поиск inode в dentry cache
    VFS->>ACL: posix_acl_permission() (проверка xattr)
    alt Ошибка в ACL
        ACL-->>Syscall: -EACCES
        Syscall-->>GoApp: errno = -EACCES (Permission Denied)
    else ACL проверка пройдена
        ACL->>LSM: security_capable() / security_inode_permission()
        alt LSM блокирует действие
            LSM-->>Syscall: -EPERM
            Syscall-->>GoApp: errno = -EPERM (Operation not permitted)
        else Все проверки пройдены
            LSM-->>VFS: Доступ подтвержден (0)
            VFS-->>GoApp: Возврат файлового дескриптора (fd)
        end
    end
  

Управление привилегиями в Go

В Go нет встроенного высокоуровневого API для работы с capabilities, поэтому мы работаем напрямую с syscall или используем проверенные сторонние обертки.

Демонстрация: безопасное падение привилегий

Типичный сценарий в бэкенде: процесс стартует от root (чтобы забиндиться на порт 443 или прочитать защищенный конфиг), а затем мгновенно падает в непривилегированного пользователя.

Go
package main

import (
    "fmt"
    "log"
    "os"
    "syscall"
)

func dropPrivileges(uid, gid int) error {
    // 1. Сначала сбрасываем supplementary groups (дополнительные группы)
    // Это критично: если не сбросить, процесс сохранит доступ к группам root
    if err := syscall.Setgroups([]int{}); err != nil {
        return fmt.Errorf("setgroups: %w", err)
    }

    // 2. Меняем GID и UID
    // Порядок важен: сначала GID, потом UID
    if err := syscall.Setgid(gid); err != nil {
        return fmt.Errorf("setgid: %w", err)
    }
    if err := syscall.Setuid(uid); err != nil {
        return fmt.Errorf("setuid: %w", err)
    }

    // 3. Очистка capabilities здесь не показана (константы syscall.CAP_CLEAR не существует).
    // Для управления наборами capabilities используют unix.Capset (см. статью про SELinux и AppArmor),
    // помня, что capset действует на один поток, либо внешние средства вроде capsh.
    return nil
}

func main() {
    // Симуляция старта от root
    if os.Geteuid() != 0 {
        log.Fatal("Этот пример должен запускаться от root")
    }

    fmt.Println("UID:", os.Getuid(), "GID:", os.Getgid())

    // Падение привилегий
    if err := dropPrivileges(1000, 1000); err != nil {
        log.Fatalf("Не удалось сбросить права: %v", err)
    }

    fmt.Println("После дропа UID:", os.Getuid(), "GID:", os.Getgid())
    // os.Open теперь не сможет прочитать файлы, доступные только root
    // Это и есть принцип наименьших привилегий
}

Итог

  1. POSIX ACL расширяет классические rwx до списка пользователей/групп, хранится в xattr и добавляет нагрузку на VFS при частых обращениях.
  2. Linux Capabilities дробят власть root на независимые биты (CAP_NET_BIND_SERVICE, CAP_DAC_OVERRIDE и др.), хранятся в атрибуте security.capability и проверяются через LSM-хуки.
  3. В Go управление привилегиями требует работы с syscall.Setuid, syscall.Setgid, syscall.Setgroups и понимания порядка сброса прав.
  4. Безопасность: Всегда сбрасывайте supplementary groups перед setuid, отключайте CAP_SYS_ADMIN в контейнерах и используйте capabilities вместо setuid там, где это возможно.
  5. Производительность: Частые проверки ACL и capabilities происходят в ядре и влияют на кэш inode/dentry. Избегайте избыточных ACL в высоконагруженных сервисах.

Мы разобрали гибкие механизмы контроля доступа. Но что делать, когда базовых capabilities и ACL недостаточно, и нужна политика на уровне ядра, изоляция процессов и принудительное разделение прав? Об этом в следующей статье.

58. SELinux и AppArmor