Перейти к содержанию
Learning Platform
Глоссарий Troubleshooting
Урок 12.04 · 22 мин
Средний
CapabilitiesNamespacesLinuxSecurityContainers

Capabilities и namespaces — разрезаем ‘всемогущество root’ на кусочки

Старый Unix имел бинарное разделение: «либо ты root и можешь всё, либо ты не root и не можешь почти ничего привилегированного». Это плохо работает в современной инфраструктуре. Программе ping нужно открывать raw socket — но зачем ей право монтировать файловые системы? Сервису-биндеру 80 порта нужен только bind privileged port — зачем ему доступ ко всему сразу?

Linux capabilities — ответ: разбить полномочия root на ~40 независимых способностей. Программа может получить CAP_NET_RAW (raw socket) и больше ничего — никакого root, никакой опасности. А namespaces — это другой механизм изоляции: даёт процессу свой взгляд на pids, mounts, network, users. На их сочетании построены Docker, Kubernetes, systemd-nspawn — все современные контейнеры.


Зачем разрезать root на capabilities

Стандартный пример: /usr/bin/ping. Чтобы отправить ICMP-пакет, нужен raw socket — это привилегированная операция. Исторически решение было: сделать ping setuid root. Запустил ping — EUID=0, можешь всё.

Проблема: если в ping есть buffer overflow, атакующий получает root. А что нужно ping’у? Только одна возможность — открыть raw socket. Всё остальное (read /etc/shadow, kill процессы, mount FS) ему не нужно. Setuid root даёт всё, а нужно лишь капельку.

Capabilities решают это:

ls -l /usr/bin/ping
# -rwxr-xr-x 1 root root 76712 May 18 12:00 /usr/bin/ping
# Заметили: НЕТ setuid (s в правах). Просто rwxr-xr-x.

getcap /usr/bin/ping
# /usr/bin/ping cap_net_raw=ep

Здесь cap_net_raw=ep — ping имеет только одну capability (CAP_NET_RAW), и только она нужна. Атакующий через уязвимость в ping получит… возможность отправлять raw сокеты. И всё. Не root.

Setuid root vs Capabilities -- что получает атакующий при компрометации
Setuid root pingСтарый подход: ping запускается как root полностью. Уязвимость даёт атакующему все права root
exploit
Full root!Может всё: чтение /etc/shadow, modify system files, install rootkit, kill any process
ping с CAP_NET_RAWСовременный подход: ping не setuid, но имеет одну capability. Уязвимость ограничена только этой способностью
exploit
Только raw socketМожет только отправлять/получать raw сокеты. Не root, не может читать /etc/shadow или менять систему

Список основных capabilities

Linux 6+ имеет ~40 capabilities. Из часто встречающихся:

CapabilityЧто даёт
CAP_NET_BIND_SERVICEBind на privileged ports (порт меньше 1024)
CAP_NET_RAWОткрывать raw и packet сокеты (ping, tcpdump)
CAP_NET_ADMINНастраивать network: routes, iptables, interfaces
CAP_SYS_ADMINОгромный набор: mount, swap, namespaces — «маленький root»
CAP_SYS_PTRACEptrace других процессов
CAP_SYS_TIMEМенять системное время
CAP_SYS_NICEМенять nice/priority других процессов
CAP_SYS_MODULEЗагружать kernel modules
CAP_KILLСлать сигналы чужим процессам
CAP_DAC_OVERRIDEИгнорировать DAC (rwx) проверки — эквивалент root для файлов
CAP_CHOWNМенять владельца файла
CAP_SETUIDsetuid() в произвольный UID
CAP_AUDIT_WRITEПисать в audit log

CAP_SYS_ADMIN — особенный. Это «catch-all», в неё запихали столько всего, что её часто называют «new root». Никогда не давайте её сервисам без острой необходимости.

# Все capabilities ядра:
capsh --print
# Current: cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,...

# Документация:
man capabilities  # большой man с полным списком

Как capabilities работают в процессе

У процесса есть НЕСКОЛЬКО наборов capabilities:

  • Permitted — что процесс ИМЕЕТ ПРАВО иметь.
  • Effective — что СЕЙЧАС используется при permission checks.
  • Inheritable — что передастся после exec в новую программу.
  • Ambient — наследуется без файловых capabilities (новый механизм для unprivileged scripts).
  • Bounding — максимум, который процесс может когда-либо иметь.

Эти наборы видны в /proc/PID/status:

cat /proc/$$/status | grep ^Cap
# CapInh: 0000000000000000
# CapPrm: 0000000000000000
# CapEff: 0000000000000000
# CapBnd: 000001ffffffffff
# CapAmb: 0000000000000000

