2.5. Capabilities
Цели
После этого материала вы сможете:
- объяснить, что такое Linux capabilities и какую задачу они решают;
- объяснить, почему
rootвнутри container — не полноценныйroot; - перечислить capabilities, которые Docker оставляет по умолчанию, и назвать назначение основных;
- посмотреть фактический набор capabilities процесса;
- применять
--cap-dropи--cap-addосознанно; - определить минимально необходимый набор capabilities для приложения;
- объяснить, что именно даёт
--privilegedи почему он почти всегда избыточен.
Предварительные знания
- 2.1. Процессы и containers;
- 2.3. Linux namespaces — в частности, что user namespace по умолчанию не создаётся;
- понимание прав доступа Linux и роли пользователя
root.
Ключевые термины
| Термин | Объяснение |
|---|---|
capability | Отдельная привилегия, на которые разделены полномочия root |
CAP_* | Именование capabilities в ядре, например CAP_NET_BIND_SERVICE |
permitted set | Набор capabilities, которые процесс может задействовать |
effective set | Набор capabilities, действующих прямо сейчас |
bounding set | Верхняя граница: capabilities, которые процесс не может получить ни при каких условиях |
privileged container | Container, запущенный с --privileged: почти все ограничения сняты |
setuid | Механизм запуска программы с правами владельца файла. Исторический предшественник capabilities |
Теория
Задача, которую решают capabilities
Классическая модель Unix знает два состояния: обычный пользователь и root. root может всё — читать любые файлы, менять владельцев, открывать привилегированные порты, загружать модули ядра, перезагружать систему.
Проблема этой модели видна на простом примере. Веб-серверу нужна ровно одна привилегия — привязаться к порту 80 (порты ниже 1024 требуют привилегий). Но для этого его приходилось запускать от root, выдавая заодно право читать /etc/shadow, загружать модули ядра и форматировать диски. Уязвимость в веб-сервере превращалась в полную компрометацию машины.
Capabilities разделяют полномочия root на отдельные права — их около сорока. Процесс получает только те, которые ему нужны.
Веб-серверу достаточно CAP_NET_BIND_SERVICE. Всё остальное можно не выдавать.
Как это связано с containers
Docker по умолчанию не создаёт user namespace (урок 2.3). Значит, root внутри container — это UID 0 и на host.
Тогда почему процесс в container не может отформатировать диск host или загрузить модуль ядра?
Ответ: Docker отбирает большинство capabilities. Процесс имеет UID 0, но набор его полномочий сильно урезан. Это второй уровень защиты после namespaces.
Формулировка, которую стоит запомнить:
rootвнутри container — это UID 0 с ограниченным набором capabilities, ограниченным набором системных вызовов (seccomp) и изолированным представлением о системе (namespaces). Три независимых механизма.
Что Docker оставляет по умолчанию
Docker сохраняет 14 capabilities и отбирает остальные:
| Capability | Что разрешает | Зачем оставлена |
|---|---|---|
CHOWN | Менять владельца файлов | Установка пакетов, создание файлов приложения |
DAC_OVERRIDE | Обходить проверки прав доступа к файлам | Работа root с файлами внутри container |
FOWNER | Действовать как владелец файла | chmod, utime на чужих файлах внутри container |
FSETID | Сохранять биты setuid/setgid при изменении файла | Установка пакетов |
KILL | Посылать сигналы процессам без совпадения UID | Управление дочерними процессами |
SETGID | Менять GID процесса | Понижение привилегий: запуск от непривилегированного пользователя |
SETUID | Менять UID процесса | То же |
SETPCAP | Изменять набор capabilities | Понижение привилегий |
SETFCAP | Устанавливать capabilities на файлы | Установка пакетов |
NET_BIND_SERVICE | Привязываться к портам ниже 1024 | Веб-серверы на портах 80 и 443 |
NET_RAW | Использовать RAW и PACKET сокеты | ping, traceroute, диагностика сети |
SYS_CHROOT | Выполнять chroot() | Работа некоторых инструментов |
MKNOD | Создавать файлы устройств | Совместимость с приложениями, создающими устройства |
AUDIT_WRITE | Писать в журнал аудита ядра | Работа login и подобных утилит |
Отобраны, в частности:
| Capability | Что давала бы |
|---|---|
SYS_ADMIN | Огромный набор административных операций, включая mount. Самая опасная capability |
SYS_MODULE | Загрузка и выгрузка модулей ядра |
SYS_TIME | Изменение системных часов (общих с host) |
SYS_PTRACE | Отладка чужих процессов, чтение их памяти |
SYS_BOOT | Перезагрузка системы |
NET_ADMIN | Настройка сети: интерфейсы, маршруты, правила фильтрации |
SYS_RAWIO | Прямой доступ к портам ввода-вывода |
MAC_ADMIN, MAC_OVERRIDE | Изменение политик SELinux и AppArmor |
Отсюда объясняется практическое наблюдение: mount внутри container не работает даже от root — нужна CAP_SYS_ADMIN, которой нет.
Принцип минимальных привилегий
Набор по умолчанию — компромисс между совместимостью и безопасностью. Он рассчитан на то, чтобы работало большинство образов, а не на минимальность.
Для конкретного приложения набор почти всегда избыточен. Типичное Python-приложение, слушающее порт 8000 от непривилегированного пользователя, не нуждается ни в одной capability.
Рекомендуемая практика для production:
docker run --cap-drop=ALL <image>
И далее добавлять только то, что действительно потребуется. Подробно — в разделе 11.
--privileged
Флаг снимает почти все ограничения:
- выдаются все capabilities;
- снимается ограничение seccomp;
- снимается профиль AppArmor;
- container получает доступ ко всем устройствам host;
/sysмонтируется в режиме записи.
Практически это означает: --privileged container может смонтировать диск host, загрузить модуль ядра, изменить настройки сети host — то есть выйти за пределы изоляции штатными средствами.
Опасная настройка.
--privilegedфактически снимает границу между container и host. Container с этим флагом следует считать частью host, а не изолированной средой. Компрометация приложения внутри означает компрометацию машины.
Законные применения существуют — Docker-in-Docker, некоторые системные агенты, — но они редки. В подавляющем большинстве случаев --privileged используется потому, что «без него не заработало», а нужна была одна конкретная capability или одно устройство.
Правильный путь: определить, чего не хватает, и выдать точечно через --cap-add или --device. Методика — в разделе «Практическое упражнение».
Внутренний механизм
Наборы capabilities процесса
У процесса не один набор, а несколько:
| Набор | Назначение |
|---|---|
| Permitted (P) | Что процесс имеет право задействовать |
| Effective (E) | Что действует прямо сейчас; ядро проверяет именно этот набор |
| Inheritable (I) | Что наследуется при execve() — механизм редко используется |
| Bounding (B) | Верхняя граница: чего процесс не получит никогда |
| Ambient (A) | Сохраняется при execve() для непривилегированных программ |
Ключевой для containers — bounding set. Docker формирует его при запуске, и capability, отсутствующая в bounding set, не может быть получена никаким способом изнутри container.
Наборы видны в /proc/<pid>/status:
CapInh: 0000000000000000
CapPrm: 00000000a80425fb
CapEff: 00000000a80425fb
CapBnd: 00000000a80425fb
CapAmb: 0000000000000000
Значения — битовые маски в шестнадцатеричном виде. Расшифровать их можно утилитой capsh.
Файловые capabilities
Capability можно назначить не только процессу, но и исполняемому файлу:
sudo setcap cap_net_bind_service=ep /usr/bin/myserver
Тогда программа получает эту привилегию при запуске от любого пользователя. Это современная альтернатива биту setuid: вместо «дать все права root» — «дать одно конкретное право».
Именно этот механизм использовался в уроке 1.6 для публикации привилегированных портов в rootless mode.
Команды и примеры
Capabilities процесса на host
capsh --print | head -5
Current: =
Bounding set =cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,...
Ambient set =
Current IAB:
Securebits: 00/0x0/1'b0
Для обычного пользователя Current: = означает пустой набор — capabilities не нужны, права определяются обычной моделью UID.
Если capsh не установлен:
sudo apt install -y libcap2-bin
Capabilities внутри container
docker run --rm alpine sh -c 'apk add -q libcap && capsh --print | head -3'
Или без установки пакетов — прочитав маску напрямую:
docker run --rm alpine grep CapBnd /proc/self/status
CapBnd: 00000000a80425fb
Расшифруем маску на host:
capsh --decode=00000000a80425fb
0x00000000a80425fb=cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,
cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,
cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap
Ровно те 14 capabilities, что перечислены в таблице выше.
Сравним с host:
grep CapBnd /proc/self/status
CapBnd: 000001ffffffffff
capsh --decode=000001ffffffffff | tr ',' '\n' | wc -l
41
41 против 14. Docker отобрал две трети полномочий.
Демонстрация: чего не может root в container
docker run --rm alpine sh -c 'mkdir -p /mnt/test && mount -t tmpfs none /mnt/test'
mount: permission denied (are you root?)
Процесс работает от root (проверим: docker run --rm alpine id даёт uid=0), но mount требует CAP_SYS_ADMIN, которой нет.
Добавим её:
docker run --rm --cap-add=SYS_ADMIN alpine \
sh -c 'mkdir -p /mnt/test && mount -t tmpfs none /mnt/test && echo "смонтировано"'
mount: mounting none on /mnt/test failed: Permission denied
Не заработало — и это важнее, чем если бы заработало. Capability выдана, а операция всё равно запрещена: её запретил другой механизм. На Ubuntu это AppArmor, чей профиль docker-default блокирует mount независимо от capabilities.
Уберём и его:
docker run --rm --cap-add=SYS_ADMIN --security-opt apparmor=unconfined alpine \
sh -c 'mkdir -p /mnt/test && mount -t tmpfs none /mnt/test && echo "смонтировано"'
смонтировано
Теперь заработало. Вывод из этих трёх запусков — главный в уроке: ограничения складываются, и снятие одного ничего не гарантирует. Container ограничен как минимум четырьмя независимыми механизмами — capabilities, seccomp, AppArmor (или SELinux) и namespaces, — и операция выполнится, только если её разрешат все.
Отсюда практическое следствие для диагностики: «выдал capability, а не работает» почти всегда означает, что запрещает не capability. Смотреть нужно dmesg на предмет apparmor="DENIED" и вывод docker inspect --format '{{.AppArmorProfile}}'.
CAP_SYS_ADMIN— самая широкая из capabilities: она покрывает десятки операций и часто рассматривается как «новыйroot». Выдавать её нужно с большой осторожностью и только при отсутствии альтернатив.
Проверка NET_BIND_SERVICE
docker run --rm python:3.13-slim python -c "
import socket
s = socket.socket()
s.bind(('0.0.0.0', 80))
print('порт 80 занят успешно')
"
порт 80 занят успешно
Работает, потому что NET_BIND_SERVICE входит в набор по умолчанию.
Уберём её:
docker run --rm --cap-drop=NET_BIND_SERVICE python:3.13-slim python -c "
import socket
s = socket.socket()
s.bind(('0.0.0.0', 80))
print('порт 80 занят успешно')
"
порт 80 занят успешно
Порт занялся без capability. Ожидание было противоположным, и это тот случай, когда проверка спасает от заблуждения с последствиями для безопасности.
Причина — в sysctl, который Docker выставляет внутри container'а:
sysctl net.ipv4.ip_unprivileged_port_start # на хосте
docker run --rm alpine sysctl net.ipv4.ip_unprivileged_port_start # внутри
net.ipv4.ip_unprivileged_port_start = 1024
net.ipv4.ip_unprivileged_port_start = 0
Ядро требует CAP_NET_BIND_SERVICE только для портов ниже значения этого параметра. На хосте граница — 1024, внутри container'а Docker опускает её до нуля, и привилегированных портов там просто не остаётся. Отбирать capability бессмысленно: она ни на что не влияет.
Вернём границу — и capability снова начнёт что-то значить:
docker run --rm --sysctl net.ipv4.ip_unprivileged_port_start=1024 \
--cap-drop=NET_BIND_SERVICE python:3.13-slim python -c "
import socket
s = socket.socket()
s.bind(('0.0.0.0', 80))
print('порт 80 занят успешно')
"
PermissionError: [Errno 13] Permission denied
Теперь связь capability с поведением видна прямо.
Что из этого следует практически. Приложение в container'е может слушать 80-й порт, не имея ни root, ни NET_BIND_SERVICE:
docker run --rm --user 10001:10001 --cap-drop=ALL python:3.13-slim python -c "
import socket
s = socket.socket()
s.bind(('0.0.0.0', 80))
print('порт 80 занят: uid 10001, capabilities отобраны все')
"
порт 80 занят: uid 10001, capabilities отобраны все
Это удобно — не нужен ни root, ни лишняя capability, — но и означает, что «слушает привилегированный порт» не является признаком привилегированного процесса. Внутри container'а привилегированных портов нет.
Отбор всех capabilities
docker run --rm --cap-drop=ALL alpine grep CapBnd /proc/self/status
CapBnd: 0000000000000000
Пустой набор. Проверим, что обычное приложение при этом работает:
docker run --rm --cap-drop=ALL python:3.13-slim python -c "
import socket, json, sys
s = socket.socket()
s.bind(('0.0.0.0', 8000))
s.listen(1)
print(json.dumps({'ok': True, 'port': 8000}))
"
{"ok": true, "port": 8000}
Приложение, слушающее непривилегированный порт, не нуждается ни в одной capability. Это и есть целевое состояние для production.
Точечное добавление
docker run --rm --cap-drop=ALL --cap-add=NET_BIND_SERVICE python:3.13-slim python -c "
import socket
s = socket.socket()
s.bind(('0.0.0.0', 80))
print('порт 80 занят, capabilities: только NET_BIND_SERVICE')
"
порт 80 занят, capabilities: только NET_BIND_SERVICE
Паттерн --cap-drop=ALL --cap-add=<нужное> — рекомендуемый подход для production.
Что даёт --privileged
docker run --rm --privileged alpine grep CapBnd /proc/self/status
CapBnd: 000001ffffffffff
Та же маска, что у root на host: все 41. Плюс отключены seccomp и AppArmor, плюс доступ к устройствам.
Демонстрация последствий — доступ к дискам host:
docker run --rm --privileged alpine sh -c 'ls /dev/sd* /dev/nvme* 2>/dev/null | head'
/dev/nvme0n1
/dev/nvme0n1p1
/dev/nvme0n1p2
Имена зависят от оборудования: на SATA-диске это будут /dev/sda, /dev/sda1. Существенно не имя, а то, что устройства видны.
Container видит блочные устройства host и, имея все capabilities, может их монтировать и изменять. Изоляции фактически нет.
Сравним с обычным запуском:
docker run --rm alpine sh -c 'ls /dev/'
console core fd full mqueue null ptmx pts random
shm stderr stdin stdout tty urandom zero
Минимальный набор виртуальных устройств. Дисков host нет.
Проверка capabilities в Compose
services:
api:
image: myapp:1.0
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
security_opt:
- no-new-privileges:true
Подробно — в разделе 09 и разделе 11.
Практическое упражнение
Задание. Определите минимально необходимый набор capabilities для приложения методом последовательного сужения.
- Возьмите образ
python:3.13-slimи приложение, которое слушает порт 80 и записывает файл в/tmp. - Запустите с набором по умолчанию — убедитесь, что работает.
- Запустите с
--cap-drop=ALL— зафиксируйте, что сломалось и с какой ошибкой. - Добавляйте capabilities по одной, пока не заработает.
- Оформите результат: итоговый набор и обоснование каждой capability.
Дополнительно: повторите для случая, когда приложение слушает порт 8000 вместо 80. Объясните разницу.
Подсказки
Подсказка 1
Тестовое приложение можно передать через python -c без создания файлов:
docker run --rm --cap-drop=ALL -p 8080:80 python:3.13-slim python -c "..."
Подсказка 2
Сообщение об ошибке подсказывает нужную capability. PermissionError при bind() на порт ниже 1024 указывает на NET_BIND_SERVICE; Operation not permitted при chown — на CHOWN.
Подсказка 3
Полный список capabilities с описаниями:
man 7 capabilities
Решение
Сначала выполните задание самостоятельно.
Показать решение
#!/usr/bin/env bash
# find-caps.sh — подбор минимального набора capabilities.
set -uo pipefail
APP='
import socket, pathlib, sys
try:
p = pathlib.Path("/tmp/probe.txt")
p.write_text("ok")
s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("0.0.0.0", PORT))
print("OK")
except Exception as e:
print(f"FAIL: {type(e).__name__}: {e}", file=sys.stderr)
sys.exit(1)
'
try_caps() {
local port="$1"; shift
local args=()
for c in "$@"; do args+=(--cap-add="$c"); done
docker run --rm --cap-drop=ALL "${args[@]}" python:3.13-slim \
python -c "${APP//PORT/$port}" 2>&1
}
echo "=== Порт 80 ==="
echo -n " по умолчанию: "
docker run --rm python:3.13-slim python -c "${APP//PORT/80}" 2>&1 | tail -1
echo -n " --cap-drop=ALL: "
try_caps 80 | tail -1
echo -n " + NET_BIND_SERVICE: "
try_caps 80 NET_BIND_SERVICE | tail -1
echo
echo "=== Порт 8000 ==="
echo -n " --cap-drop=ALL: "
try_caps 8000 | tail -1
Ожидаемый вывод:
=== Порт 80 ===
по умолчанию: OK
--cap-drop=ALL: FAIL: PermissionError: [Errno 13] Permission denied
+ NET_BIND_SERVICE: OK
=== Порт 8000 ===
--cap-drop=ALL: OK
Результат и обоснование.
| Сценарий | Минимальный набор | Обоснование |
|---|---|---|
| Порт 80 (или любой ниже 1024) | NET_BIND_SERVICE | Привязка к привилегированному порту — единственная операция, требующая capability. Запись в /tmp от root работает без capabilities, потому что владелец каталога позволяет |
| Порт 8000 (или любой от 1024) | пустой | Ни одна операция не требует привилегий |
Практический вывод. Если приложение слушает порт от 1024 и работает со своими файлами, оно не нуждается ни в одной capability. Это типичный случай для Python-сервисов за reverse proxy.
Если по каким-то причинам нужен порт 80, есть два пути: выдать NET_BIND_SERVICE или — лучше — слушать порт 8000 внутри container и публиковать наружу как -p 80:8000. Второй вариант вообще не требует capabilities, потому что привязку к порту 80 выполняет Docker на host, а не приложение.
Второй вариант иллюстрирует общий принцип: часто привилегия не нужна вовсе, если немного изменить постановку задачи.
Проверка результата
docker run --rm --cap-drop=ALL alpine grep CapBnd /proc/self/status
CapBnd: 0000000000000000
Нулевая маска подтверждает, что вы умеете полностью отбирать capabilities. Проверьте, что ваше приложение из проекта 2 при этом работает.
Типичные ошибки
| Ошибка | Причина | Исправление |
|---|---|---|
Использование --privileged, чтобы «заработало» | Не выяснено, какой именно привилегии не хватает | Определить конкретную capability и выдать через --cap-add |
Мнение, что root в container безопасен «потому что изолирован» | Смешаны три разных механизма | Изоляцию даёт namespaces, ограничение привилегий — capabilities и seccomp |
| Оставление набора по умолчанию в production | Он рассчитан на совместимость, а не на минимальность | --cap-drop=ALL и точечное добавление |
--cap-add=SYS_ADMIN для одной операции | Эта capability покрывает десятки операций | Найти узкую альтернативу; часто задача решается иначе |
| Привязка приложения к порту 80 внутри container | Требует capability без необходимости | Слушать порт от 1024, публиковать через -p 80:8000 |
Ожидание, что --cap-drop защитит от всего | Capabilities — один из уровней, не единственный | Дополнять seccomp, non-root user, read-only rootfs |
Проверка capabilities по id вместо маски | id показывает UID, а не полномочия | Смотреть CapBnd в /proc/self/status |
Контрольные вопросы
На понимание:
- Какую проблему классической модели
rootрешают capabilities? - Почему
rootвнутри container не может выполнитьmount, хотя это UID 0? - Сколько capabilities Docker оставляет по умолчанию и почему именно набор такого размера?
- Что такое bounding set и почему он важнее остальных наборов для containers?
- Что именно делает
--privileged, кроме выдачи всех capabilities?
На применение:
- Как посмотреть фактический набор capabilities процесса внутри container?
- Как запустить приложение с минимальным набором привилегий?
- Как определить, какой capability не хватает приложению?
На диагностику:
- Приложение падает с
PermissionErrorприbind()на порт 443. Назовите два решения, одно из которых не требует capability. - В чужом
compose.yamlнайденprivileged: true. Какие вопросы задать автору и что предложить?
Краткое резюме
- Capabilities разделяют полномочия
rootпримерно на сорок отдельных прав. - Docker не создаёт user namespace по умолчанию, поэтому
rootв container — это UID 0 на host. - Ограничение достигается отбором capabilities, профилем seccomp и namespaces — тремя независимыми механизмами.
- По умолчанию Docker оставляет 14 capabilities из 41.
mountвнутри container не работает, потому что нетCAP_SYS_ADMIN.- Набор по умолчанию рассчитан на совместимость и для конкретного приложения избыточен.
- Рекомендуемая практика:
--cap-drop=ALLи точечное добавление нужного. - Приложение, слушающее порт от 1024, обычно не нуждается ни в одной capability.
--privilegedснимает capabilities, seccomp, AppArmor и открывает устройства host — изоляции практически не остаётся.- Часто привилегия не нужна вовсе, если изменить постановку: слушать порт 8000 и публиковать как 80.
Официальные источники
| Источник | Ссылка | Что подтверждает |
|---|---|---|
| Docker Engine security: kernel capabilities | https://docs.docker.com/engine/security/#linux-kernel-capabilities | Подход allowlist, отбор большинства capabilities по умолчанию, невозможность mount и создания устройств |
| docker run reference: runtime privilege and capabilities | https://docs.docker.com/reference/cli/docker/container/run/#privileged | Список capabilities по умолчанию, поведение --cap-add, --cap-drop, --privileged |
capabilities(7) man page | https://man7.org/linux/man-pages/man7/capabilities.7.html | Полный перечень capabilities, наборы permitted/effective/bounding/ambient, файловые capabilities |
capsh(1) man page | https://man7.org/linux/man-pages/man1/capsh.1.html | Просмотр и расшифровка масок capabilities |
setcap(8) man page | https://man7.org/linux/man-pages/man8/setcap.8.html | Назначение capabilities исполняемым файлам |
| Compose file: services | https://docs.docker.com/reference/compose-file/services/ | Атрибуты cap_add, cap_drop, privileged, security_opt |
Навигация
← Предыдущий материал
Вернуться к разделу
Следующий материал → Container runtime и OCI
Главное оглавление