← Linux: изоляция
12 мин · средне · Урок 3 из 4

Контейнер как процесс

Контейнер как обычный процесс: namespaces + cgroups + capabilities.

Контейнер — это процесс

Слово «контейнер» звучит как отдельная сущность, коробка со своим устройством внутри. На деле это обычный процесс Linux, которому при запуске выдали четыре вещи: ограниченную видимость, лимиты на ресурсы, урезанный набор прав и свой корень файловой системы.

Контейнер — это процесс, вокруг которого ядро выставило изоляцию через namespaces, ограничило ресурсы через cgroups и обрезало права через capabilities. Никакого отдельного «движка виртуализации» под ним нет.

Проверить это можно прямо на хосте. Запусти контейнер и найди его в общем списке процессов:

docker run -d --name web nginx
ps aux | grep nginx
$ ps aux | grep nginx
root   20431  0.0  0.1  10620  6200 Ss  12:03  0:00 nginx: master process
101    20489  0.0  0.0  11072  2600 S   12:03  0:00 nginx: worker

PID 20431 — это номер в общей таблице процессов хоста, не какой-то внутренний. С точки зрения ядра хоста контейнер ничем не отличается от любого другого запущенного бинарника.

процесс PID 20431 на хосте namespaces что он видит cgroups сколько съест capabilities что ему можно корень свой / ядро то же самое, своего движка под контейнером нет
Четыре настройки ядра поверх обычного процесса. Убери их — останется просто бинарник.

Четыре слоя изоляции

namespaces (namespace — пространство имён) изолируют то, что процесс видит: свои PID, свою сеть, свои точки монтирования. Внутри контейнера процесс думает, что он PID 1, а снаружи у него номер вроде 20431.

# заглянуть, в каких namespaces сидит процесс
ls -l /proc/20431/ns
lrwxrwxrwx net -> 'net:[4026532211]'
lrwxrwxrwx pid -> 'pid:[4026532213]'
lrwxrwxrwx mnt -> 'mnt:[4026532209]'

cgroups (control group — контрольная группа) вешают лимиты: сколько CPU и памяти процессу можно съесть. Если задать лимит памяти, ядро убьёт процесс при его превышении.

docker run -m 256m nginx
cat /sys/fs/cgroup/memory.max   # внутри контейнера покажет 268435456

capabilities (capability — отдельное право суперпользователя) разбивают всевластие root на мелкие разрешения. Контейнер получает лишь часть из них, а не полный набор.

docker run --rm alpine grep CapEff /proc/1/status
CapEff: 00000000a80425fb

CapEff — битовая маска действующих capabilities процесса: каждый бит отвечает за одно право. У root на хосте маска забита единицами (0000003fffffffff), у процесса в контейнере она заметно короче.

Расшифровать маску в имена помогает capsh из пакета libcap2-bin. В alpine его нет, поэтому запускать удобнее с хоста:

$ capsh --decode=00000000a80425fb
0x00000000a80425fb=cap_chown,cap_dac_override,cap_fowner,...

В списке нет, например, cap_sys_admin, поэтому смонтировать произвольную файловую систему изнутри такой контейнер не сможет.

Корень из образа подставляется сменой корня файловой системы: chroot или pivot_root переносит / процесса на распакованный образ. Сам образ лежит слоями поверх overlay-файловой системы (overlayfs складывает несколько каталогов в одно дерево, где верхний слой доступен на запись).

Чем это отличается от виртуальной машины

Виртуальная машина несёт своё ядро и работает поверх гипервизора (hypervisor — слой, эмулирующий железо). Контейнер своего ядра не имеет:

uname -r   # даст одинаковую версию и на хосте, и в контейнере

Один и тот же результат внутри и снаружи означает, что системный вызов из контейнера обрабатывает то же самое ядро хоста. Отсюда практический вывод: граница изоляции у контейнера тоньше, чем у виртуальной машины. Дыра в ядре бьёт по всем контейнерам сразу, тогда как виртуалки разделены собственными ядрами.

Две частые ловушки

Первая: «изоляция контейнера равна изоляции виртуальной машины». Это не так. Общее ядро означает меньшую границу. Уязвимость ядра или ошибка в настройке namespaces способна вывести процесс за пределы контейнера.

Вторая: «root в контейнере — это root на хосте». Тоже мимо. root внутри ограничен выданными capabilities, а при включённом user namespace его UID 0 отображается на непривилегированный UID хоста.

# внутри контейнера
id                       # uid=0(root)
# на хосте тот же процесс
cat /proc/20489/status | grep Uid   # Uid: 100000 100000 ...

Внутри процесс видит себя root, а хост видит обычного пользователя 100000. Захват такого root не даёт власти над машиной.

Что важно запомнить

Контейнер — это процесс с изоляцией, а не отдельная машина. На хосте он виден в ps как любой другой.

Четыре слоя: namespaces (видимость), cgroups (ресурсы), capabilities (права), корень из образа поверх overlay.

Ядро общее с хостом. Поэтому граница изоляции тоньше, чем у виртуальной машины с её собственным ядром.

root в контейнере ограничен capabilities и user namespace. Это не root на хосте.

Дальше эти механизмы собирает вместе Docker, а потом оркестрирует Kubernetes.

Прошли материал до конца? Отметьте урок пройденным.