57. ACL, capabilities и привилегии процессов
Введение: Почему классических rwx недостаточно
В предыдущей главе (56. Безопасность ОС. Права доступа, users, groups) мы детально исследовали классическую модель прав доступа UNIX: разделение на категории owner, group, others и девять битов rwx. Эта элегантная схема, рожденная на заре вычислительной эпохи в стенах Bell Labs, великолепно справлялась со своими задачами в мире мэйнфреймов и однопоточных утилит. Однако с развитием сложных многопользовательских систем, контейнеризации и распределенных микросервисов ее простота превратилась в фундаментальное архитектурное ограничение.
Классическая модель страдает от двух критических изъянов:
- Катастрофическая нехватка гранулярности. В рамках модели
rwxневозможно предоставить право на чтение (read) и выполнение (execute) конкретному сервисному пользователю (например, аудитору или сборщику метрик Prometheus), запретив ему запись (write), если этот пользователь не входит в основную группу файла. Единственный обходной путь в классическом UNIX — создавать под каждую комбинацию прав отдельную группу пользователей в/etc/groupи постоянно жонглировать ими, что превращает администрирование в логистический кошмар. - Тотальная бинарность власти
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 для конфигурационного файла сервиса:
# Пример вывода 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:
- Permitted — максимальный набор, который процесс может иметь.
- Inheritable — набор, который передается дочерним процессам через
execve. - 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(), происходит следующий конвейер:
- User Space: Go вызывает
syscall.Openat()илиsyscall.Chmod(). - Kernel Space: Ядро находит
inodeв dentry-кэше. - VFS Layer: В зависимости от операции, VFS вызывает хуки безопасности.
- Для файлов:
posix_acl_permission()проверяет ACL. - Для системных операций:
security_capable()проверяет capabilities текущего процесса.
- Для файлов:
- LSM (Linux Security Modules): Если в системе включены
58. SELinux и AppArmor, ядро передает контекст в LSM-модуль. LSM накладывает свои политики поверх нативных проверок. - Результат: Возврат
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 или прочитать защищенный конфиг), а затем мгновенно падает в непривилегированного пользователя.
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
// Это и есть принцип наименьших привилегий
}
Итог
- POSIX ACL расширяет классические
rwxдо списка пользователей/групп, хранится вxattrи добавляет нагрузку на VFS при частых обращениях. - Linux Capabilities дробят власть
rootна независимые биты (CAP_NET_BIND_SERVICE,CAP_DAC_OVERRIDEи др.), хранятся в атрибутеsecurity.capabilityи проверяются через LSM-хуки. - В Go управление привилегиями требует работы с
syscall.Setuid,syscall.Setgid,syscall.Setgroupsи понимания порядка сброса прав. - Безопасность: Всегда сбрасывайте
supplementary groupsпередsetuid, отключайтеCAP_SYS_ADMINв контейнерах и используйте capabilities вместоsetuidтам, где это возможно. - Производительность: Частые проверки ACL и capabilities происходят в ядре и влияют на кэш inode/dentry. Избегайте избыточных ACL в высоконагруженных сервисах.
Мы разобрали гибкие механизмы контроля доступа. Но что делать, когда базовых capabilities и ACL недостаточно, и нужна политика на уровне ядра, изоляция процессов и принудительное разделение прав? Об этом в следующей статье.