Как работает изоляция контейнеров на уровне ядра Linux?
Контейнеры — это не виртуальные машины. Они не содержат собственного ядра операционной системы. Все контейнеры на хосте разделяют одно ядро Linux, а изоляция обеспечивается тремя основными механизмами ядра: namespaces (изоляция видимости), cgroups (ограничение ресурсов) и union filesystems (послойная файловая система). Понимание этих механизмов необходимо для оценки реальной степени изоляции и осознанного выбора дополнительных мер защиты.
1. Linux Namespaces (изоляция видимости ресурсов):
Namespaces создают для процесса внутри контейнера иллюзию собственной изолированной операционной системы. Каждый namespace изолирует определённый тип системных ресурсов:
| Namespace | Что изолирует | Флаг clone() | Описание |
|---|---|---|---|
| PID | Процессы | CLONE_NEWPID | Контейнер видит только свои процессы. Процесс entrypoint получает PID 1 внутри контейнера, хотя на хосте у него другой PID |
| NET | Сеть | CLONE_NEWNET | Собственный сетевой стек: IP-адрес, интерфейсы, таблица маршрутизации, порты |
| MNT | Файловая система | CLONE_NEWNS | Собственные точки монтирования, изолированные от хоста |
| UTS | Hostname | CLONE_NEWUTS | Собственное имя хоста (hostname), независимое от реального хоста |
| IPC | Межпроцессное взаимодействие | CLONE_NEWIPC | Собственные семафоры, очереди сообщений, разделяемая память |
| USER | Пользователи | CLONE_NEWUSER | Собственная таблица UID/GID. Root (UID 0) внутри контейнера может маппиться на непривилегированного пользователя на хосте |
| Cgroup | Cgroup-иерархия | CLONE_NEWCGROUP | Контейнер видит собственную иерархию cgroups, а не всю иерархию хоста |
Пример
# Посмотреть namespaces контейнера по PID процесса
docker inspect --format '{{.State.Pid}}' my-container
# Например, PID = 12345
ls -la /proc/12345/ns/
# lrwxrwxrwx 1 root root 0 ... cgroup -> cgroup:[4026532234]
# lrwxrwxrwx 1 root root 0 ... ipc -> ipc:[4026532232]
# lrwxrwxrwx 1 root root 0 ... mnt -> mnt:[4026532230]
# lrwxrwxrwx 1 root root 0 ... net -> net:[4026532235]
# lrwxrwxrwx 1 root root 0 ... pid -> pid:[4026532233]
# lrwxrwxrwx 1 root root 0 ... user -> user:[4026531837]
# lrwxrwxrwx 1 root root 0 ... uts -> uts:[4026532231]
Каждый namespace идентифицируется числовым inode (числа в квадратных скобках). Контейнеры с одинаковыми inode делят один namespace. Например, если USER namespace совпадает с хостовым — значит, user namespace remapping не включён.
2. Cgroups (Control Groups — ограничение ресурсов):
Если namespaces отвечают за то, что контейнер видит, то cgroups отвечают за то, сколько ресурсов он может потребить:
| Подсистема | Что ограничивает |
|---|---|
| cpu | Процессорное время (CPU shares, quotas) |
| memory | Оперативная память (limits, swap) |
| blkio | Disk I/O (пропускная способность, IOPS) |
| pids | Количество процессов (защита от fork bomb) |
| cpuset | Привязка к конкретным ядрам CPU (CPU pinning) |
| devices | Доступ к устройствам хоста (/dev/*) |
Пример
# Просмотр cgroup-лимитов контейнера
cat /sys/fs/cgroup/memory/docker/<container_id>/memory.limit_in_bytes
cat /sys/fs/cgroup/cpu/docker/<container_id>/cpu.cfs_quota_us
cat /sys/fs/cgroup/pids/docker/<container_id>/pids.max
Для Java особенно важно: начиная с Java 10, JVM по умолчанию включает -XX:+UseContainerSupport и корректно читает cgroup-лимиты для автоматической настройки размера heap и количества потоков GC. Без этой поддержки JVM может попытаться использовать всю RAM хоста, что приведёт к OOMKill.
3. Seccomp (Secure Computing — фильтрация системных вызовов):
Seccomp позволяет фильтровать системные вызовы (syscalls), доступные процессу. Docker по умолчанию применяет профиль, блокирующий примерно 44 системных вызова из более чем 300, включая: reboot (перезагрузка хоста), mount (монтирование файловых систем), swapon (управление swap), clock_settime (изменение системного времени), bpf (загрузка BPF-программ) и другие потенциально опасные вызовы.
4. Linux Capabilities:
Традиционная модель привилегий Linux бинарна: процесс либо root (всемогущий), либо обычный пользователь. Capabilities дробят привилегии root на примерно 40 отдельных единиц, позволяя дать процессу только необходимые:
Пример
# Capabilities контейнера по умолчанию
docker run --rm alpine cat /proc/1/status | grep Cap
# CapPrm: 00000000a80425fb
# Включает: CHOWN, DAC_OVERRIDE, FSETID, FOWNER, NET_RAW и др.
Docker по умолчанию оставляет минимальный набор capabilities, но для максимальной безопасности рекомендуется --cap-drop=ALL с добавлением только действительно нужных.
5. Ограничения изоляции контейнеров:
Поскольку контейнеры разделяют ядро Linux с хостом, существуют принципиальные ограничения изоляции:
- Уязвимость ядра = потенциальный побег из контейнера (container escape). Эксплоит уязвимости ядра может дать злоумышленнику доступ к хостовой системе.
/procи/sysчастично доступны — некоторые файлы в этих виртуальных файловых системах содержат информацию о хосте.- Флаг
--privilegedполностью отключает изоляцию — контейнер получает доступ ко всем устройствам хоста и capabilities root. - Время (clock) является общим для всех контейнеров и хоста — нет способа изолировать системные часы.
6. Усиление изоляции (для высокочувствительных систем):
Когда стандартной контейнерной изоляции недостаточно, существуют технологии, добавляющие дополнительный уровень:
| Технология | Описание | Накладные расходы |
|---|---|---|
| gVisor (Google) | Виртуальное ядро в user-space, перехватывающее все syscalls контейнера | Средние (замедление syscall-heavy нагрузок) |
| Kata Containers | Лёгкие виртуальные машины с собственным ядром, OCI-совместимые | Низкие (быстрый старт, но больше RAM) |
| Firecracker (AWS) | microVM с минимальным VMM, используется в AWS Lambda и Fargate | Минимальные (старт за ~125 мс) |
Пример
# Использование gVisor как runtime class в Kubernetes
apiVersion: v1
kind: Pod
spec:
runtimeClassName: gvisor # Указать runtime class
containers:
- name: banking-app
image: registry.bank.local/banking-service:1.0.0
gVisor перехватывает все системные вызовы контейнера и выполняет их в собственном ядре (Sentry), работающем в user-space. Даже при наличии уязвимости в «ядре» gVisor атакующий не получит доступ к реальному ядру хоста. Для банковских систем с обработкой платёжных данных стоит рассмотреть Kata Containers или gVisor как дополнительный уровень изоляции.