Linux Capabilities: как устроен "новый root" и зачем они нужны в Docker
С Linux Capabilities я впервые столкнулся во время работы над проектом ServeHub-2. При настройке контейнеров в Docker Compose потребовалось запустить WG-easy с поддержкой AmneziaWG. Сервису были нужны привилегии для работы с сетевыми интерфейсами и загрузки модулей ядра. Раньше я встречал термин capabilities, но не разбирался в деталях, поэтому решил изучить механизм на практике.
В этой статье разберём, зачем Linux capabilities появились, чем они отличаются от полномочий root, как увидеть их у процесса и как применять ограниченный набор привилегий в Docker Compose и Kubernetes.
Почему одного root оказалось слишком много
Традиционно процессы в Linux разделялись на две большие группы. Обычные пользовательские приложения проходили стандартные проверки доступа на основе владельца, группы и режима файла. Процессы с UID 0, то есть root, обладали практически неограниченными полномочиями и могли обходить большинство проверок ядра.
Такой подход был удобен, но небезопасен. Если приложению требовалось выполнить всего одну привилегированную операцию, ему часто приходилось предоставлять полный набор прав суперпользователя. В случае уязвимости злоумышленник получал контроль не только над самим приложением, но и над значительной частью операционной системы.
Начиная с Linux 2.2 полномочия root были разделены на отдельные независимые возможности - capabilities. Благодаря этому процессу можно выдать только те права, которые действительно нужны для конкретной задачи.
Например:
- `CAP_NET_BIND_SERVICE` разрешает прослушивать порты с номерами ниже 1024;
- `CAP_CHOWN` позволяет менять владельца и группу файлов;
- `CAP_NET_ADMIN` отвечает за изменение сетевых интерфейсов, маршрутов и ряда сетевых параметров;
- `CAP_SYS_MODULE` разрешает загружать и выгружать модули ядра;
- `CAP_DAC_OVERRIDE` позволяет обходить обычные проверки прав доступа к файлам.
Таким образом, Linux capabilities - это механизм ядра, а не дополнительная функция конкретного приложения. Набор возможностей назначается процессу, контролируется ядром и проверяется в момент выполнения системного вызова.
Подробнее о принципах работы и примерах можно узнать в материале о [Linux capabilities и разделении привилегий root](https://habr.com/ru/articles/1075296/?utm_campaign=1075296&utm_source=habrahabr&utm_medium=rss).
Эксперимент с двумя контейнерами
Чтобы увидеть разницу на практике, можно создать два контейнера из одного базового образа. В обоих случаях процесс запускается от имени root: UID и GID равны нулю.
На хостовой системе создадим файл `/file`, принадлежащий root, и установим для него права `000`. Затем подключим этот файл к обоим контейнерам.
На первый взгляд результаты должны быть одинаковыми: образ один и тот же, пользователь тот же, файл тот же, а команда используется одна - `cat /file`. Однако в одном контейнере содержимое файла будет выведено успешно, а во втором появится ошибка:
```text
Permission denied
```
Причина заключается не в UID, образе или файловой системе. Отличается набор capabilities, назначенный контейнерам при запуске.
Информацию о текущем процессе можно посмотреть через `/proc`, например:
```bash
cat /proc/self/status
```
В выводе будут строки, начинающиеся с `Cap`:
- `CapInh` - наследуемые возможности, которые могут переходить к дочерним процессам;
- `CapPrm` - разрешённый набор, то есть максимум capabilities, доступных процессу;
- `CapEff` - эффективный набор, который ядро использует непосредственно при проверке операций;
- `CapBnd` - ограничивающий набор, определяющий верхнюю границу возможных привилегий;
- `CapAmb` - набор, сохраняющийся при переходе между пользователями, в том числе при работе с непривилегированными процессами.
В эксперименте значение `CapEff` первого контейнера заканчивается на `FB`, а второго - на `F9`. Разница небольшая, но она означает отсутствие одного установленного бита.
Найти различающийся бит можно с помощью операции XOR:
```bash
printf '%xn' $((0xFB ^ 0xF9))
```
Результатом будет шестнадцатеричное значение `2`. В двоичном виде это `0010`, то есть отличается только один флаг. Определить его название помогает утилита `capsh`:
```bash
capsh --decode=0x2
```
В данном случае речь идёт о `CAP_DAC_OVERRIDE`.
Аббревиатура DAC означает Discretionary Access Control - избирательное управление доступом. Это классическая модель Linux, основанная на владельце файла, группе и разрешениях для остальных пользователей. `CAP_DAC_OVERRIDE` позволяет процессу игнорировать такие ограничения и открывать файлы на чтение или запись независимо от установленных флагов.
Первый контейнер был запущен со стандартным набором привилегий Docker, куда входит `CAP_DAC_OVERRIDE`. Во втором эту capability заранее удалили. Поэтому даже root-процесс не смог прочитать файл с правами `000`.
Это важный момент: UID 0 внутри контейнера не всегда означает полный контроль. Контейнерный root ограничен capabilities, пространствами имён, seccomp-профилем и другими механизмами изоляции.
Настройка capabilities в Docker Compose
В Docker capabilities обычно добавляются и удаляются явно. Например:
```yaml
services:
app:
image: alpine:latest
cap_drop:
- ALL
cap_add:
- NET_ADMIN
```
Такая конфигурация сначала убирает все доступные привилегии, а затем возвращает только `CAP_NET_ADMIN`. Это гораздо безопаснее, чем запуск контейнера с `privileged: true`, поскольку приложение получает не полный набор возможностей, а строго ограниченный.
Если сервису необходимо слушать порт 80, может понадобиться:
```yaml
services:
web:
image: nginx:latest
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
```
Для WireGuard-подобных решений часто требуется `NET_ADMIN`, а иногда - `SYS_MODULE`, если приложение действительно загружает модули ядра. Однако добавлять `SYS_MODULE` без необходимости не следует: это одна из наиболее чувствительных привилегий.
При выборе параметров важна не только настройка Linux capabilities, но и понимание поведения конкретного приложения. Лучше начать с минимального набора, проверить запуск и добавить только ту capability, отсутствие которой приводит к ошибке.
Как работает проверка в ядре
Когда приложение выполняет системный вызов, ядро проверяет не только UID процесса. Для привилегированных действий учитывается эффективный набор capabilities. Если нужный бит отсутствует, операция завершается отказом, даже если процесс запущен от root.
Например, попытка изменить маршрут потребует сетевых полномочий, загрузка модуля - `CAP_SYS_MODULE`, а обход стандартных разрешений файла - `CAP_DAC_OVERRIDE`.
Именно поэтому права доступа Linux capabilities нельзя рассматривать как обычные права файла. Они не заменяют владельца, группу и режим доступа, а дополняют модель безопасности отдельным уровнем контроля.
В Docker контейнер получает capabilities при создании. В Kubernetes аналогичная настройка задаётся в спецификации pod:
```yaml
apiVersion: v1
kind: Pod
metadata:
name: network-tool
spec:
containers:
- name: tool
image: alpine:latest
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
add:
- NET_ADMIN
```
Параметр `allowPrivilegeEscalation: false` дополнительно запрещает процессу получать расширенные права через механизмы вроде setuid. На практике его стоит использовать вместе с минимальным набором capabilities и непривилегированным пользователем.
Безопасность важнее удобства
Самая распространённая ошибка - использовать `privileged: true`, когда приложению требуется всего одна возможность. Такой режим значительно ослабляет изоляцию контейнера и фактически предоставляет ему расширенный доступ к ресурсам хоста.
Более безопасная схема выглядит так:
1. отключить все capabilities через `cap_drop: ALL`;
2. определить, какая операция не выполняется;
3. добавить одну необходимую capability;
4. проверить работу сервиса;
5. отдельно оценить необходимость доступа к устройствам, модулям ядра и сетевым пространствам имён.
Для диагностики полезны команды `capsh`, `getcap`, `setcap`, просмотр `/proc/
Важен и контекст запуска. Возможности, доступные контейнеру, зависят не только от Compose-файла, но и от настроек Docker daemon, профиля seccomp, AppArmor или SELinux, режима rootless и политики безопасности самого хоста.
Именно поэтому Docker capabilities настройка должна выполняться как часть общей модели защиты, а не как способ быстро устранить ошибку запуска. Чем меньше привилегий получает сервис, тем ниже потенциальный ущерб при компрометации приложения. Для систем с повышенными требованиями к безопасности этот принцип особенно важен: ограниченный процесс проще контролировать, анализировать и изолировать, чем контейнер с полным набором прав root.
Практические примеры применения и дополнительные пояснения по теме доступны в материале о [настройке capabilities для Docker и Kubernetes](https://habr.com/ru/articles/1075296/?utm_campaign=1075296&utm_source=habrahabr&utm_medium=rss).