# Декодировать:
capsh --decode=000001ffffffffff
# 0x000001ffffffffff=cap_chown,cap_dac_override,...

Обычный пользовательский процесс имеет пустые Eff/Prm — никаких эффективных capabilities. Только Bound широкий (потенциально может получить, если запустить нужный setuid-binary или из-под root).

Корневой процесс (root) имеет полный набор по умолчанию.


File capabilities: вместо setuid

Можно установить capabilities на исполняемый файл — они применятся к запущенному процессу:

# Дать локальной программе capability bind privileged port:
sudo setcap 'cap_net_bind_service=ep' /usr/local/bin/myapp

# Проверить:
getcap /usr/local/bin/myapp
# /usr/local/bin/myapp cap_net_bind_service=ep

# Теперь myapp может биндиться на порт 80 без setuid:
/usr/local/bin/myapp --port 80   # works! без root

=ep означает: добавить и в effective, и в permitted при запуске. +ep — алиас.

Это золотой паттерн для bind privileged port: вместо запуска сервиса под root + drop privileges, просто дать ему точечную capability.

# Найти все файлы с capabilities в системе:
sudo getcap -r / 2>/dev/null
# /usr/bin/ping cap_net_raw=ep
# /usr/sbin/tcpdump cap_net_admin,cap_net_raw=eip
# /usr/bin/mtr cap_net_raw=ep

Capabilities в systemd

systemd-units могут декларативно требовать capabilities:

[Service]
ExecStart=/usr/bin/myapp
User=appuser
Group=appuser

# Capability set -- что максимум может иметь:
CapabilityBoundingSet=CAP_NET_BIND_SERVICE

# Не получит новых capabilities через exec:
NoNewPrivileges=yes

# Дать ambient capability (наследуется в exec):
AmbientCapabilities=CAP_NET_BIND_SERVICE

Запуская сервис, systemd конфигурирует наборы capabilities заранее. Сервис стартует под appuser (не root!), но имеет CAP_NET_BIND_SERVICE — значит может биндиться на 80/443.

Это правильный современный паттерн запуска сетевых сервисов.


Что такое namespaces

Namespaces — другой механизм. Это изоляция представления о ресурсах. Процесс в своём namespace видит свой набор ресурсов, не зная о других.

Linux имеет восемь namespaces:

NamespaceЧто изолирует
pidPID-таблица: процесс видит только своих, init = PID 1
mntMount table: своё представление о смонтированных FS
netСеть: свои interfaces, routes, sockets
utshostname, domainname
ipcSysV IPC, POSIX message queues
userUID/GID mapping: внутри UID=0, снаружи 1000
cgroupCgroup hierarchy
timeСистемное время (новое, Linux 5.6+)

Контейнер — это процесс, запущенный во всех этих namespaces одновременно, плюс cgroups для лимитов ресурсов, плюс LSM (SELinux/AppArmor) для дополнительных проверок.

Контейнер: процесс + cgroups + namespaces + chroot
ProcessОбычный процесс на хосте
+
cgroupsЛимиты ресурсов: CPU, RAM, IO, PIDs
+
namespacesИзоляция взгляда на ресурсы: свои PID, mounts, net
chroot/pivot_rootСвой корень файловой системы
+
seccompWhitelist разрешённых syscalls
=
Container!Сочетание всего этого = то, что мы называем контейнером

Поиграть с namespaces

unshare — утилита, создающая новый namespace и запускающая процесс в нём.

# Создать новый PID namespace -- внутри будем PID 1:
sudo unshare --pid --fork --mount-proc bash
# В новом bash:
ps aux
# USER  PID  CMD
# root    1  bash
# root    2  ps aux
# Вы PID 1! /proc монтирован заново, только эти процессы видны.
exit

# Network namespace -- своя сеть, никаких интерфейсов:
sudo unshare --net bash
# Внутри:
ip link
# Только lo (loopback)
exit

# UTS namespace -- свой hostname:
sudo unshare --uts bash
hostname container-test
hostname        # container-test внутри
# В другом терминале на хосте:
# hostname     # старое имя -- видит хост
exit

# User namespace -- внутри вы 'root' (UID 0), снаружи обычный пользователь:
unshare --user --map-root-user bash
id
# uid=0(root) gid=0(root) groups=0(root)
# При этом за пределами вы UID 1000.
exit

Последнее особенно интересно: user namespace позволяет «получить root» внутри namespace без реальных привилегий снаружи. Внутри вы можете делать всё, что обычно требует root (chmod чужих файлов, например), но только над ресурсами, которые мапятся в ваш UID хоста. Это безопасный root.

