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 из 0
Концептуальный
Вопрос 1 из 6. В чём принципиальное преимущество capabilities перед setuid root для программы вроде ping?

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

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

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

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