Безопасность и изоляция: права, capabilities, SELinux, namespaces, cgroups, jails
Операционная система существует ради одной задачи, которая формулируется до неприличия просто: несколько взаимно недоверяющих программ работают на одном железе, и ни одна не должна испортить другую. Всё остальное — планировщик, файловые системы, сетевой стек — это удобства. Изоляция — это причина, по которой ядро вообще отделено от приложений и по которой процессор имеет два режима работы.
Но «безопасность ОС» — это не один механизм, а слоёный пирог из десятка независимых подсистем, которые накапливались сорок лет и почти не согласованы между собой. Программисту это важно не в теории. Именно здесь живут вопросы, на которые приходится отвечать в проде: почему контейнер, запущенный от «непривилегированного» пользователя, всё-таки читает /proc/kcore; почему бинарник с setcap cap_net_bind_service перестал биндиться на 80-й порт после того, как в юнит добавили NoNewPrivileges=yes; почему chmod 777 не помог, а getenforce показывает Enforcing; почему strace говорит, что вызова не существует, хотя он точно есть.
Разберём слои снизу вверх: сначала классическая модель прав, потом её починка через capabilities, потом мандатный контроль, потом фильтрация вызовов, потом изоляция ресурсов и представлений — и в конце то, как из всего этого собирают контейнер, jail и sandbox в разных ОС.
Модель угроз: от чего вообще защищаемся
Прежде чем изучать механизмы, стоит зафиксировать, какие границы бывают. Ошибка выбора механизма почти всегда — это ошибка в понимании границы.
доверия)) Пользователь vs пользователь uid/gid и mode-биты POSIX ACL квоты Процесс vs ядро кольца CPU валидация аргументов syscall SMEP / SMAP / KPTI Процесс vs сам себя ASLR, canary, NX RELRO, PIE, CFI pledge, Landlock, seccomp Сервис vs остальная система capabilities SELinux / AppArmor systemd sandboxing Арендатор vs арендатор namespaces + cgroups jails, zones гипервизор KVM/Xen Владелец железа vs ОС Secure Boot TPM, measured boot дисковое шифрование
Ключевая мысль, которую стоит усвоить сразу: чем ниже в этом списке граница, тем она прочнее и тем дороже стоит. Разделение по uid — почти бесплатно и защищает от случайности, а не от атакующего с локальным доступом. Гипервизор надёжен, но стоит десятков мегабайт памяти и заметной доли CPU. Контейнер находится ровно посередине и — это принципиально — не является границей безопасности сам по себе, о чём ниже будет отдельный разговор.
Дискреционный доступ: uid, gid и двенадцать бит
Классическая Unix-модель, придуманная в начале 1970-х и не изменившаяся с тех пор ни на йоту. Каждый процесс несёт набор идентификаторов, каждый объект — владельца, группу и биты прав. Это DAC (Discretionary Access Control): «дискреционный» означает, что владелец объекта сам решает, кому дать доступ.
У процесса не один uid, а целых четыре комплекта:
$ cat /proc/self/status | grep -E '^(Uid|Gid|Groups)'
Uid: 1000 1000 1000 1000
Gid: 1000 1000 1000 1000
Groups: 4 24 27 30 46 100 1000
Четыре числа в строке Uid — это, по порядку: real (кто запустил), effective (по кому проверяются права), saved set-uid (куда можно вернуться) и filesystem uid (Linux-специфичное расширение, историческое наследие NFS-сервера в ядре). Разделение real и effective существует ради одного механизма — setuid-битов.
Биты прав — это 12 бит, а не 9, как обычно рисуют:
$ ls -l /usr/bin/passwd /tmp /usr/bin/wall
-rwsr-xr-x 1 root root 68208 /usr/bin/passwd
drwxrwxrwt 18 root root 4096 /tmp
-rwxr-sr-x 1 root tty 3532 /usr/bin/wall
sв позиции владельца — setuid: приexecve()effective uid становится равен владельцу файла. Такpasswdпишет в/etc/shadowот имени root.sв позиции группы — setgid. На каталоге смысл другой: новые файлы наследуют группу каталога (базовый приём для общих директорий команды).t— sticky bit. На каталоге запрещает удалять чужие файлы, даже если у вас есть право записи в каталог. Без него любой пользователь мог бы удалить чужой файл в/tmp.
Права проверяются в порядке «владелец → группа → остальные», и проверка останавливается на первом совпадении. Это регулярный источник недоумения:
# файл, доступный всем, кроме владельца
$ chmod 077 secret.txt && ls -l secret.txt
----rwxrwx 1 alice alice 42 secret.txt
$ cat secret.txt # alice — владелец, её ветка даёт 0
cat: secret.txt: Permission denied
Alice не может прочитать свой файл, хотя «все остальные» могут. Ядро увидело, что uid совпал с владельцем, применило биты владельца и дальше не смотрело.
Когда девяти бит мало — есть POSIX ACL, реализованные поверх расширенных атрибутов:
$ setfacl -m u:deploy:rx /srv/app
$ getfacl /srv/app
# file: srv/app
# owner: root
# group: root
user::rwx
user:deploy:r-x
group::r-x
mask::r-x
other::---
$ ls -ld /srv/app
drwxr-xr-x+ 3 root root 4096 /srv/app # плюс в конце = есть ACL
Ловушка ACL — запись mask. Она ограничивает эффективные права всех именованных пользователей и групп, и chmod g+w меняет именно маску, а не групповые права. Классический сценарий: администратор выдал ACL, потом сделал chmod 750, и все именованные записи молча обрезались.
Почему DAC-модели недостаточно
Проблема в одном слове: root. В классическом Unix проверка выглядела буквально как if (euid == 0) return OK;. Одна учётная запись имеет все права на свете, и любой процесс, которому нужно одно маленькое привилегированное действие (например, забиндить порт 80 или открыть raw-сокет для ping), получает вместе с ним право стереть диск, загрузить модуль ядра и прочитать чужую память.
Второй недостаток — «дискреционность». Пользователь может отдать свои данные кому угодно, включая троян, работающий от его имени. DAC защищает пользователей друг от друга, но не защищает пользователя от его собственных программ.
Оба недостатка чинили отдельно и по-разному: первый — capabilities, второй — мандатный контроль.
Capabilities: root, разобранный на детали
POSIX capabilities (Linux, с версии 2.2, 1999) разбивают всемогущество root на отдельные, независимо выдаваемые права. В ядре 6.x их 41 штука (CAP_LAST_CAP == 40, см. /usr/include/linux/capability.h).
Самые ходовые:
| Capability | Что даёт | Типичный потребитель |
|---|---|---|
CAP_NET_BIND_SERVICE |
привязка к портам < 1024 | веб-сервер |
CAP_NET_RAW |
raw- и packet-сокеты | ping, tcpdump |
CAP_NET_ADMIN |
настройка интерфейсов, iptables | сетевые демоны, CNI |
CAP_DAC_OVERRIDE |
игнорировать проверку mode-битов | бэкап-агенты |
CAP_CHOWN |
менять владельца файла | распаковка архивов |
CAP_SETUID / CAP_SETGID |
произвольная смена uid/gid | login, контейнерные рантаймы |
CAP_SYS_PTRACE |
трассировка любых процессов | отладчики, профайлеры |
CAP_SYS_ADMIN |
≈ 30 разных вещей: mount, quota, namespaces… | «новый root» |
CAP_BPF, CAP_PERFMON |
eBPF и perf (выделены в 5.8) | observability-агенты |
CAP_SYS_ADMIN — известный провал дизайна. В неё сваливали всё, что не подходило под другие категории, и сегодня она эквивалентна root по последствиям. Если вы видите её в манифесте контейнера, изоляции по факту нет. Michael Kerrisk называл это «the new root» ещё в 2012 году в обзоре на LWN, и с тех пор лучше не стало.
Пять наборов и формула execve
Здесь начинается место, где ошибаются почти все. У потока не один набор capabilities, а пять, и при execve() они пересчитываются по вполне конкретной формуле.
Проверить фактическое состояние всегда можно через /proc:
$ grep ^Cap /proc/self/status
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 000001ffffffffff
CapAmb: 0000000000000000
$ capsh --decode=000001ffffffffff | head -3
0x000001ffffffffff=cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,
cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,...
Практический пример — сервис, который слушает 443-й порт, но не является root:
# было: запуск от root и потом drop privileges — сложно и легко ошибиться
# стало: одна capability на бинарнике
$ sudo setcap 'cap_net_bind_service=+ep' /usr/local/bin/myserver
$ getcap /usr/local/bin/myserver
/usr/local/bin/myserver cap_net_bind_service=ep
$ sudo -u nobody /usr/local/bin/myserver --port 443
listening on :443
Суффикс +ep означает: положить в permitted-набор файла и поднять бит effective, чтобы процессу не пришлось звать capset() самому.
Самая частая ловушка. В systemd-юните стоит NoNewPrivileges=yes (правильная практика!) — и файловые capabilities перестают работать вообще, потому что no_new_privs по определению запрещает execve() повышать привилегии. Решение — не setcap, а декларация в юните:
[Service]
ExecStart=/usr/local/bin/myserver --port 443
User=myapp
NoNewPrivileges=yes
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE
AmbientCapabilities работает потому, что systemd сам ставит capability в ambient-набор до execve(), а ambient переживает exec обычного (непривилегированного) файла — ровно ради этого сценария его и добавили в ядро 4.3.
Как правильно сбрасывать привилегии в C
Если демон всё же стартует от root, сброс должен быть необратимым. Порядок операций критичен: setgid строго до setuid, иначе после потери root вы уже не сможете сменить группу.
#define _GNU_SOURCE
#include <sys/capability.h>
#include <sys/prctl.h>
#include <grp.h>
#include <unistd.h>
#include <err.h>
/* Оставить только CAP_NET_BIND_SERVICE и уйти под непривилегированный uid. */
static void drop_to(uid_t uid, gid_t gid)
{
/* 1. Сохранить capabilities при смене uid — иначе ядро обнулит их. */
if (prctl(PR_SET_KEEPCAPS, 1, 0, 0, 0) < 0)
err(1, "PR_SET_KEEPCAPS");
/* 2. Сузить bounding set: это НЕОБРАТИМО, обратно вернуть нельзя. */
for (int cap = 0; cap <= CAP_LAST_CAP; cap++)
if (cap != CAP_NET_BIND_SERVICE)
prctl(PR_CAPBSET_DROP, cap, 0, 0, 0);
/* 3. Группы: сначала дополнительные, потом основная, только затем uid. */
if (setgroups(0, NULL) < 0) err(1, "setgroups");
if (setgid(gid) < 0) err(1, "setgid");
if (setuid(uid) < 0) err(1, "setuid");
/* 4. Проверка: убедиться, что вернуться в root уже нельзя. */
if (setuid(0) == 0)
errx(1, "ФАТАЛЬНО: сброс привилегий не сработал");
/* 5. Оставить в permitted/effective ровно одну capability. */
cap_t caps = cap_init();
cap_value_t keep[] = { CAP_NET_BIND_SERVICE };
cap_set_flag(caps, CAP_PERMITTED, 1, keep, CAP_SET);
cap_set_flag(caps, CAP_EFFECTIVE, 1, keep, CAP_SET);
if (cap_set_proc(caps) < 0) err(1, "cap_set_proc");
cap_free(caps);
/* 6. Запретить любое будущее повышение привилегий. */
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0)
err(1, "no_new_privs");
}
Шаг 4 не паранойя: исторические CVE в OpenSSH, Sendmail и Postfix возникали именно потому, что возвращаемое значение setuid() не проверялось, а вызов мог упасть, например, из-за RLIMIT_NPROC.
Порядок в целом выглядит так:
лишнего note right of Bounded необратимо: обратно даже root не вернёт end note Bounded --> Unprivileged: setgroups → setgid → setuid Unprivileged --> Verified: setuid(0) должен УПАСТЬ Verified --> Confined: no_new_privs + seccomp Confined --> Sandboxed: chroot/pivot_root,
Landlock, unveil Sandboxed --> Serving: обработка недоверенного ввода Serving --> Serving: запросы Serving --> [*]: exit Verified --> Compromised: пропущенная проверка Compromised --> Root: возврат привилегий = CVE
Мандатный доступ: LSM, SELinux и AppArmor
DAC чинит вопрос «кто», MAC (Mandatory Access Control) чинит вопрос «что вообще позволено этой программе, независимо от того, чего хочет её владелец». Политику пишет администратор системы, и root-процесс её нарушить не может.
Технически в Linux всё это построено на LSM — Linux Security Modules, каркасе из ~250 хуков, расставленных по ядру в местах принятия решений. LSM появился в 2001 году после того, как Линус отказался вмерживать SELinux напрямую и потребовал общий интерфейс (оригинальная статья USENIX 2002).
номер вызова?} B -- нет --> Z1["SIGSYS / ENOSYS"] B -- да --> C["VFS: path lookup"] C --> D{DAC: euid/egid
vs mode + ACL} D -- да --> F["LSM hook:
security_file_open"] D -- нет --> E{есть
CAP_DAC_OVERRIDE?} E -- нет --> Z2["EACCES"] E -- да --> F F --> G{SELinux: разрешает ли
политика пару
subj_t → obj_t : file} G -- нет --> Z3["EACCES + запись AVC
в audit.log"] G -- да --> H{AppArmor/Landlock/
BPF-LSM в стеке} H -- нет --> Z4["EACCES"] H -- да --> I["fd возвращён процессу"] style Z1 fill:#e0a0a0,stroke:#b05a5a style Z2 fill:#e0a0a0,stroke:#b05a5a style Z3 fill:#e0a0a0,stroke:#b05a5a style Z4 fill:#e0a0a0,stroke:#b05a5a style I fill:#8fbf8f,stroke:#4f8f4f
Та же картина в пространственном виде — с ошибками, которые вы увидите на каждом этапе:
Обратите внимание: LSM может только запретить, но не разрешить. Если DAC уже сказал «нет», SELinux не откроет файл. Это сознательное решение: MAC-модуль не должен уметь ослаблять базовую модель.
SELinux: type enforcement на практике
SELinux (АНБ, 2000; в mainline с 2.6) — самая мощная и самая недолюбливаемая реализация MAC. Её основа — type enforcement: у каждого субъекта и объекта есть контекст вида user:role:type:level, а политика — это гигантский список разрешённых троек «тип субъекта, тип объекта, класс операции».
$ ls -Z /var/www/html/index.html
unconfined_u:object_r:httpd_sys_content_t:s0 /var/www/html/index.html
$ ps -eZ | grep nginx
system_u:system_r:httpd_t:s0 1234 ? 00:00:01 nginx
Процесс nginx имеет тип httpd_t, файл — тип httpd_sys_content_t. В политике есть правило allow httpd_t httpd_sys_content_t:file { read getattr open }; — поэтому чтение работает. Записи нет — поэтому веб-сервер физически не может испортить контент, даже будучи скомпрометированным и работая от root.
Самый частый инцидент в жизни администратора выглядит так: файлы скопировали из домашнего каталога, они унаследовали тип user_home_t, и nginx возвращает 403 при chmod 644 и корректном владельце.
$ sudo ausearch -m AVC -ts recent | tail -4
type=AVC msg=audit(1721030400.123:456): avc: denied { read } for pid=1234
comm="nginx" name="index.html" dev="dm-0" ino=131074
scontext=system_u:system_r:httpd_t:s0
tcontext=unconfined_u:object_r:user_home_t:s0 tclass=file permissive=0
# лечение: вернуть корректный тип по правилам file contexts
$ sudo restorecon -Rv /var/www/html
Relabeled /var/www/html/index.html from unconfined_u:object_r:user_home_t:s0
to unconfined_u:object_r:httpd_sys_content_t:s0
Ключевой навык — уметь читать AVC-запись: scontext (кто), tcontext (к чему), tclass (какого класса объект), { read } (что делал). Этих четырёх полей хватает, чтобы понять причину в 90 % случаев.
Многое настраивается булевыми переключателями без правки политики:
$ getsebool -a | grep httpd_can_network
httpd_can_network_connect --> off
httpd_can_network_connect_db --> off
$ sudo setsebool -P httpd_can_network_connect on # -P = persist через ребут
Антипаттерн, который надо назвать вслух: setenforce 0 в качестве «решения». Это выключает защиту на всей машине. Правильно — либо булев переключатель, либо semanage fcontext для нестандартных путей, либо, в крайнем случае, точечный модуль:
# audit2allow генерирует модуль по конкретному отказу — читать сгенерированное ОБЯЗАТЕЛЬНО,
# он с радостью выдаст вам разрешение на всё, если отказов было много
$ sudo ausearch -m AVC -ts recent | audit2allow -M myapp_fix
$ cat myapp_fix.te
$ sudo semodule -i myapp_fix.pp
AppArmor: то же самое, но по путям
AppArmor (Immunix, затем SUSE; сегодня дефолт в Ubuntu и Debian) вместо меток на inode использует пути в профилях. Это заметно проще для человека и заметно слабее по гарантиям: жёсткая ссылка, bind-mount или переименование могут вывести файл из-под правила, тогда как метка SELinux живёт на самом inode.
# /etc/apparmor.d/usr.local.bin.myapp
#include <tunables/global>
/usr/local/bin/myapp {
#include <abstractions/base>
#include <abstractions/nameservice>
capability net_bind_service,
/usr/local/bin/myapp mr,
/etc/myapp/** r,
/var/lib/myapp/** rw,
/var/log/myapp/*.log w,
owner /run/myapp.pid rw,
network inet stream,
deny /etc/shadow rwx,
deny /home/** rwx,
}
$ sudo apparmor_parser -r /etc/apparmor.d/usr.local.bin.myapp
$ sudo aa-status | head -5
apparmor module is loaded.
41 profiles are loaded.
38 profiles are in enforce mode.
3 profiles are in complain mode.
# отладочный режим: не запрещать, но логировать всё, что запретил бы
$ sudo aa-complain /usr/local/bin/myapp
Практический выбор: если у вас RHEL/Fedora/CentOS Stream — SELinux, он уже включён и вся экосистема под него настроена. Если Ubuntu/Debian/SUSE — AppArmor. Драться с дистрибутивом бессмысленно.
Landlock: MAC для тех, у кого нет root
С ядра 5.13 (2021) есть Landlock — LSM, которым может пользоваться непривилегированный процесс, чтобы ограничить сам себя. Это прямой ответ на OpenBSD-шный unveil, и это самый практичный для прикладного разработчика механизм из всего семейства MAC: не нужен ни root, ни политика в системе, ни согласование с админом.
/* Разрешить процессу читать только /usr и писать только в /var/lib/myapp. */
struct landlock_ruleset_attr ra = {
.handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_WRITE_FILE |
LANDLOCK_ACCESS_FS_MAKE_REG,
};
int rs = syscall(SYS_landlock_create_ruleset, &ra, sizeof(ra), 0);
struct landlock_path_beneath_attr pb = {
.allowed_access = LANDLOCK_ACCESS_FS_READ_FILE,
.parent_fd = open("/usr", O_PATH | O_CLOEXEC),
};
syscall(SYS_landlock_add_rule, rs, LANDLOCK_RULE_PATH_BENEATH, &pb, 0);
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); /* обязательное условие */
syscall(SYS_landlock_restrict_self, rs, 0); /* с этого момента — необратимо */
Документация и версии ABI — на landlock.io и в Documentation/userspace-api/landlock.
seccomp: фильтр на входе в ядро
Все предыдущие механизмы отвечают на вопрос «можно ли этому субъекту трогать этот объект». seccomp отвечает на другой: «можно ли этому процессу вообще звать этот системный вызов». Это сокращение attack surface: в ядре Linux ~350 системных вызовов, типичному приложению нужно 40–60, а уязвимости регулярно находят именно в редко используемых.
Режим SECCOMP_MODE_STRICT (2005) разрешал ровно четыре вызова и был почти бесполезен. Практический режим — SECCOMP_MODE_FILTER (3.5, 2012): вы загружаете cBPF-программу, которая на каждый syscall видит его номер, архитектуру и шесть скалярных аргументов, и возвращает вердикт.
#include <linux/filter.h>
#include <linux/seccomp.h>
#include <linux/audit.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
/* Разрешить только read/write/exit_group/rt_sigreturn, остальное — убить процесс. */
static struct sock_filter filter[] = {
/* Проверка архитектуры обязательна: без неё 32-битный ABI обходит фильтр. */
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, arch)),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 1, 0),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS),
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_read, 4, 0),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_write, 3, 0),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_exit_group, 2, 0),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_rt_sigreturn, 1, 0),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
};
static struct sock_fprog prog = {
.len = sizeof(filter) / sizeof(filter[0]),
.filter = filter,
};
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); /* иначе seccomp откажет */
syscall(SYS_seccomp, SECCOMP_SET_MODE_FILTER, 0, &prog);
Писать cBPF руками мучительно — в проде используют libseccomp, которая компилирует декларативные правила в тот же фильтр.
Важные детали, которые определяют пригодность механизма:
- Фильтр видит только скаляры. Строку по указателю прочитать нельзя — это была бы TOCTOU-гонка: другой поток изменит память между проверкой и использованием. Поэтому «запретить
open()только для/etc/shadow» через seccomp невозможно; это работа для LSM. - Фильтры складываются и необратимы. Каждый новый фильтр применяется дополнительно к существующим, снять нельзя.
- Вердикты:
KILL_PROCESS,KILL_THREAD,TRAP(SIGSYS),ERRNO(n)(вернуть код ошибки),USER_NOTIF(передать решение в userspace-супервизор — ядро 5.0, основа для реализации syscall’ов в контейнерных рантаймах),LOG,ALLOW. ERRNO(ENOSYS)— вежливый вариант. Программы обычно умеют деградировать, если вызов «не поддержан», но падают от SIGSYS. Docker по умолчанию отдаётEPERMдля ~44 из 350+ вызовов (профиль по умолчанию).
Диагностика: если strace показывает загадочный ENOSYS на очевидно существующем вызове или процесс умирает от SIGSYS, первым делом смотрите:
$ grep Seccomp /proc/1234/status
Seccomp: 2 # 0=выкл, 1=strict, 2=filter
Seccomp_filters: 3
Namespaces: изоляция представлений
До сих пор речь шла о запретах. Namespaces работают иначе: они не запрещают доступ к объекту, а делают объект несуществующим с точки зрения процесса. Процесс не получает EPERM — он просто не видит, что там что-то есть. Это принципиально более чистая модель: нельзя атаковать то, чего нет в вашей вселенной имён.
В Linux их восемь:
| Namespace | Что изолирует | Появился | Флаг clone() |
|---|---|---|---|
mnt |
таблицу монтирований | 2.4.19 (2002) | CLONE_NEWNS |
uts |
hostname и domainname | 2.6.19 | CLONE_NEWUTS |
ipc |
System V IPC, POSIX-очереди | 2.6.19 | CLONE_NEWIPC |
pid |
пространство PID | 2.6.24 | CLONE_NEWPID |
net |
интерфейсы, порты, таблицы маршрутов, iptables | 2.6.29 | CLONE_NEWNET |
user |
отображение uid/gid и capabilities | 3.8 (2013) | CLONE_NEWUSER |
cgroup |
видимую корневую точку иерархии cgroup | 4.6 | CLONE_NEWCGROUP |
time |
CLOCK_MONOTONIC и CLOCK_BOOTTIME |
5.6 | CLONE_NEWTIME |
Каждый namespace — это объект в ядре, на который ссылаются через файлы в /proc:
$ ls -l /proc/self/ns/
lrwxrwxrwx 1 m m 0 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 m m 0 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 m m 0 mnt -> 'mnt:[4026531841]'
lrwxrwxrwx 1 m m 0 net -> 'net:[4026531840]'
lrwxrwxrwx 1 m m 0 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 m m 0 user -> 'user:[4026531837]'
lrwxrwxrwx 1 m m 0 uts -> 'uts:[4026531838]'
Числа в скобках — inode-номера. Два процесса в одном namespace имеют одинаковый номер; это самый быстрый способ проверить, действительно ли изоляция есть:
$ readlink /proc/self/ns/net /proc/$(pgrep -f nginx | head -1)/ns/net
net:[4026531840]
net:[4026532296] # разные — nginx в своём сетевом namespace
Три системных вызова управляют всем: clone() с флагами (создать процесс сразу в новых), unshare() (перевести себя в новые), setns() (войти в существующий по файловому дескриптору — так работает nsenter и docker exec).
Практика: собрать «контейнер» голыми руками
# Сетевой namespace: полностью отдельный стек
$ sudo ip netns add sandbox
$ sudo ip netns exec sandbox ip link show
1: lo: <LOOPBACK> mtu 65536 state DOWN # только loopback, и тот опущен
$ sudo ip netns exec sandbox ss -tlnp # портов не видно вообще
# PID namespace: наш процесс становится PID 1
$ sudo unshare --pid --fork --mount-proc bash
# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 12144 3452 pts/2 S 14:22 0:00 bash
root 12 0.0 0.0 14024 3320 pts/2 R+ 14:22 0:00 ps aux
--mount-proc здесь обязателен: без него /proc останется смонтированным из родительского namespace и ps покажет все процессы системы. Классическая ошибка «PID namespace не работает» — это почти всегда забытый remount /proc.
User namespace — тот, что меняет всё
CLONE_NEWUSER — единственный namespace, который непривилегированный пользователь может создать сам. Внутри него процесс получает полный набор capabilities, но только по отношению к объектам, созданным внутри этого же namespace.
$ id -u
1000
$ unshare --user --map-root-user bash
# id -u
0
# cat /proc/self/uid_map
0 1000 1
# touch /etc/test
touch: cannot touch '/etc/test': Permission denied
Мы «стали root», но /etc принадлежит настоящему uid 0 из родительского namespace, а наш «root» отображён на uid 1000 — доступа нет. Именно так работают rootless-контейнеры (Podman, Buildah, rootless Docker): диапазоны uid берутся из /etc/subuid, а маппинг настраивает setuid-хелпер newuidmap:
$ grep $USER /etc/subuid /etc/subgid
/etc/subuid:m:100000:65536
/etc/subgid:m:100000:65536
$ podman unshare cat /proc/self/uid_map
0 1000 1
1 100000 65536
Trade-off, о котором надо знать честно. User namespaces радикально расширили поверхность атаки на ядро: непривилегированный пользователь получил возможность дёргать код, который раньше был доступен только root (монтирование, netfilter, USB). Значительная доля Linux LPE-эксплойтов последних лет начинается со строчки unshare(CLONE_NEWUSER|CLONE_NEWNET). Поэтому Debian и Ubuntu исторически держали переключатель:
# Ubuntu ≥ 23.10: ограничение через AppArmor
$ sysctl kernel.apparmor_restrict_unprivileged_userns
kernel.apparmor_restrict_unprivileged_userns = 1
# RHEL и старые Debian
$ sysctl user.max_user_namespaces kernel.unprivileged_userns_clone
Решение — компромисс: выключите — сломаете Podman, Flatpak, Chrome-sandbox и systemd-юниты с PrivateUsers=; оставите — примете риск. В многопользовательских системах и на хостах CI обычно ограничивают.
cgroups: изоляция ресурсов, а не прав
Namespaces отвечают на «что я вижу», cgroups — на «сколько я могу съесть». Это отдельная ось, и путать их не надо: cgroups не мешают процессу читать чужие файлы, зато мешают ему выесть всю память и уронить хост. Формально это защита не от кражи данных, а от отказа в обслуживании, — но в проде именно она спасает чаще.
cgroup v2 (единая иерархия, стабильна с 4.5, дефолт почти везде с 2021) смонтирована в /sys/fs/cgroup:
$ mount | grep cgroup2
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate)
$ cat /sys/fs/cgroup/cgroup.controllers
cpuset cpu io memory hugetlb pids rdma misc
Создание группы с лимитами — это буквально работа с файлами:
$ sudo mkdir /sys/fs/cgroup/myapp
# делегировать контроллеры вниз: без этого файлы memory.max в дочерней не появятся
$ echo "+memory +pids +cpu" | sudo tee /sys/fs/cgroup/cgroup.subtree_control
$ echo "512M" | sudo tee /sys/fs/cgroup/myapp/memory.max
$ echo "400M" | sudo tee /sys/fs/cgroup/myapp/memory.high # мягкий порог: throttle
$ echo "100" | sudo tee /sys/fs/cgroup/myapp/pids.max # анти-forkbomb
$ echo "50000 100000" | sudo tee /sys/fs/cgroup/myapp/cpu.max # 0.5 ядра
$ echo $$ | sudo tee /sys/fs/cgroup/myapp/cgroup.procs # поместить shell сюда
Разница memory.max и memory.high — практически важная. max — жёсткий предел: при превышении срабатывает cgroup-OOM-killer. high — мягкий: ядро начинает агрессивно вытеснять страницы и притормаживать аллокации, но процесс не убивает. Для сервисов с пиками правильная конфигурация — high заметно ниже max.
Наблюдение за давлением (PSI, ядро 4.20) — лучший сигнал для автоскейлинга, чем средняя загрузка:
$ cat /sys/fs/cgroup/myapp/memory.pressure
some avg10=12.43 avg60=8.21 avg300=2.11 total=1832919
full avg10=3.11 avg60=1.02 avg300=0.31 total=402118
$ cat /sys/fs/cgroup/myapp/memory.events
low 0
high 1204 # сколько раз упирались в мягкий порог
max 3
oom 0
oom_kill 0
Два правила cgroup v2, на которых спотыкаются:
- No internal processes. Процессы могут жить только в листовых узлах иерархии (кроме корня). Нельзя одновременно держать процессы в
/sys/fs/cgroup/myappи включать там контроллеры для дочерних групп. - Делегирование через
cgroup.subtree_control. Родитель явно перечисляет, какие контроллеры доступны детям. Ребёнок не может включить то, чего не дал родитель.
В реальности вручную с этим почти не работают: systemd управляет всей иерархией, и правильный способ — свойства юнита (MemoryMax=512M, CPUQuota=50%, TasksMax=100) либо systemd-run --scope -p MemoryMax=1G ... для разовых задач.
Как из этого собирают контейнер
Контейнер — не сущность ядра. Это соглашение о том, как одновременно применить namespaces, cgroups, capabilities, seccomp, LSM и pivot_root. Вот что делает runc при старте контейнера:
потенциальный побег
Порядок здесь не декоративный. seccomp применяется предпоследним, потому что сам runc до execve нуждается в вызовах, которые фильтр запретит. no_new_privs ставится до seccomp, потому что это условие ядра для непривилегированной загрузки фильтра. pivot_root предпочтительнее chroot, потому что chroot тривиально обходится (классический трюк с открытым fd на каталог).
Контейнер — не граница безопасности
Это надо повторить прямым текстом, потому что архитектурные решения строят на обратном предположении. Все контейнеры на хосте делят одно ядро. Поверхность атаки — ~350 системных вызовов плюс весь код драйверов, доступный через /proc, /sys и ioctl. Одна ошибка в ядре — и изоляция снимается целиком:
- CVE-2019-5736 — перезапись бинарника
runcна хосте через/proc/self/exeизнутри контейнера. - CVE-2022-0847 (Dirty Pipe) — запись в произвольные страницы page cache, включая read-only bind-mount из хоста.
- CVE-2024-21626 (Leaky Vessels) — утечка файлового дескриптора рабочего каталога наружу из контейнера.
Следствия для архитектуры:
- Не размещайте недоверенный код разных арендаторов в контейнерах на одном хосте. Для мультитенантности — виртуальные машины (Firecracker, Kata Containers) или перехват syscall’ов в userspace (gVisor, где ядро Linux частично переписано на Go и работает в пользовательском пространстве).
--privilegedотключает почти всё описанное выше разом. Он эквивалентен root на хосте, точка.- Монтирование
/var/run/docker.sockвнутрь контейнера — тоже root на хосте: имея доступ к сокету, можно запустить привилегированный контейнер с примонтированным/. - Не запускайте процессы внутри контейнера от uid 0:
USER appв Dockerfile плюсrunAsNonRoot: trueв Kubernetes.
Подробнее о спектре «контейнер — микро-VM — гипервизор» — в статье Виртуализация и контейнеры.
BSD: jails и Capsicum
FreeBSD пришла к изоляции раньше и другим путём. Jail (FreeBSD 4.0, 2000, статья Poul-Henning Kamp и Robert Watson) — это не композиция из восьми механизмов, а один системный вызов и один целостный концепт: «поддерево ФС + набор адресов + собственный root, чьи привилегии ядро урезает по единому набору правил».
# /etc/jail.conf
web {
path = "/jails/web";
host.hostname = "web.example.com";
vnet; # собственный сетевой стек целиком
vnet.interface = "epair0b";
allow.raw_sockets = 0;
allow.mount = 0;
allow.sysvipc = 0;
exec.start = "/bin/sh /etc/rc";
exec.stop = "/bin/sh /etc/rc.shutdown";
persist;
}
# freebsd
$ sudo service jail start web
$ jls
JID IP Address Hostname Path
1 - web.example.com /jails/web
$ jexec 1 /bin/sh
Отличия от Linux-контейнеров, важные при выборе платформы:
- Целостность модели. В Linux забыть один флаг = дыра. В FreeBSD jail — это одна сущность с одним набором правил
allow.*; трудно случайно оставить половину изоляции. - VNET даёт полноценный отдельный сетевой стек, включая свои
pf-правила и таблицы маршрутизации, — аналогCLONE_NEWNET, но появился раньше и интегрирован глубже. - Иерархия jail-в-jail поддерживается штатно (
children.max). - ZFS-датасеты делегируются в jail (
jailed=on), что даёт арендатору собственные снапшоты — комбинация, которой в Linux до сих пор нет из коробки. - Нет образов и реестра. Экосистемы уровня OCI и Kubernetes у jails нет, и это главная практическая причина, по которой индустрия ушла в Linux.
Capsicum (FreeBSD 9.0, 2012; статья USENIX Security 2010) — вторая идея FreeBSD, ортогональная jails: capability-модель на файловых дескрипторах. Процесс входит в «capability mode» и после этого теряет доступ ко всему глобальному пространству имён — открыть файл по пути нельзя вообще, работать можно только с уже открытыми fd и производными от них.
#include <sys/capsicum.h>
int fd = open("/var/data/input.bin", O_RDONLY);
cap_rights_t rights;
cap_rights_init(&rights, CAP_READ, CAP_FSTAT); /* только чтение по этому fd */
cap_rights_limit(fd, &rights);
if (cap_enter() < 0) err(1, "cap_enter"); /* необратимо */
/* Отсюда open("/etc/passwd") вернёт ECAPMODE. */
Это строже seccomp: ограничение выражено не в терминах «какие вызовы», а в терминах «к каким объектам», и не имеет проблемы TOCTOU с путями.
OpenBSD пошёл третьим путём — минимализм и удобство для программиста. pledge(2) (5.9, 2015) объявляет, какими классами функциональности программа собирается пользоваться дальше:
#include <unistd.h>
/* Фаза старта: читаем конфиг, открываем сокет, пишем pid-файл. */
if (pledge("stdio rpath wpath cpath inet unix dns proc exec", NULL) == -1)
err(1, "pledge");
setup_listeners();
read_config();
/* После инициализации сужаемся до минимума — обратно расширить нельзя. */
if (pledge("stdio inet", NULL) == -1)
err(1, "pledge");
/* Любая попытка open() отсюда убьёт процесс сигналом SIGABRT. */
unveil(2) (6.4, 2018) дополняет его файловой частью:
unveil("/etc/myapp", "r"); /* только чтение */
unveil("/var/log/myapp", "rw"); /* чтение и запись */
unveil(NULL, NULL); /* зафиксировать список — дальше ничего не добавить */
Сравните объём кода: seccomp-фильтр выше занимает 20 строк BPF и требует библиотеки, pledge — одна строка, читаемая вслух. Именно поэтому в OpenBSD pledge реально применён почти во всей базовой системе, а в Linux seccomp-профили пишут единицы. Дизайн, которым пользуются, побеждает дизайн, который мощнее. Подробнее о философии семейства — в статье Семейство BSD.
Solaris Zones (2005) заслуживают упоминания как исторически самая зрелая реализация: zones плюс RBAC с ролями и профилями плюс privileges (аналог capabilities, но детальнее — их около 80) плюс интеграция с ZFS и Crossbow. Многое из того, что Linux собирал по кускам десять лет, там было единой архитектурой в 2005-м; наследует это illumos.
macOS и Windows: другие модели
XNU
macOS унаследовал от TrustedBSD ту же MAC-инфраструктуру, что дала Linux идею LSM, и построил поверх несколько слоёв:
- Sandbox (исторически Seatbelt) — профили на диалекте Scheme в
/System/Library/Sandbox/Profiles/, применяемые черезsandbox_init(3). Всё в App Store работает под App Sandbox обязательно. - Entitlements — подписанные ключи в бинарнике, разрешающие конкретные возможности. Посмотреть:
codesign -d --entitlements :- /Applications/Safari.app. - SIP (System Integrity Protection), с 10.11 — ограничивает даже root:
/System,/usr(кроме/usr/local) и/binнеприкосновенны, а критичные процессы нельзя трассировать. Управление — только из recovery черезcsrutil. Это признание того же факта, что и capabilities: всемогущий root — плохая идея. - TCC (Transparency, Consent, Control) — те самые диалоги «приложение хочет доступ к камере/микрофону/Документам». Состояние — в SQLite-базе
~/Library/Application Support/com.apple.TCC/TCC.db, читать напрямую нельзя (защищено самим TCC). - Signed System Volume с 11.0 — корневая ФС смонтирована read-only и криптографически верифицируется целиком через дерево хешей.
Windows NT
Модель NT спроектирована с нуля в 1993-м и с Unix почти не пересекается — она изначально ACL-ориентированная:
- SID (Security Identifier) вместо числового uid:
S-1-5-21-…-1001. - Access token — прикреплён к процессу, содержит SID пользователя, SID групп, список привилегий (
SeDebugPrivilege,SeBackupPrivilege— прямой аналог capabilities, появившийся на 6 лет раньше) и уровень целостности. - Security descriptor на каждом объекте (не только на файлах — на процессах, мьютексах, ключах реестра, каналах) с DACL из ACE-записей allow/deny и SACL для аудита. Это заметно выразительнее 12 бит Unix.
- Mandatory Integrity Control (Vista) — уровни Low/Medium/High/System. Процесс не может писать в объект более высокого уровня. Вкладки Internet Explorer и позже браузерные рендереры работают на Low.
- UAC — администратор получает два токена: обычный и с фильтрацией; повышение переключает на полный.
- AppContainer (Windows 8) — изоляция по SID-возможностям, база для UWP и для sandbox современных браузеров.
- Job objects — примерный аналог cgroups: лимиты памяти, CPU, числа процессов.
Практический вывод для кроссплатформенного кода: не пытайтесь эмулировать chmod на Windows. Модели не изоморфны — Unix-права выражаются через ACL, но не наоборот. Подробнее — Другие ОС.
Защита внутри процесса
Всё описанное защищает систему от процесса. Отдельный слой защищает процесс от собственных ошибок памяти — эти механизмы включаются флагами компилятора, и их регулярно теряют при сборке.
$ checksec --file=/usr/local/bin/myapp
RELRO STACK CANARY NX PIE RPATH FORTIFY
Full RELRO Canary found NX on PIE No RPATH Yes
- ASLR — рандомизация адресов. Проверить:
sysctl kernel.randomize_va_space(должно быть2). Работает для исполняемого файла только если тот собран как PIE (-fPIE -pie); иначе базовый адрес фиксирован и ROP-цепочки строятся тривиально. - NX / W^X — страница либо исполняемая, либо записываемая. OpenBSD обеспечивает это жёстче всех, вплоть до запрета
mprotect(PROT_WRITE|PROT_EXEC)черезW^X-политику на уровне ФС. - Stack canary —
-fstack-protector-strong, разумный дефолт. - RELRO —
-Wl,-z,relro,-z,nowделает GOT read-only после старта, закрывая перезапись указателей на функции. - FORTIFY_SOURCE —
-D_FORTIFY_SOURCE=3(glibc 2.35+) добавляет проверки размеров вmemcpy,sprintfи родственные, когда размер известен компилятору. - CFI —
clang -fsanitize=cfiс LTO; в ядре LinuxCONFIG_CFI_CLANGвключён по умолчанию на arm64 в Android с 2021 года.
Со стороны ядра: SMEP/SMAP (ядро не может исполнять код и читать данные пользовательских страниц), KPTI (разделение таблиц страниц как обход Meltdown, ценой ~5–30 % на syscall-интенсивных нагрузках), lockdown-режим (запрет способов записи в память ядра даже для root при Secure Boot). Про механику страниц и таблиц — в статье Управление памятью.
Собираем всё вместе: hardening одного systemd-юнита
Теория становится практикой в одном файле. systemd — это, помимо прочего, готовый фронтенд ко всем механизмам этой статьи.
[Unit]
Description=Myapp API
After=network.target
[Service]
ExecStart=/usr/local/bin/myapp
User=myapp
Group=myapp
# --- привилегии ---
NoNewPrivileges=yes
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE
# --- файловая система (mount namespace) ---
ProtectSystem=strict # весь / только на чтение
ProtectHome=yes # /home, /root, /run/user не видны
PrivateTmp=yes # собственный /tmp
ReadWritePaths=/var/lib/myapp
StateDirectory=myapp
# --- ядро и устройства ---
PrivateDevices=yes # только /dev/null, zero, random, tty
ProtectKernelTunables=yes # /proc/sys, /sys только на чтение
ProtectKernelModules=yes # запрет загрузки модулей
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectProc=invisible # чужие процессы не видны в /proc
RestrictNamespaces=yes # нельзя создавать namespaces
LockPersonality=yes
# --- память и вызовы ---
MemoryDenyWriteExecute=yes # ни одной W+X страницы (несовместимо с JIT!)
SystemCallFilter=@system-service
SystemCallFilter=~@privileged @resources
SystemCallArchitectures=native
RestrictRealtime=yes
RestrictSUIDSGID=yes
# --- сеть ---
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
IPAddressDeny=any
IPAddressAllow=10.0.0.0/8 127.0.0.1
# --- ресурсы (cgroup v2) ---
MemoryMax=512M
MemoryHigh=400M
CPUQuota=200%
TasksMax=256
[Install]
WantedBy=multi-user.target
У systemd есть встроенный аудитор, который оценивает юнит и объясняет каждый пункт:
$ systemd-analyze security myapp.service
NAME DESCRIPTION EXPOSURE
✓ PrivateNetwork= Service has access to the host's network 0.5
✗ User=/DynamicUser= Service runs under a static non-root user 0.3
✓ CapabilityBoundingSet=~CAP_SYS_ADMIN Service has no administrator... 0.0
...
→ Overall exposure level for myapp.service: 2.1 OK 🙂
Практическое замечание: MemoryDenyWriteExecute=yes ломает любой JIT — Node.js, JVM, PyPy, .NET, современные браузерные движки. RestrictNamespaces=yes ломает всё, что запускает контейнеры или использует sandbox. Включайте по одному и проверяйте, а не копируйте блок целиком.
Типичные заблуждения
«Мы запускаем контейнеры, значит изолированы». Контейнер защищает от случайного взаимовлияния, а не от целенаправленной атаки. Общее ядро — общая судьба.
«root внутри контейнера — это же не настоящий root». Без user namespace это ровно uid 0 хоста, просто с урезанным набором capabilities. Проброс тома, ошибка в рантайме, --privileged — и он становится настоящим.
«chmod 777 решит проблему прав». Если мешает SELinux, не решит: getenforce, ausearch -m AVC. Если мешает namespace — тоже не решит, потому что дело не в правах. И в обоих случаях 777 — это отдельная новая уязвимость.
«setuid-бинарник безопасен, если код корректный». Ядро при setuid-exec переходит в secure-execution mode (AT_SECURE=1), сбрасывает LD_PRELOAD и LD_LIBRARY_PATH, но остаётся всё остальное окружение, дескрипторы, ulimits, локали. PwnKit (CVE-2021-4034 в pkexec) — эксплуатация окружения при пустом argv. Не пишите новые setuid-бинарники; используйте capabilities или сервис, доступный через unix-сокет.
«seccomp запретит доступ к файлу». Не запретит: фильтр не читает память по указателю. Пути — это LSM, Landlock или unveil.
«cgroups — это безопасность». Это защита от DoS. Данные они не защищают.
«у нас нет root-процессов, значит capabilities не нужны». Нужны в обратную сторону: CapabilityBoundingSet= в юните гарантирует, что процесс не поднимет привилегии даже при успешной эксплуатации setuid-бинарника или ошибки в рантайме.
«ASLR всё закроет». Без PIE рандомизации основного образа нет; при утечке одного адреса вся рандомизация обесценивается; на 32 битах энтропии слишком мало для реального сопротивления брутфорсу.
Мини-итог
Уровни защиты складываются, а не заменяют друг друга, и каждый закрывает свой класс угроз:
| Механизм | Отвечает на вопрос | Обходится, если |
|---|---|---|
| DAC (mode, ACL) | кто владеет объектом | атакующий получил нужный uid |
| capabilities | какие привилегированные действия можно | выдан CAP_SYS_ADMIN |
| MAC (SELinux/AppArmor) | что позволено этой программе | политика в permissive или слишком широка |
| seccomp | какие syscall’ы доступны | нужный вызов не отфильтрован |
| namespaces | что процесс вообще видит | уязвимость в ядре, проброс из хоста |
| cgroups | сколько ресурсов можно съесть | лимит не выставлен |
| гипервизор | доступ к железу | побег из VM (редко и дорого) |
Практический минимум для любого сервиса: не root; NoNewPrivileges=yes; пустой или минимальный CapabilityBoundingSet; read-only корень с явным списком записываемых путей; лимиты памяти и задач; MAC-профиль в enforce, а не permissive. Это десять строк в юните и почти нулевая стоимость — и они закрывают подавляющее большинство реальных сценариев эскалации.
Связанные темы трека: механика перехода в ядро и стоимость проверок — Системные вызовы и IPC; архитектурная причина существования границы «ядро/пользователь» — Архитектуры ядер; наблюдение за отказами и профилирование — Наблюдаемость и производительность.
Источники
Книги:
- Michael Kerrisk. The Linux Programming Interface — главы 9, 38, 39 про uid/gid, setuid и capabilities остаются лучшим изложением на русском и английском.
- Robert Love. Linux Kernel Development, 3rd ed. — обзор LSM и модели прав в ядре.
- Marshall Kirk McKusick et al. The Design and Implementation of the FreeBSD Operating System, 2nd ed. — jails, Capsicum, MAC framework из первых рук.
- Pavel Yosifovich, Mark Russinovich et al. Windows Internals, part 1, 7th ed. — глава «Security» про токены, ACL и integrity levels.
- Jonathan Levin. MacOS and iOS Internals, Volume III: Security & Insecurity — sandbox, entitlements, AMFI.
Статьи и спецификации:
- Chris Wright et al. «Linux Security Modules: General Security Support for the Linux Kernel», USENIX Security 2002.
- Robert Watson et al. «Capsicum: practical capabilities for UNIX», USENIX Security 2010.
- Poul-Henning Kamp, Robert Watson. «Jails: Confining the omnipotent root», SANE 2000.
- Jerome Saltzer, Michael Schroeder. «The Protection of Information in Computer Systems», 1975 — источник принципов least privilege и fail-safe defaults; всё выше — их реализации.
- Theo de Raadt. «pledge and unveil in OpenBSD», доклады BSDCan.
Документация и man-страницы:
man 7 capabilities,man 7 namespaces,man 7 user_namespaces,man 2 seccomp,man 2 unshare,man 2 setns,man 7 cgroups,man 5 systemd.exec,man 5 systemd.resource-control.man 8 jail,man 2 jail,man 4 capsicum,man 2 pledge,man 2 unveil— BSD-семейство.- Documentation/admin-guide/LSM и cgroup-v2 в дереве ядра.
- SELinux Notebook — исчерпывающее руководство по политике.
- AppArmor wiki, Landlock, gVisor.
- LWN: «Namespaces in operation» — серия из семи частей Майкла Керриска, лучшее введение в namespaces; «Ambient capabilities».
Что дальше
Мы разбирали защиту работающей системы, но всё это опирается на предположение, что само ядро и initramfs загрузились нескомпрометированными. Пора посмотреть, что происходит до первого процесса: как прошивка находит загрузчик, чем UEFI отличается от BIOS, зачем нужен initramfs и как Secure Boot связывает цепочку доверия от чипа до systemd.
Следующая статья: Загрузка системы: BIOS и UEFI, MBR/GPT, GRUB, systemd-boot, initramfs.