Rootless Docker и user namespaces: практический результат этого урока

/proc/PID/ns: посмотреть namespaces процесса

ls -l /proc/$$/ns/
# lrwxrwxrwx 1 levoely levoely 0 May 18 12:00 cgroup -> 'cgroup:[4026531835]'
# lrwxrwxrwx 1 levoely levoely 0 May 18 12:00 ipc -> 'ipc:[4026531839]'
# lrwxrwxrwx 1 levoely levoely 0 May 18 12:00 mnt -> 'mnt:[4026531841]'
# lrwxrwxrwx 1 levoely levoely 0 May 18 12:00 net -> 'net:[4026531840]'
# lrwxrwxrwx 1 levoely levoely 0 May 18 12:00 pid -> 'pid:[4026531836]'
# lrwxrwxrwx 1 levoely levoely 0 May 18 12:00 user -> 'user:[4026531837]'
# lrwxrwxrwx 1 levoely levoely 0 May 18 12:00 uts -> 'uts:[4026531838]'

Числа в скобках — inode-номера namespace’ов. Два процесса в одном namespace имеют одинаковый номер.

# Сравнить namespace shell и какого-то Docker-контейнера:
readlink /proc/$$/ns/pid
# pid:[4026531836] -- хостовой
docker run -d --name test alpine sleep 3600
CONTAINER_PID=$(docker inspect -f '{{.State.Pid}}' test)
readlink /proc/$CONTAINER_PID/ns/pid
# pid:[4026532187] -- свой namespace

Docker под капотом — это unshare + cgroups + chroot

Когда вы делаете docker run alpine, под капотом происходит примерно:

  1. Подготовка корня: Создаётся overlay FS из слоёв image. Это будет корнем контейнера.
  2. clone() с CLONE_NEW флагами: Создаются namespaces — pid, mnt, net, uts, ipc, user (опционально).
  3. pivot_root в подготовленный корень.
  4. cgroups: настраиваются лимиты CPU/RAM/IO в /sys/fs/cgroup/.
  5. Capabilities: ограничиваются (drop CAP_SYS_ADMIN и др. — Docker по умолчанию dropу 14 capabilities).
  6. Seccomp profile: активируется фильтр syscalls (Docker блокирует ~50 опасных).
  7. AppArmor/SELinux profile (если есть).
  8. exec /entrypoint: запускается процесс контейнера.

Всё это — стандартные Linux примитивы. Никаких «магических контейнеров», обёрнутые в удобный CLI.

# Можно посмотреть, какие namespace у контейнера:
docker run --name test -d alpine sleep 3600
PID=$(docker inspect -f '{{.State.Pid}}' test)
ls -l /proc/$PID/ns/
# Сравнить с хостовыми /proc/1/ns -- разные inode

# Capabilities контейнера:
docker exec test grep ^Cap /proc/self/status
# CapBnd: ... (типично 00000000a80425fb = subset, без SYS_ADMIN и др.)

Реальный пример: запустить веб-сервер на порту 80 без root

«Старый» способ: сервис стартует как root, после bind делает setuid в www-data.

Современный способ:

# 1. Создать сервис под dedicated user, без root прав:
sudo useradd -r -s /sbin/nologin webapp

# 2. Дать бинарнику capability:
sudo setcap 'cap_net_bind_service=ep' /usr/local/bin/myapp

# 3. systemd unit:
cat << 'EOF' | sudo tee /etc/systemd/system/myapp.service
[Unit]
Description=My web app
After=network.target

[Service]
Type=simple
User=webapp
Group=webapp
ExecStart=/usr/local/bin/myapp --port 80
AmbientCapabilities=CAP_NET_BIND_SERVICE
NoNewPrivileges=yes
ProtectSystem=strict
PrivateTmp=yes

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl start myapp
sudo systemctl status myapp

Сервис бежит под webapp (UID >1000), имеет только CAP_NET_BIND_SERVICE (для порта 80). Если его взломают — атакующий не получит root, не сможет читать чужие файлы, не сможет менять систему.


Попробуй сам

# 1. Какие capability имеют ваши команды:
getcap /usr/bin/ping /usr/sbin/tcpdump /usr/bin/su 2>/dev/null

# 2. Посмотреть текущие capability вашего shell:
cat /proc/$$/status | grep ^Cap
capsh --print

# 3. Все namespaces shell:
ls -l /proc/$$/ns/

# 4. Создать свой namespace и посмотреть:
sudo unshare --pid --fork --mount-proc bash
ps -ef    # вы PID 1
exit

