Capabilities и seccomp: почему root контейнера — это root с вырванными когтями
Root контейнера — не root хоста. Capabilities дробят власть root на ~40 флагов — Docker держит ~14 и сбрасывает остальные. Seccomp фильтрует таблицу syscall, блокируя ~44 из 350+ по умолчанию. --privileged выбрасывает оба — так и случаются побеги.
В run-команде Dockerfile стоял --privileged, а комментарий рядом гласил # нужно, чтобы health check мог пинговать. Кто-то годы назад поймал «operation not permitted» при вызове ping, погуглил, нашёл ответ на Stack Overflow «добавь —privileged», и заработало. Что ему на самом деле требовалось — одна capability: CAP_NET_RAW, чтобы открыть сырой ICMP-сокет. Что --privileged дал — всё: каждую capability, полное дерево устройств хоста в /dev, никакого seccomp-фильтра, запись в /sys. Пентест позже доказал цену: из этого контейнера тестер примонтировал корневую ФС хоста (CAP_SYS_ADMIN + видимые блочные устройства) и записал cron-задачу на ноду. Одну отсутствующую capability «починили», убрав каждый барьер, что был у контейнера. Двухстрочный правильный фикс — сбросить все capabilities, добавить обратно CAP_NET_RAW — дал ping ровно то, что нужно, а атакующему — ничего.
Capabilities: root, поделённый
Вот вопрос, обнажающий самую частую ошибку безопасности контейнеров: контейнер работает как root, получает EPERM — что делаешь? Неправильный ответ — --privileged — именно так случаются побеги. Правильный ответ требует знать, какой capability не хватает, и выдать только его.
В традиционном Unix было бинарно: UID 0 мог всё, остальных проверяли. Linux-capabilities (дискретные привилегии ядра, разрешающие конкретные привилегированные операции) дробят эту монолитную власть root на ~40 отдельных привилегий, каждая выдаётся независимо. Несколько важных:
- CAP_SYS_ADMIN — «новый root», мешок включающий
mount, создание namespace и десятки операций; самая опасная для выдачи. - CAP_NET_RAW — открывать сырые и packet-сокеты (что реально нужно
ping). - CAP_NET_BIND_SERVICE — биндиться на порты ниже 1024.
- CAP_SYS_PTRACE — трассировать/цепляться к другим процессам (отладчик; и способ читать память другого процесса).
- CAP_DAC_OVERRIDE — обходить проверки прав на чтение/запись/исполнение файлов.
- CAP_SYS_MODULE — грузить модули ядра (прямой путь к захвату ядра).
Обрати внимание, насколько каждая узкая: CAP_NET_RAW даёт сырые сокеты и ничего больше; CAP_NET_BIND_SERVICE покрывает только биндинг на порты ниже 1024. В этой узости весь смысл — выдавая ровно то, что нужно сервису, ты не даёшь атакующему ничего сверх. Сравни с CAP_SYS_ADMIN, отпирающей монтирование, создание namespace и десятки других операций: выдать её ради одной нужды значит обнажить всё остальное.
Когда контейнер работает как «root» (UID 0), он не получает все из них. Docker стартует контейнер с дефолтным allow-листом примерно из 14 capabilities и сбрасывает остальные — так что процесс-root контейнера, вызвавший mount, получает EPERM, ведь CAP_SYS_ADMIN нет в наборе. Поэтому «root контейнера» — это root с вырванными когтями: у него личность UID 0, но доля его властей ядра. Правильная поза для укреплённого сервиса — --cap-drop=ALL, а затем --cap-add только одной-двух, что доказуемо нужны, — least privilege в бетоне.
# По умолчанию сброшены (НЕ выданы) Docker — среди прочих:
CAP_SYS_ADMIN CAP_SYS_MODULE CAP_SYS_PTRACE CAP_SYS_TIME
CAP_NET_ADMIN CAP_SYS_RAWIO CAP_DAC_READ_SEARCH CAP_SYSLOG ...
# Укреплённый сервис:
docker run --cap-drop=ALL --cap-add=CAP_NET_BIND_SERVICE myappSeccomp: фильтрация таблицы syscall
Capabilities гейтят привилегированные операции; seccomp (secure computing) фильтрует сам интерфейс syscall — BPF-программу, которую ядро прогоняет на каждом syscall, решая allow / errno / kill. Docker поставляет дефолтный seccomp-профиль, блокирующий примерно 44 из 350+ syscall на x86-64, выбранных потому, что они опасны и редко нужны в контейнерах: keyctl (kernel keyring, история container-breakout), ptrace (пока не ослаблен), mount/umount2, reboot, swapon, kexec_load, bpf, clock_settime, init_module и другие. Фильтр — defense-in-depth, дополняющий capabilities: даже процесс с CAP_SYS_ADMIN всё равно заблокирован от mount, если seccomp-профиль отказывает этому syscall. Дефолтный профиль настроен так, что подавляющее большинство реальных нагрузок его не замечают, — а syscall, что он блокирует, ровно те, к которым тянется побег.
Контейнер, работающий как root, вызывает mount() и получает EPERM, хотя он UID 0. Почему root контейнера не может смонтировать, и какова единственная самая чистая причина?
▸Почему это работает
Зачем два механизма вместо одного? Capabilities отвечают на «дозволено ли этому процессу выполнить эту привилегированную операцию?», а seccomp — на «дозволено ли этому процессу вообще сделать этот syscall?» — разные слои. Capability бывает слишком грубой: CAP_SYS_ADMIN отпирает десятки операций, так что выдача её ради одной нужды обнажает остальные, и вот где seccomp зарабатывает своё место, сужая поверхность syscall даже при наличии широкой capability. Вместе с user namespace (ремаппинг root) и cgroups (лимит ресурсов) это независимые слои: поражение одного — скажем, баг ядра, обходящий seccomp, — всё равно оставляет остальные стоять. --privileged опасен именно потому, что схлопывает несколько слоёв разом.
—privileged: рубильник, выключающий всё
--privileged — это не «чуть больше доступа», это одновременное снятие почти каждой границы безопасности контейнера. Он выдаёт все capabilities, отключает seccomp-фильтр, открывает все хостовые устройства под /dev и даёт запись в /sys и части /proc. Со всеми capabilities (CAP_SYS_ADMIN) и видимыми хостовыми блочными устройствами процесс может смонтировать корневую ФС хоста и писать в неё — побег с cron-задачей из Hook. Легитимные применения узки (Docker-in-Docker, некоторые драйверные и storage-нагрузки), и даже им обычно нужна конкретная capability и устройство, а не общий рубильник. Рефлекс тянуться к --privileged, когда что-то вернуло EPERM, — самый частый способ превратить контейнер в вектор захвата хоста. Дисциплинированный шаг — прочесть, какая capability или syscall были отказаны, и выдать ровно их.
Команда добавляет --privileged, потому что ping вернул «operation not permitted». Пентестер затем монтирует корневую ФС хоста изнутри контейнера. Что изменил --privileged, что позволило смонтировать ФС хоста?
- 01Объясни, как capabilities и seccomp — два разных слоя, на конкретном примере, где оба участвуют в отказе mount().
- 02Опиши, что именно убирает --privileged и почему EPERM, «починенный» им, опасен, на примере ping.
Работать как root внутри контейнера — не то же, что быть root на хосте, и это навязывают два механизма ядра. Capabilities ломают старую власть UID-0 «всё-или-ничего» на около 40 независимо выдаваемых привилегий — CAP_SYS_ADMIN (mount и куда больше), CAP_NET_RAW (сырые сокеты для ping), CAP_SYS_PTRACE, CAP_SYS_MODULE и прочие. Docker выдаёт контейнеру лишь дефолтный набор примерно из 14 и сбрасывает остальные, так что root контейнера, вызвавший mount или пытающийся загрузить модуль, получает EPERM; укреплённая поза — —cap-drop=ALL плюс —cap-add только одной-двух, что сервису доказуемо нужны. Seccomp — второй, ортогональный слой: BPF-фильтр на интерфейсе syscall, и дефолтный профиль Docker блокирует около 44 из 350-с-лишним syscall — keyctl, ptrace, mount, reboot, kexec_load, init_module и прочие побего-уровневые вызовы — оставляя обычные нагрузки нетронутыми. Эти двое дополняют друг друга, и вместе с user namespace и cgroups образуют независимые слои, так что взлом одного оставляет остальные стоять. —privileged — антипаттерн: он выдаёт каждую capability, выключает seccomp, открывает все хостовые устройства и распахивает /sys на запись — схлопывая несколько границ разом. Поэтому контейнер, поймавший единственный EPERM (нужна была лишь CAP_NET_RAW для ping) и «починенный» через —privileged, стал путём для тестера смонтировать корневую ФС хоста и подсадить cron-задачу. Прочти отказанную capability или syscall и выдай ровно их; никогда не меняй каждый барьер на одно отсутствующее разрешение.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.