19.2. Границы изоляции
Цели
После этого материала вы сможете:
- объяснить, почему общее ядро — ограничение принципиальное, а не устранимое;
- измерить площадь атаки на ядро числом, а не оценкой;
- назвать три класса container escape и указать, какой встречается чаще;
- объяснить, почему
rootв container'е по умолчанию равенrootна хосте; - выбрать средство изоляции под уровень доверия к коду;
- назвать, что остаётся общим даже в виртуальной машине.
Предварительные знания
- 17.3. Namespaces — шесть из восьми;
- 12.4. Capabilities и privileged;
- 12.5. Seccomp, AppArmor, SELinux;
- 17.1. Engine API и сокет — почему доступ к сокету равен
root.
Docker на машине курса не установлен. Измерения площади атаки выполняются на ядре хоста — они от Docker не зависят.
Ключевые термины
| Термин | Объяснение |
|---|---|
container escape | Выход процесса за пределы изоляции на хост |
граница доверия | Черта, за которой код считается недоверенным |
площадь атаки | Объём интерфейса, доступного атакующему |
побочный канал | Утечка сведений через измеримые свойства, а не через данные |
микро-ВМ | Виртуальная машина с минимальным набором устройств |
Теория
Одно ядро на всех
Container — это процесс. Изоляция обеспечивается тем, что ядро показывает процессу ограниченный вид системы. Само ядро при этом одно.
container A ─┐
container B ─┼──→ одно ядро Linux ──→ оборудование
container C ─┘
Отсюда следует утверждение, вокруг которого строится весь урок: любая уязвимость ядра доступна из любого container'а. Никакая настройка Docker этого не меняет, потому что интерфейс к ядру — системные вызовы — и есть то, ради чего процесс запускается.
Сравните с виртуальной машиной:
ВМ A (своё ядро) ─┐
ВМ B (своё ядро) ─┼──→ гипервизор ──→ оборудование
ВМ C (своё ядро) ─┘
Здесь уязвимость гостевого ядра не даёт доступа к соседям: нужна ещё уязвимость гипервизора, а его интерфейс несопоставимо меньше.
Разница не в качестве изоляции, а в размере поверхности, которую нужно защитить.
Площадь атаки, выраженная числом
Интерфейс ядра — системные вызовы. Их число можно посчитать, а не оценить: на x86-64 в текущих ядрах их больше трёхсот.
Профиль seccomp по умолчанию запрещает несколько десятков — те, что заведомо опасны или бесполезны в container'е. Остальные доступны.
| Слой | Что ограничивает | Что остаётся |
|---|---|---|
| Профиль seccomp | Несколько десятков вызовов | Основная часть интерфейса |
| Capabilities | Привилегированные операции | Непривилегированные — все |
| Namespaces | Видимость объектов | Сам интерфейс вызовов |
| Cgroups | Объём ресурсов | Не относится к безопасности |
| AppArmor или SELinux | Доступ к путям и операциям | Зависит от профиля |
Ни один слой не уменьшает число доступных вызовов радикально — они и не должны: приложение обязано работать.
Почему root в container'е — это root на хосте
Docker создаёт шесть namespaces из восьми (урок 17.3). Среди созданных нет user namespace.
Практическое следствие:
UID 0 внутри container'а == UID 0 на хосте
Разделяет их не преобразование идентификаторов, а namespaces и capabilities. Если процесс сумеет выйти за них — он окажется на хосте от имени root, без дополнительных шагов.
Именно поэтому три вещи ниже эквивалентны выдаче прав root на хост:
| Действие | Почему |
|---|---|
--privileged | Снимает seccomp, AppArmor, возвращает все capabilities |
Монтирование /var/run/docker.sock | Позволяет создать container с любыми параметрами (урок 17.1) |
CAP_SYS_ADMIN | Даёт монтирование и значительную часть операций root |
Первое очевидно, второе — нет: сокет выглядит как «просто файл», а даёт полный контроль над daemon'ом, работающим от root.
Включение user namespace (userns-remap) меняет картину: root внутри становится непривилегированным пользователем снаружи. Это заметно повышает планку — и ломает часть сценариев (тома, разделяемые сети, некоторые образы), поэтому по умолчанию не включено.
Три класса escape
| Класс | Механизм | Как часто |
|---|---|---|
| Уязвимость runtime | Ошибка в runc, containerd, daemon | Редко, но громко |
| Уязвимость ядра | Ошибка в обработке системного вызова | Регулярно |
| Неверная конфигурация | --privileged, сокет, лишние capabilities | Подавляющее большинство |
Третья строка — главный практический вывод. Публичные разборы escape в эксплуатации чаще описывают не уязвимость, а конфигурацию: смонтированный сокет, --privileged «чтобы заработало», проброшенный / в режиме чтения-записи.
Первые два класса иллюстрируются известными случаями в runc:
| Пример | Механизм |
|---|---|
| CVE-2019-5736 | Процесс в container'е перезаписывал двоичный файл runc на хосте через /proc/self/exe |
| CVE-2024-21626 | Утечка файлового дескриптора открывала доступ к файловой системе хоста |
Существенно не то, что уязвимости были, а то, что они будут: интерфейс между container'ом и хостом сложен, и полностью корректной реализации ожидать не стоит. Модель угроз должна это учитывать.
Недоверенный код
Отсюда правило, которое стоит запомнить в такой формулировке:
Container — граница между вашими сервисами, а не между вами и злоумышленником.
Если код недоверенный — присланный пользователем, скачанный, выполняемый по запросу, — одних container'ов недостаточно. Что применяют вместо:
| Средство | Как работает | Цена |
|---|---|---|
| user namespace | root внутри ≠ root снаружи | Ломает часть сценариев |
| gVisor | Ядро в пространстве пользователя перехватывает вызовы | Часть вызовов не поддержана; медленнее на I/O |
| Kata Containers | Каждый container — лёгкая ВМ со своим ядром | Расход памяти, дольше старт |
| Firecracker | Микро-ВМ с минимумом устройств | Требует своей инфраструктуры |
| Обычная ВМ | Своё ядро, зрелый гипервизор | Тяжелее по всем статьям |
| Отдельная машина | Общего нет ничего, кроме сети | Дороже всего |
Порядок в таблице — по возрастанию изоляции и стоимости. Выбор делается не по «уровню безопасности вообще», а по тому, кто именно недоверенный.
Побочные каналы
Даже полное разделение ядра не разделяет оборудование. Общими остаются:
| Что общее | Что может утечь |
|---|---|
| Кэш процессора | Данные соседа через измерение времени доступа |
| Ветвление предсказателя | То же семейство атак |
| Гиперпоточность | Соседний поток на том же физическом ядре |
| Полоса памяти и диска | Косвенные сведения о нагрузке |
| Источники времени | Точные измерения, нужные для атак |
Это относится и к виртуальным машинам: гипервизор разделяет ядра, но не физический кэш.
Практический вывод для многопользовательских систем: при действительно высоких требованиях изоляция достигается отдельным оборудованием, а не настройками. Промежуточные меры — отключение гиперпоточности, привязка к ядрам — снижают риск, но не устраняют класс.
Выбор по границе доверия
| Кто выполняет код | Достаточно |
|---|---|
| Ваша команда, ваш код | Container |
| Разные команды одной организации | Container + namespace + политики |
| Зависимости из публичных репозиториев | Container + сканирование + минимум прав |
| Код пользователей вашего продукта | Микро-ВМ или ВМ |
| Заведомо враждебный код (анализ вредоносного ПО) | Отдельная машина, отдельная сеть |
Строка «зависимости из публичных репозиториев» заслуживает отдельного внимания: код в них выполняется с правами вашего приложения и уже находится внутри границы. Container от него не защищает вовсе — он защищает хост от приложения, а не приложение от собственных зависимостей (урок 12.7).
Внутренний механизм
Почему уязвимости в интерфейсе неизбежны
Ядро обязано принимать данные от непривилегированного процесса и обрабатывать их корректно при любых значениях. Число сочетаний огромно: сотни вызовов, у каждого аргументы, у многих — структуры со ссылками.
Namespaces добавляют к этому измерение: тот же вызов должен вести себя правильно в зависимости от того, в каком namespace находится процесс, и правильно обрабатывать пересечение границ (/proc, /sys, файловые дескрипторы, setns).
Уязвимости runc из таблицы выше обе относятся именно к пересечению границы: файловый дескриптор или путь, полученный по одну сторону, использовался по другую.
Это объясняет, почему такие ошибки повторяются: сложность сосредоточена в местах, где изоляция должна прерываться контролируемым образом.
Что именно разделяет user namespace
Без user namespace ядро сравнивает идентификаторы напрямую: UID 0 — это UID 0.
С user namespace вводится преобразование: диапазон идентификаторов внутри отображается на другой диапазон снаружи.
внутри: UID 0 → снаружи: UID 100000
внутри: UID 1000 → снаружи: UID 101000
Процесс, вышедший за namespaces, оказывается снаружи под UID 100000 — обычным непривилегированным пользователем. Файлы root ему недоступны.
Ограничение остаётся: уязвимость самого ядра user namespace не спасает, потому что ядро всё то же. Планка поднимается, класс угрозы сохраняется.
Ограничение непривилегированных user namespace
Сам механизм user namespace исторически стал источником уязвимостей: он позволяет непривилегированному процессу получить CAP_SYS_ADMIN внутри своего namespace и через это добраться до кода ядра, ранее доступного только root.
Поэтому дистрибутивы ограничивают его создание. В Ubuntu 24.04 за это отвечает параметр kernel.apparmor_restrict_unprivileged_userns.
Здесь виден компромисс в чистом виде: тот же механизм, который усиливает изоляцию container'ов (userns-remap), ослабляет защиту хоста, если доступен всем. Универсально правильной настройки нет.
Команды и примеры
Измерение площади атаки
mkdir -p /tmp/isolation && cd /tmp/isolation
echo "═══ ядро и его интерфейс ═══"
uname -r
printf 'системных вызовов x86-64: %s\n' \
"$(grep -c '^#define __NR_' /usr/include/x86_64-linux-gnu/asm/unistd_64.h)"
echo
echo "═══ capabilities ═══"
grep CapBnd /proc/self/status
python3 - <<'PY'
from pathlib import Path
line = next(l for l in Path("/proc/self/status").read_text().splitlines()
if l.startswith("CapBnd:"))
value = int(line.split()[1], 16)
print(f" capabilities в bounding set: {bin(value).count('1')}")
print(" каждая — отдельная привилегированная операция ядра")
PY
echo
echo "═══ что видно из любого container'а ═══"
for p in kernel.hostname kernel.pid_max vm.max_map_count fs.file-max; do
printf ' %-20s %s\n' "$p" "$(sysctl -n "$p" 2>/dev/null)"
done
echo " версия ядра одна на всех: $(uname -r)"
Ожидаемый вывод:
═══ ядро и его интерфейс ═══
6.8.0-88-generic
системных вызовов x86-64: 373
═══ capabilities ═══
CapBnd: 000001ffffffffff
capabilities в bounding set: 41
каждая — отдельная привилегированная операция ядра
═══ что видно из любого container'а ═══
kernel.hostname egrebnev-vm
kernel.pid_max 4194304
vm.max_map_count 1048576
fs.file-max 9223372036854775807
версия ядра одна на всех: 6.8.0-88-generic
Числа получены на машине курса: 373 системных вызова и 41 capability. Профиль seccomp по умолчанию закрывает несколько десятков вызовов — то есть основная часть интерфейса остаётся доступной, и иначе быть не может: приложение обязано работать.
Строка версия ядра одна на всех — не риторика. Она означает буквально: uname -r внутри container'а вернёт то же самое, потому что ядро то же самое.
Ограничение user namespace
cd /tmp/isolation
echo "═══ настройка ограничения ═══"
sysctl kernel.apparmor_restrict_unprivileged_userns 2>/dev/null \
|| echo "параметр отсутствует (не Ubuntu 24.04+)"
echo
echo "═══ попытка создать user namespace без привилегий ═══"
unshare --user --map-root-user /bin/true 2>&1 || echo " код возврата: $?"
Ожидаемый вывод:
═══ настройка ограничения ═══
kernel.apparmor_restrict_unprivileged_userns = 1
═══ попытка создать user namespace без привилегий ═══
unshare: write failed /proc/self/uid_map: Operation not permitted
код возврата: 1
Значение параметра проверено на машине курса. Попытка действительно не удалась.
Существенная оговорка: приписать неудачу только этому параметру здесь нельзя. Среда, в которой выполняется пример, дополнительно ограничена — попытка создать mount namespace также завершается отказом, хотя к этому параметру она отношения не имеет. Корректный вывод: параметр включён, а причина конкретного отказа требует проверки на обычной машине.
Разбирать это стоит именно так, потому что подмена «команда не сработала» на «сработало ограничение X» — обычный источник ложных выводов при отладке изоляции.
Средства изоляции под уровень доверия
cd /tmp/isolation
cat > isolation.py <<'PY'
"""Выбор средства изоляции по границе доверия."""
from __future__ import annotations
import json
# (средство, своё ядро, свой UID-диапазон, общее оборудование, цена)
MEANS = [
("Container", False, False, True, "минимальная"),
("Container + userns-remap", False, True, True, "ломает часть сценариев"),
("gVisor", "частично", True, True, "часть вызовов не поддержана"),
("Kata Containers", True, True, True, "память и время старта"),
("Firecracker", True, True, True, "своя инфраструктура"),
("Виртуальная машина", True, True, True, "тяжелее по всем статьям"),
("Отдельная машина", True, True, False, "дороже всего"),
]
# (кто выполняет код, достаточное средство, почему)
BOUNDARIES = [
("Ваша команда, ваш код", "Container",
"граница между сервисами, а не против злоумышленника"),
("Разные команды организации", "Container + namespace + политики",
"ошибка соседа не должна доходить до вас"),
("Зависимости из публичных репозиториев", "Container + сканирование",
"код УЖЕ внутри границы: container от него не защищает"),
("Код пользователей продукта", "Микро-ВМ или ВМ",
"недоверенный код у общего ядра недопустим"),
("Заведомо враждебный код", "Отдельная машина и сеть",
"побочные каналы не устраняются настройками"),
]
# Что остаётся общим даже при полном разделении ядра
SHARED_ANYWAY = [
("Кэш процессора", "данные соседа через измерение времени"),
("Предсказатель переходов", "то же семейство атак"),
("Гиперпоточность", "соседний поток на том же физическом ядре"),
("Полоса памяти и диска", "косвенные сведения о нагрузке"),
]
def main() -> None:
print(f" {'средство':<26} {'своё ядро':<12} {'свои UID':<10} "
f"{'общее ЖЕЛЕЗО':<14} цена")
print(" " + "─" * 96)
for name, kernel, uids, hw, cost in MEANS:
k = {True: "да", False: "нет"}.get(kernel, str(kernel))
u = "да" if uids else "нет"
h = "да" if hw else "нет"
print(f" {name:<26} {k:<12} {u:<10} {h:<14} {cost}")
print()
print(f" {'кто выполняет код':<40} {'достаточно':<32} почему")
print(" " + "─" * 116)
for who, means, why in BOUNDARIES:
print(f" {who:<40} {means:<32} {why}")
print()
print(" Строка про зависимости — самая недооценённая: код из публичных")
print(" репозиториев выполняется с правами приложения и УЖЕ находится")
print(" внутри границы. Container защищает хост от приложения,")
print(" а не приложение от собственных зависимостей.")
print()
print(f" {'общее даже при своём ядре':<30} что может утечь")
print(" " + "─" * 76)
for what, leak in SHARED_ANYWAY:
print(f" {what:<30} {leak}")
print()
print(" Гипервизор разделяет вычислительные ядра, но не физический кэш.")
print(" При действительно высоких требованиях изоляция достигается")
print(" отдельным ОБОРУДОВАНИЕМ, а не настройками.")
print()
print(json.dumps({"средств": len(MEANS), "границ": len(BOUNDARIES),
"общее_всегда": len(SHARED_ANYWAY)}, ensure_ascii=False))
if __name__ == "__main__":
main()
PY
echo "═══ выбор средства ═══"
python3 isolation.py
Ожидаемый вывод:
═══ выбор средства ═══
средство своё ядро свои UID общее ЖЕЛЕЗО цена
────────────────────────────────────────────────────────────────────────────────────────────────
Container нет нет да минимальная
Container + userns-remap нет да да ломает часть сценариев
gVisor частично да да часть вызовов не поддержана
Kata Containers да да да память и время старта
Firecracker да да да своя инфраструктура
Виртуальная машина да да да тяжелее по всем статьям
Отдельная машина да да нет дороже всего
кто выполняет код достаточно почему
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Ваша команда, ваш код Container граница между сервисами, а не против злоумышленника
Разные команды организации Container + namespace + политики ошибка соседа не должна доходить до вас
Зависимости из публичных репозиториев Container + сканирование код УЖЕ внутри границы: container от него не защищает
Код пользователей продукта Микро-ВМ или ВМ недоверенный код у общего ядра недопустим
Заведомо враждебный код Отдельная машина и сеть побочные каналы не устраняются настройками
общее даже при своём ядре что может утечь
────────────────────────────────────────────────────────────────────────────
Кэш процессора данные соседа через измерение времени
...
Обратите внимание на столбец «общее железо»: он равен «да» у всех строк, кроме последней. Полное разделение даёт только отдельная машина.
Конфигурация как главный класс escape
cd /tmp/isolation
cat > audit.py <<'PY'
"""Разбор конфигураций, эквивалентных выдаче root на хост."""
from __future__ import annotations
import json
import sys
from pathlib import Path
import yaml
# (признак, что даёт атакующему, оправданные случаи)
DANGEROUS = {
"privileged": ("снимает seccomp и AppArmor, возвращает все capabilities",
"сборка образов, работа с устройствами — и то не всегда"),
"docker_sock": ("позволяет создать container с любыми параметрами: "
"это root на хосте",
"агенты CI — и только с осознанием последствий"),
"cap_sys_admin": ("монтирование и значительная часть операций root",
"почти никогда"),
"pid_host": ("видимость и сигналы всем процессам хоста",
"средства наблюдения"),
"network_host": ("сетевая изоляция снята полностью",
"производительность сети — ценой изоляции"),
"root_mount": ("файловая система хоста доступна изнутри",
"резервное копирование — только для чтения"),
"run_as_root": ("UID 0 внутри равен UID 0 снаружи",
"почти никогда"),
}
def audit_service(name: str, svc: dict) -> list[str]:
found: list[str] = []
if svc.get("privileged"):
found.append("privileged")
for v in svc.get("volumes") or []:
spec = v if isinstance(v, str) else f"{v.get('source')}:{v.get('target')}"
if "docker.sock" in spec:
found.append("docker_sock")
if spec.startswith("/:") or spec.startswith("/ "):
found.append("root_mount")
caps = [c.upper() for c in (svc.get("cap_add") or [])]
if "SYS_ADMIN" in caps or "ALL" in caps:
found.append("cap_sys_admin")
if svc.get("pid") == "host":
found.append("pid_host")
if svc.get("network_mode") == "host":
found.append("network_host")
user = str(svc.get("user", ""))
if user in ("0", "root", "0:0"):
found.append("run_as_root")
return found
def main(path: str) -> int:
compose = yaml.safe_load(Path(path).read_text())
result: dict[str, list[str]] = {}
for name, svc in compose.get("services", {}).items():
found = audit_service(name, svc or {})
if found:
result[name] = found
total = sum(len(v) for v in result.values())
print(f" {'сервис':<14} {'признак':<16} что это даёт")
print(" " + "─" * 96)
for name, findings in result.items():
for f in findings:
what, _ = DANGEROUS[f]
print(f" {name:<14} {f:<16} {what}")
print()
print(f" сервисов с находками: {len(result)}, находок всего: {total}")
print()
print(f" {'признак':<16} когда оправдан")
print(" " + "─" * 76)
seen = {f for v in result.values() for f in v}
for f in sorted(seen):
print(f" {f:<16} {DANGEROUS[f][1]}")
print()
print(" Все перечисленное — НЕ уязвимости, а конфигурация.")
print(" Именно этот класс составляет подавляющее большинство")
print(" случаев escape в эксплуатации.")
print()
print(json.dumps({"сервисов": len(result), "находок": total},
ensure_ascii=False))
# 0 — чисто, 1 — есть находки
return 1 if total else 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "compose.yaml"))
PY
cat > compose.yaml <<'EOF'
services:
ci-runner:
image: runner:1
volumes:
- /var/run/docker.sock:/var/run/docker.sock
user: "0:0"
monitoring:
image: monitor:1
pid: host
cap_add: [SYS_ADMIN]
backup:
image: backup:1
volumes:
- /:/host
proxy:
image: proxy:1
network_mode: host
app:
image: app:1
user: "10001:10001"
read_only: true
cap_drop: [ALL]
EOF
echo "═══ разбор конфигурации ═══"
python3 audit.py compose.yaml; echo " код возврата: $?"
cd /tmp && rm -rf /tmp/isolation
Ожидаемый вывод:
═══ разбор конфигурации ═══
сервис признак что это даёт
────────────────────────────────────────────────────────────────────────────────────────────────
ci-runner docker_sock позволяет создать container с любыми параметрами: это root на хосте
ci-runner run_as_root UID 0 внутри равен UID 0 снаружи
monitoring cap_sys_admin монтирование и значительная часть операций root
monitoring pid_host видимость и сигналы всем процессам хоста
backup root_mount файловая система хоста доступна изнутри
proxy network_host сетевая изоляция снята полностью
сервисов с находками: 4, находок всего: 6
признак когда оправдан
────────────────────────────────────────────────────────────────────────────
cap_sys_admin почти никогда
docker_sock агенты CI — и только с осознанием последствий
network_host производительность сети — ценой изоляции
pid_host средства наблюдения
root_mount резервное копирование — только для чтения
run_as_root почти никогда
Все перечисленное — НЕ уязвимости, а конфигурация.
Именно этот класс составляет подавляющее большинство
случаев escape в эксплуатации.
{"сервисов": 4, "находок": 6}
код возврата: 1
Сервис app в находки не попал — у него user, read_only и снятые capabilities. Это и есть проверка на отрицательном примере: инструмент, отмечающий всё подряд, бесполезен.
Практическое упражнение
Задание. Постройте инструмент оценки изоляции и примените его к конфигурации.
Требования:
- Измерить площадь атаки числом: системные вызовы и capabilities — на реальном ядре.
- Показать, что видно из любого container'а, потому что ядро общее.
- Разобрать
compose.yamlи найти конфигурации, эквивалентныеrootна хосте. - Проверить инструмент на безопасном сервисе: он не должен давать находок.
- Построить таблицу выбора средства изоляции по границе доверия.
- Назвать, что остаётся общим даже при своём ядре.
- Отметить всё, что не проверялось, и отделить проверенное от предполагаемого.
Подсказки
Подсказка 1
Число системных вызовов на x86-64 считается по заголовку ядра:
grep -c '^#define __NR_' /usr/include/x86_64-linux-gnu/asm/unistd_64.h
Подсказка 2
CapBnd в /proc/self/status — шестнадцатеричная маска. Число установленных битов и есть число capabilities.
Подсказка 3
Инструмент, находящий проблемы во всём, ничем не лучше инструмента, не находящего ничего. Нужен сервис, который проверку проходит.
Решение
Показать решение
mkdir -p /tmp/isolab && cd /tmp/isolab
cat > surface.py <<'PY'
"""Измерение площади атаки на общее ядро.
Все числа берутся из системы, а не задаются в коде: заголовок ядра
и /proc/self/status. Это отличает измерение от оценки.
"""
from __future__ import annotations
import json
import platform
import re
from pathlib import Path
SYSCALL_HEADERS = [
"/usr/include/x86_64-linux-gnu/asm/unistd_64.h",
"/usr/include/asm/unistd_64.h",
"/usr/include/asm-generic/unistd.h",
]
# Параметры, значение которых внутри container'а совпадает с хостом,
# потому что ядро одно.
SHARED_SYSCTL = [
"kernel.pid_max",
"vm.max_map_count",
"fs.file-max",
"kernel.threads-max",
]
def count_syscalls() -> tuple[int, str]:
for path in SYSCALL_HEADERS:
p = Path(path)
if p.exists():
n = len(re.findall(r"^#define __NR_", p.read_text(), re.M))
return n, path
return 0, "заголовок не найден"
def count_capabilities() -> int:
for line in Path("/proc/self/status").read_text().splitlines():
if line.startswith("CapBnd:"):
return bin(int(line.split()[1], 16)).count("1")
return 0
def read_sysctl(name: str) -> str:
p = Path("/proc/sys") / name.replace(".", "/")
try:
return p.read_text().strip()
except OSError:
return "недоступен"
def main() -> None:
syscalls, header = count_syscalls()
caps = count_capabilities()
result = {
"ядро": platform.release(),
"системных_вызовов": syscalls,
"источник": header,
"capabilities": caps,
"общие_параметры": {n: read_sysctl(n) for n in SHARED_SYSCTL},
"userns_ограничен": read_sysctl("kernel.apparmor_restrict_unprivileged_userns"),
}
print(json.dumps(result, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
PY
cat > audit.py <<'PY'
"""Поиск конфигураций, эквивалентных выдаче root на хост.
Ищутся не уязвимости, а решения в конфигурации: именно этот класс
составляет подавляющее большинство случаев escape в эксплуатации.
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
import yaml
# (признак, вес, что даёт атакующему, когда оправдан)
RULES = {
"privileged": (10, "снимает seccomp и AppArmor, возвращает все capabilities",
"сборка образов, и то не всегда"),
"docker_sock": (10, "создание container с любыми параметрами — это root на хосте",
"агенты CI, с осознанием последствий"),
"cap_sys_admin": (8, "монтирование и значительная часть операций root",
"почти никогда"),
"root_mount_rw": (9, "файловая система хоста доступна на запись",
"никогда"),
"root_mount_ro": (4, "файловая система хоста доступна на чтение",
"резервное копирование"),
"pid_host": (5, "видимость и сигналы всем процессам хоста",
"средства наблюдения"),
"network_host": (4, "сетевая изоляция снята полностью",
"производительность сети ценой изоляции"),
"run_as_root": (6, "UID 0 внутри равен UID 0 снаружи",
"почти никогда"),
"cap_add_all": (10, "все capabilities возвращены",
"никогда"),
"seccomp_off": (7, "фильтр системных вызовов отключён",
"отладка, временно"),
}
def audit_service(svc: dict) -> list[str]:
found: list[str] = []
if svc.get("privileged"):
found.append("privileged")
for v in svc.get("volumes") or []:
if isinstance(v, str):
parts = v.split(":")
source = parts[0]
mode = parts[2] if len(parts) > 2 else "rw"
else:
source = str(v.get("source", ""))
mode = "ro" if v.get("read_only") else "rw"
if "docker.sock" in source:
found.append("docker_sock")
elif source == "/":
found.append("root_mount_ro" if mode == "ro" else "root_mount_rw")
caps = {c.upper() for c in (svc.get("cap_add") or [])}
if "ALL" in caps:
found.append("cap_add_all")
elif "SYS_ADMIN" in caps:
found.append("cap_sys_admin")
if svc.get("pid") == "host":
found.append("pid_host")
if svc.get("network_mode") == "host":
found.append("network_host")
user = str(svc.get("user", "")).strip()
if user in ("0", "root", "0:0"):
found.append("run_as_root")
for opt in svc.get("security_opt") or []:
if "seccomp=unconfined" in str(opt):
found.append("seccomp_off")
return found
def main(path: str) -> int:
compose = yaml.safe_load(Path(path).read_text())
services = compose.get("services", {})
report: dict[str, dict[str, object]] = {}
for name, svc in services.items():
found = audit_service(svc or {})
report[name] = {
"находки": found,
"вес": sum(RULES[f][0] for f in found),
}
total = sum(int(r["вес"]) for r in report.values())
clean = [n for n, r in report.items() if not r["находки"]]
print(json.dumps({
"сервисов": len(services),
"чистых": clean,
"суммарный_вес": total,
"отчёт": report,
"правила": {k: {"вес": v[0], "даёт": v[1], "оправдан": v[2]}
for k, v in RULES.items()},
}, ensure_ascii=False))
return 1 if total else 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "compose.yaml"))
PY
cat > choose.py <<'PY'
"""Выбор средства изоляции по границе доверия."""
from __future__ import annotations
import json
MEANS = [
("Container", "нет", "нет", "да", "минимальная"),
("Container + userns-remap", "нет", "да", "да", "ломает часть сценариев"),
("gVisor", "частично", "да", "да", "часть вызовов не поддержана"),
("Kata Containers", "да", "да", "да", "память и время старта"),
("Firecracker", "да", "да", "да", "своя инфраструктура"),
("Виртуальная машина", "да", "да", "да", "тяжелее по всем статьям"),
("Отдельная машина", "да", "да", "НЕТ", "дороже всего"),
]
BOUNDARIES = [
("Ваша команда, ваш код", "Container",
"граница между сервисами, а не против злоумышленника"),
("Разные команды организации", "Container + политики",
"ошибка соседа не должна доходить до вас"),
("Зависимости из публичных репозиториев", "Container + сканирование",
"код УЖЕ внутри границы: container от него не защищает"),
("Код пользователей продукта", "Микро-ВМ или ВМ",
"недоверенный код у общего ядра недопустим"),
("Заведомо враждебный код", "Отдельная машина и сеть",
"побочные каналы не устраняются настройками"),
]
SHARED_ANYWAY = [
("Кэш процессора", "данные соседа через измерение времени доступа"),
("Предсказатель переходов", "то же семейство атак"),
("Гиперпоточность", "соседний поток на том же физическом ядре"),
("Полоса памяти и диска", "косвенные сведения о нагрузке соседа"),
("Источники точного времени", "измерения, нужные для самих атак"),
]
ESCAPE_CLASSES = [
("Уязвимость runtime", "ошибка в runc, containerd, daemon", "редко, но громко"),
("Уязвимость ядра", "ошибка в обработке системного вызова", "регулярно"),
("Неверная конфигурация", "privileged, сокет, лишние capabilities",
"ПОДАВЛЯЮЩЕЕ БОЛЬШИНСТВО"),
]
def main() -> None:
print(f" {'средство':<26} {'своё ядро':<12} {'свои UID':<10} "
f"{'общее железо':<14} цена")
print(" " + "─" * 94)
for name, kernel, uids, hw, cost in MEANS:
print(f" {name:<26} {kernel:<12} {uids:<10} {hw:<14} {cost}")
only_full = [m[0] for m in MEANS if m[3] == "НЕТ"]
print()
print(f" полное разделение даёт только: {', '.join(only_full)}")
print()
print(f" {'класс escape':<26} {'механизм':<44} частота")
print(" " + "─" * 96)
for name, mech, freq in ESCAPE_CLASSES:
print(f" {name:<26} {mech:<44} {freq}")
print()
print(f" {'кто выполняет код':<40} {'достаточно':<28} почему")
print(" " + "─" * 112)
for who, means, why in BOUNDARIES:
print(f" {who:<40} {means:<28} {why}")
print()
print(f" {'общее даже при своём ядре':<30} что может утечь")
print(" " + "─" * 78)
for what, leak in SHARED_ANYWAY:
print(f" {what:<30} {leak}")
print()
print(json.dumps({"средств": len(MEANS), "границ": len(BOUNDARIES),
"общее_всегда": len(SHARED_ANYWAY),
"классов_escape": len(ESCAPE_CLASSES)}, ensure_ascii=False))
if __name__ == "__main__":
main()
PY
cat > compose.yaml <<'EOF'
services:
ci-runner:
image: runner:1
volumes:
- /var/run/docker.sock:/var/run/docker.sock
user: "0:0"
monitoring:
image: monitor:1
pid: host
cap_add: [SYS_ADMIN]
backup:
image: backup:1
volumes:
- /:/host:ro
legacy:
image: legacy:1
privileged: true
security_opt:
- seccomp=unconfined
app:
image: app:1
user: "10001:10001"
read_only: true
cap_drop: [ALL]
security_opt:
- no-new-privileges:true
EOF
fail=0
ok() { printf ' ✓ %s\n' "$1"; }
bad() { printf ' ✗ %s\n' "$1"; fail=1; }
printf '\n═══ Требование 1: площадь атаки числом ═══\n'
python3 surface.py > surface.json
python3 - <<'PY'
import json
d = json.load(open("surface.json"))
print(f" ядро: {d['ядро']}")
print(f" системных вызовов: {d['системных_вызовов']}")
print(f" источник числа: {d['источник']}")
print(f" capabilities в bounding: {d['capabilities']}")
print()
print(" Профиль seccomp по умолчанию закрывает несколько десятков вызовов.")
print(" Основная часть интерфейса остаётся доступной — и иначе быть")
print(" не может: приложение обязано работать.")
print()
print(" Это и есть площадь атаки на ОБЩЕЕ ядро: она измерена,")
print(" а не оценена, и не зависит от настроек Docker.")
PY
syscalls="$(python3 -c "import json;print(json.load(open('surface.json'))['системных_вызовов'])")"
caps="$(python3 -c "import json;print(json.load(open('surface.json'))['capabilities'])")"
printf '\n вызовов: %s, capabilities: %s\n' "$syscalls" "$caps"
[ "$syscalls" -gt 300 ] && [ "$caps" -gt 30 ] \
&& ok "площадь атаки измерена на реальном ядре" \
|| bad "вызовов $syscalls, capabilities $caps"
printf '\n═══ Требование 2: что общее, потому что ядро одно ═══\n'
python3 - <<'PY'
import json
d = json.load(open("surface.json"))
print(f" {'параметр':<24} значение (одно на хост и все container'ы)")
print(" " + "─" * 72)
for name, value in d["общие_параметры"].items():
print(f" {name:<24} {value}")
print()
print(f" версия ядра: {d['ядро']} — uname -r внутри вернёт то же самое")
print()
print(f" ограничение непривилегированных user namespace: "
f"{d['userns_ограничен']}")
print()
print(" Тот же механизм, который УСИЛИВАЕТ изоляцию container'ов")
print(" (userns-remap), ОСЛАБЛЯЕТ защиту хоста, если доступен всем.")
print(" Универсально правильной настройки нет.")
PY
shared="$(python3 -c "
import json
print(len(json.load(open('surface.json'))['общие_параметры']))")"
[ "$shared" -ge 3 ] \
&& ok "общие параметры прочитаны из системы, а не перечислены по памяти" \
|| bad "параметров: $shared"
printf '\n═══ Требование 3: конфигурации, равные root на хосте ═══\n'
python3 audit.py compose.yaml > audit.json; audit_code=$?
python3 - <<'PY'
import json
d = json.load(open("audit.json"))
rules = d["правила"]
print(f" {'сервис':<14} {'признак':<16} {'вес':>4} что даёт")
print(" " + "─" * 100)
for name, r in d["отчёт"].items():
for f in r["находки"]:
print(f" {name:<14} {f:<16} {rules[f]['вес']:>4} {rules[f]['даёт']}")
print()
print(f" суммарный вес: {d['суммарный_вес']}")
print()
print(f" {'признак':<16} когда оправдан")
print(" " + "─" * 66)
seen = sorted({f for r in d["отчёт"].values() for f in r["находки"]})
for f in seen:
print(f" {f:<16} {rules[f]['оправдан']}")
print()
print(" Всё перечисленное — НЕ уязвимости, а конфигурация.")
print(" Именно этот класс составляет подавляющее большинство")
print(" случаев escape в эксплуатации.")
PY
weight="$(python3 -c "import json;print(json.load(open('audit.json'))['суммарный_вес'])")"
printf '\n код возврата: %s, суммарный вес находок: %s\n' "$audit_code" "$weight"
[ "$audit_code" -eq 1 ] && [ "$weight" -gt 0 ] \
&& ok "опасные конфигурации найдены, код возврата отличает их от чистой" \
|| bad "код $audit_code, вес $weight"
printf '\n═══ Требование 4: проверка на безопасном сервисе ═══\n'
python3 - <<'PY'
import json
d = json.load(open("audit.json"))
clean = d["чистых"]
print(f" сервисов всего: {d['сервисов']}")
print(f" без единой находки: {clean}")
print()
if clean:
print(" Сервис app не попал в находки: user числовой, read_only,")
print(" capabilities сняты, повышение привилегий запрещено.")
print()
print(" Это проверка инструмента, а не конфигурации: средство,")
print(" отмечающее ВСЁ, ничем не лучше средства, не отмечающего")
print(" ничего. Без чистого сервиса находки ничего не значат.")
else:
print(" ПРОБЛЕМА: чистых сервисов нет — инструмент не различает")
PY
n_clean="$(python3 -c "import json;print(len(json.load(open('audit.json'))['чистых']))")"
[ "$n_clean" -ge 1 ] \
&& ok "есть сервис, проходящий проверку — находки различимы" \
|| bad "чистых сервисов: $n_clean"
printf '\n═══ Требования 5 и 6: выбор средства и что общее всегда ═══\n'
python3 choose.py > choose.log
sed -n '1,40p' choose.log | sed 's/^/ /'
means="$(tail -1 choose.log | python3 -c "import json,sys;print(json.load(sys.stdin)['средств'])")"
always="$(tail -1 choose.log | python3 -c "import json,sys;print(json.load(sys.stdin)['общее_всегда'])")"
printf '\n средств: %s, общего даже при своём ядре: %s\n' "$means" "$always"
[ "$means" -ge 6 ] && [ "$always" -ge 4 ] \
&& ok "таблица выбора построена; названо общее, не устранимое настройками" \
|| bad "средств $means, общего $always"
printf '\n═══ Требование 7: проверенное и предполагаемое ═══\n'
python3 - <<'PY'
VERIFIED = [
("число системных вызовов", "прочитано из заголовка ядра"),
("число capabilities", "прочитано из /proc/self/status"),
("общие параметры ядра", "прочитаны из /proc/sys"),
("ограничение user namespace", "прочитано значение sysctl"),
("разбор конфигурации", "выполнен на файле, включая чистый сервис"),
]
NOT_RUN = [
("любой container escape", "Docker не установлен; воспроизведение эксплойта не входит в задачу"),
("действие профиля seccomp", "требует запущенного container'а"),
("поведение userns-remap", "требует настройки daemon"),
("побочные каналы", "требует специальных условий и оборудования"),
("причина отказа unshare", "среда дополнительно ограничена: mount ns тоже недоступен"),
]
print(f" {'ВЫПОЛНЕНО':<34} как")
print(" " + "─" * 84)
for name, how in VERIFIED:
print(f" {name:<34} {how}")
print()
print(f" {'НЕ ВЫПОЛНЯЛОСЬ':<34} причина")
print(" " + "─" * 104)
for name, why in NOT_RUN:
print(f" {name:<34} {why}")
print()
print(f" проверено: {len(VERIFIED)}, не проверялось: {len(NOT_RUN)}")
print()
print(" Последняя строка важнее остальных: отказ unshare НЕЛЬЗЯ")
print(" приписать одному лишь параметру apparmor_restrict — среда")
print(" ограничена дополнительно. Подмена «команда не сработала»")
print(" на «сработало ограничение X» — обычный источник ложных")
print(" выводов при отладке изоляции.")
print()
print(" Частота классов escape («подавляющее большинство —")
print(" конфигурация») взята из публичных разборов, а не измерена здесь.")
PY
ok "проверенное отделено от предполагаемого; причина отказа не додумана"
printf '\n═══ ИТОГ ═══\n'
[ "$fail" -eq 0 ] && echo " все требования выполнены" || echo " ЕСТЬ ПРОВАЛЫ"
echo " примечание: Docker не использовался; измерения выполнены на ядре хоста"
cd /tmp && rm -rf /tmp/isolab
exit "$fail"
Ожидаемый вывод:
═══ Требование 1: площадь атаки числом ═══
ядро: 6.8.0-88-generic
системных вызовов: 373
источник числа: /usr/include/x86_64-linux-gnu/asm/unistd_64.h
capabilities в bounding: 41
Профиль seccomp по умолчанию закрывает несколько десятков вызовов.
Основная часть интерфейса остаётся доступной — и иначе быть
не может: приложение обязано работать.
Это и есть площадь атаки на ОБЩЕЕ ядро: она измерена,
а не оценена, и не зависит от настроек Docker.
вызовов: 373, capabilities: 41
✓ площадь атаки измерена на реальном ядре
═══ Требование 2: что общее, потому что ядро одно ═══
параметр значение (одно на хост и все container'ы)
────────────────────────────────────────────────────────────────────────
kernel.pid_max 4194304
vm.max_map_count 1048576
fs.file-max 9223372036854775807
kernel.threads-max ...
версия ядра: 6.8.0-88-generic — uname -r внутри вернёт то же самое
ограничение непривилегированных user namespace: 1
Тот же механизм, который УСИЛИВАЕТ изоляцию container'ов
(userns-remap), ОСЛАБЛЯЕТ защиту хоста, если доступен всем.
✓ общие параметры прочитаны из системы, а не перечислены по памяти
═══ Требование 3: конфигурации, равные root на хосте ═══
сервис признак вес что даёт
────────────────────────────────────────────────────────────────────────────────────────────────────
ci-runner docker_sock 10 создание container с любыми параметрами — это root на хосте
ci-runner run_as_root 6 UID 0 внутри равен UID 0 снаружи
monitoring cap_sys_admin 8 монтирование и значительная часть операций root
monitoring pid_host 5 видимость и сигналы всем процессам хоста
backup root_mount_ro 4 файловая система хоста доступна на чтение
legacy privileged 10 снимает seccomp и AppArmor, возвращает все capabilities
legacy seccomp_off 7 фильтр системных вызовов отключён
суммарный вес: 50
признак когда оправдан
──────────────────────────────────────────────────────────────────
cap_sys_admin почти никогда
docker_sock агенты CI, с осознанием последствий
pid_host средства наблюдения
privileged сборка образов, и то не всегда
root_mount_ro резервное копирование
run_as_root почти никогда
seccomp_off отладка, временно
код возврата: 1, суммарный вес находок: 50
✓ опасные конфигурации найдены, код возврата отличает их от чистой
═══ Требование 4: проверка на безопасном сервисе ═══
сервисов всего: 5
без единой находки: ['app']
Сервис app не попал в находки: user числовой, read_only,
capabilities сняты, повышение привилегий запрещено.
Это проверка инструмента, а не конфигурации: средство,
отмечающее ВСЁ, ничем не лучше средства, не отмечающего
ничего. Без чистого сервиса находки ничего не значат.
✓ есть сервис, проходящий проверку — находки различимы
═══ Требования 5 и 6: выбор средства и что общее всегда ═══
средство своё ядро свои UID общее железо цена
──────────────────────────────────────────────────────────────────────────────────────────────
Container нет нет да минимальная
Container + userns-remap нет да да ломает часть сценариев
gVisor частично да да часть вызовов не поддержана
Kata Containers да да да память и время старта
Firecracker да да да своя инфраструктура
Виртуальная машина да да да тяжелее по всем статьям
Отдельная машина да да НЕТ дороже всего
полное разделение даёт только: Отдельная машина
класс escape механизм частота
────────────────────────────────────────────────────────────────────────────────────────────────
Уязвимость runtime ошибка в runc, containerd, daemon редко, но громко
Уязвимость ядра ошибка в обработке системного вызова регулярно
Неверная конфигурация privileged, сокет, лишние capabilities ПОДАВЛЯЮЩЕЕ БОЛЬШИНСТВО
...
✓ таблица выбора построена; названо общее, не устранимое настройками
═══ Требование 7: проверенное и предполагаемое ═══
ВЫПОЛНЕНО как
────────────────────────────────────────────────────────────────────────────────────
число системных вызовов прочитано из заголовка ядра
число capabilities прочитано из /proc/self/status
общие параметры ядра прочитаны из /proc/sys
ограничение user namespace прочитано значение sysctl
разбор конфигурации выполнен на файле, включая чистый сервис
НЕ ВЫПОЛНЯЛОСЬ причина
────────────────────────────────────────────────────────────────────────────────────────────────────────
любой container escape Docker не установлен; воспроизведение эксплойта не входит в задачу
действие профиля seccomp требует запущенного container'а
поведение userns-remap требует настройки daemon
побочные каналы требует специальных условий и оборудования
причина отказа unshare среда дополнительно ограничена: mount ns тоже недоступен
проверено: 5, не проверялось: 5
Последняя строка важнее остальных: отказ unshare НЕЛЬЗЯ
приписать одному лишь параметру apparmor_restrict — среда
ограничена дополнительно.
✓ проверенное отделено от предполагаемого; причина отказа не додумана
═══ ИТОГ ═══
все требования выполнены
примечание: Docker не использовался; измерения выполнены на ядре хоста
Все требования выполнены. Числа — 373 системных вызова, 41 capability, значения общих параметров ядра — прочитаны из системы на машине курса.
Три решения, определяющие качество.
Числа читаются из системы, а не задаются в коде. Написать SYSCALLS = 373 было бы проще и выглядело бы так же. Но тогда инструмент показывал бы число, взятое из чужой машины, — а весь смысл в том, что площадь атаки принадлежит вашему ядру. Заголовок ядра и /proc/self/status дают ответ для той системы, где инструмент запущен.
Есть сервис, проходящий проверку. Из пяти сервисов один — app — не даёт ни одной находки. Без него отчёт «нашли семь проблем» не отличался бы от отчёта инструмента, который отмечает любую конфигурацию. Это тот же принцип, что и проверка проверяющего в уроке 19.1.
Причина отказа unshare не додумана. Соблазн был написать «попытка отклонена, потому что apparmor_restrict_unprivileged_userns=1» — связь выглядит очевидной. Но в той же среде не создаётся и mount namespace, к которому этот параметр отношения не имеет, значит ограничение шире. Честный вывод: параметр включён, а причина конкретного отказа требует проверки на обычной машине. Подмена «команда не сработала» на «сработало ограничение X» — обычный источник ложных выводов при отладке изоляции.
Чего решение не делает. Ни один escape не воспроизведён: Docker не установлен, да и воспроизведение эксплойтов не входит в задачу курса. Действие профиля seccomp, поведение userns-remap и побочные каналы не проверялись — все три требуют запущенной системы или специальных условий. Веса в правилах разбора назначены по опыту, а не выведены: они годятся для упорядочивания находок, но «суммарный вес 50» не значит ничего в отрыве от списка. Утверждение о частоте классов escape («подавляющее большинство — конфигурация») взято из публичных разборов, а не измерено здесь, и приведено именно как утверждение, а не как результат.
Проверка результата
grep -c '^#define __NR_' /usr/include/x86_64-linux-gnu/asm/unistd_64.h
grep CapBnd /proc/self/status
uname -r
Для своей конфигурации ответьте на один вопрос: чей код здесь выполняется? Если хотя бы часть его написана не вашей командой и не проверена, container — не та граница, на которую можно опираться.
Типичные ошибки
| Ошибка | Причина | Исправление |
|---|---|---|
| Считать container границей безопасности | Так говорят | Это граница между вашими сервисами |
| Запускать недоверенный код в container'е | Кажется изолированным | Нужна ВМ или отдельная машина |
Монтировать docker.sock «только для чтения» | Выглядит безопасно | Через API создаётся любой container: это root |
--privileged «чтобы заработало» | Быстро решает проблему | Снимает всю защиту разом |
| Полагаться на seccomp как на главную меру | Профиль есть по умолчанию | Закрывает малую часть интерфейса |
| Считать, что уязвимости кончились | Давно не было громких | Класс сохраняется: интерфейс сложен |
| Ждать защиты от зависимостей | Container изолирует | Зависимости уже внутри границы |
| Приписывать отказ одной причине | Совпало по времени | Проверить, не шире ли ограничение |
| Полагать, что ВМ решает всё | Своё ядро | Оборудование остаётся общим |
Контрольные вопросы
На понимание:
- Почему уязвимость ядра доступна из любого container'а?
- Почему
rootв container'е по умолчанию равенrootна хосте? - Какой класс escape встречается чаще всего?
- Что именно даёт user namespace и чего не даёт?
- Что остаётся общим даже при своём ядре?
На применение:
- Как измерить площадь атаки на ядро своей машины?
- Какое средство выбрать для кода пользователей продукта?
- Почему монтирование
docker.sockравно правамrootна хосте?
На диагностику:
- В конфигурации есть
privileged: trueиseccomp=unconfined. Что осталось от изоляции? unshareвозвращает отказ. Как убедиться, что причина именно в настройке, о которой вы подумали?
Краткое резюме
- Ядро одно на все container'ы: любая его уязвимость доступна из любого container'а.
- Разница с виртуальной машиной — в размере поверхности, которую нужно защищать.
- Площадь атаки измерима: сотни системных вызовов и десятки capabilities.
- Профиль seccomp закрывает малую часть интерфейса — иначе приложение не заработает.
- Docker не создаёт user namespace, поэтому UID 0 внутри равен UID 0 снаружи.
--privileged,docker.sockиCAP_SYS_ADMINэквивалентны выдачеrootна хост.- Три класса escape: runtime, ядро, конфигурация — и третий встречается чаще всех.
- Container — граница между вашими сервисами, а не между вами и злоумышленником.
- Для недоверенного кода применяют user namespace, gVisor, Kata, микро-ВМ или отдельную машину.
userns-remapподнимает планку, но не меняет класс угрозы: ядро то же.- Тот же механизм, что усиливает изоляцию container'ов, ослабляет защиту хоста, если доступен всем.
- Оборудование остаётся общим даже в виртуальной машине; полное разделение даёт только отдельная машина.
Официальные источники
| Источник | Ссылка | Что подтверждает |
|---|---|---|
| Docker: безопасность | https://docs.docker.com/engine/security/ | Модель угроз и её границы |
| Docker: user namespace | https://docs.docker.com/engine/security/userns-remap/ | Преобразование идентификаторов |
| Docker: seccomp | https://docs.docker.com/engine/security/seccomp/ | Профиль по умолчанию |
| Kernel: namespaces | https://man7.org/linux/man-pages/man7/namespaces.7.html | Что именно разделяется |
| Kernel: capabilities | https://man7.org/linux/man-pages/man7/capabilities.7.html | Полный список привилегий |
| Kernel: user_namespaces | https://man7.org/linux/man-pages/man7/user_namespaces.7.html | Преобразование UID и ограничения |
| gVisor | https://gvisor.dev/docs/ | Перехват вызовов в пространстве пользователя |
| Kata Containers | https://katacontainers.io/docs/ | Лёгкая ВМ на container |
| Firecracker | https://firecracker-microvm.github.io/ | Микро-ВМ |
| NVD: CVE-2019-5736 | https://nvd.nist.gov/vuln/detail/CVE-2019-5736 | Перезапись runc через /proc/self/exe |
| NVD: CVE-2024-21626 | https://nvd.nist.gov/vuln/detail/CVE-2024-21626 | Утечка файлового дескриптора в runc |
Навигация
← Предыдущий материал
Вернуться к разделу
Следующий материал → Когда Docker применять не следует
Главное оглавление