# 5. Network namespace:
sudo unshare --net bash
ip link   # только lo
ping 8.8.8.8   # фейл, нет сетевых интерфейсов
exit

# 6. User namespace (без sudo!):
unshare --user --map-root-user bash
id    # UID=0 внутри
ls -l /etc/shadow   # всё равно не прочитаете -- mapping не покрывает root host
exit

# 7. Сравнить с Docker:
docker run --rm alpine cat /proc/self/status | grep Cap
# CapBnd: 00000000a80425fb -- Docker drop'нул многое

# 8. Снять capability с своего бинарника:
sudo cp /usr/bin/ping /tmp/myping
sudo setcap 'cap_net_raw=ep' /tmp/myping
getcap /tmp/myping
/tmp/myping 8.8.8.8 -c 1   # сработает без sudo
sudo setcap -r /tmp/myping
/tmp/myping 8.8.8.8 -c 1   # permission denied

Проверка знанийKnowledge check
Команда хочет запустить веб-сервис, который слушает порт 443 (TLS), читает SSL-сертификаты из /etc/ssl/private/, и пишет логи в /var/log/myapp/. Как настроить это с минимальными правами? Опиши конкретный сетап c capabilities, systemd-unit, и какие именно chown/chmod нужны.
ОтветAnswer
План минимальных привилегий для этого сервиса: 1) Создать dedicated пользователя БЕЗ shell-доступа: ``` sudo useradd -r -s /sbin/nologin -d /nonexistent myapp ``` -r = system account (UID less than 1000), без home, без shell. 2) Файлы и права: Бинарник: ``` sudo chown root:root /usr/local/bin/myapp sudo chmod 0755 /usr/local/bin/myapp sudo setcap 'cap_net_bind_service=ep' /usr/local/bin/myapp ``` Capability на бинарник -- альтернатива systemd AmbientCapabilities, можно делать любым из двух. SSL-сертификаты (приватные ключи -- особенно секретно): ``` sudo mkdir -p /etc/myapp/ssl sudo chown root:myapp /etc/myapp/ssl sudo chmod 0750 /etc/myapp/ssl sudo cp cert.pem /etc/myapp/ssl/ sudo cp key.pem /etc/myapp/ssl/ sudo chown root:myapp /etc/myapp/ssl/*.pem sudo chmod 0640 /etc/myapp/ssl/*.pem ``` Логика: owner root (чтобы myapp НЕ мог переписать SSL), group myapp (myapp читает через g+r), other --- (никаких чужих). Лог-директория: ``` sudo mkdir -p /var/log/myapp sudo chown myapp:myapp /var/log/myapp sudo chmod 0755 /var/log/myapp ``` Сюда myapp пишет как owner. 3) systemd unit с максимальным hardening: ```ini [Unit] Description=My TLS web service After=network.target [Service] Type=simple User=myapp Group=myapp ExecStart=/usr/local/bin/myapp --port 443 --cert /etc/myapp/ssl/cert.pem --key /etc/myapp/ssl/key.pem StandardOutput=append:/var/log/myapp/app.log StandardError=append:/var/log/myapp/error.log # Минимальные capabilities CapabilityBoundingSet=CAP_NET_BIND_SERVICE AmbientCapabilities=CAP_NET_BIND_SERVICE # Hardening NoNewPrivileges=yes ProtectSystem=strict ProtectHome=yes PrivateTmp=yes ReadWritePaths=/var/log/myapp ReadOnlyPaths=/etc/myapp PrivateDevices=yes ProtectKernelTunables=yes ProtectKernelModules=yes ProtectControlGroups=yes RestrictNamespaces=yes LockPersonality=yes MemoryDenyWriteExecute=yes [Install] WantedBy=multi-user.target ``` Что мы получили: - Сервис бежит как myapp (UID system account), НЕ root. - Имеет только CAP_NET_BIND_SERVICE -- может биндить privileged port 443. - Не может писать в /etc, /usr, / -- ProtectSystem=strict. - Не видит /home -- ProtectHome. - Не имеет нового /tmp -- PrivateTmp. - ReadWritePaths=/var/log/myapp -- единственное место, куда может писать. - ReadOnlyPaths=/etc/myapp -- может только читать конфиги и SSL. - MemoryDenyWriteExecute -- защита от RCE через JIT. При компрометации (RCE в коде сервиса) атакующий получает: - Доступ только к /var/log/myapp (запись) и /etc/myapp (чтение, включая SSL key). - Не root, не может читать /etc/shadow, не может монтировать FS. - Нет sudo, нет shell. Это и есть defense in depth. SSL key всё равно достижим (сервис должен его читать), но больше ничего важного.

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 4