12.1. Модель угроз
Цели
После этого материала вы сможете:
- объяснить, почему изоляция container не равна границе безопасности, и назвать условия, при которых она ей становится;
- перечислить, что контейнеризация защищает, а что нет;
- построить модель угроз для конкретного сервиса по воспроизводимой процедуре;
- определить границы доверия в своей системе и назвать активы за каждой;
- оценить, какие меры оправданы для вашего сценария, а какие избыточны;
- отличить угрозу от неудобства и расставить приоритеты.
Предварительные знания
Ключевые термины
| Термин | Объяснение |
|---|---|
граница доверия | Рубеж, при пересечении которого меняется уровень доверия к данным или коду |
актив | То, что имеет ценность и подлежит защите |
вектор | Путь, которым угроза может реализоваться |
поверхность атаки | Совокупность точек, доступных для воздействия |
container escape | Выход из изоляции container на host |
защита в глубину | Несколько независимых рубежей |
Теория
Container — это процесс
Ключевое утверждение раздела:
Container — обычный процесс host-системы, ограниченный namespaces, cgroups и capabilities. Все они реализованы в том же ядре, что и всё остальное.
Сравнение с виртуальной машиной:
| Container | Виртуальная машина | |
|---|---|---|
| Ядро | Общее с host | Своё |
| Граница изоляции | Механизмы ядра | Гипервизор |
| Поверхность атаки | Все системные вызовы ядра | Интерфейс гипервизора |
| Уязвимость в ядре | Затрагивает host и все container'ы | Ограничена гостем |
| Накладные расходы | Минимальные | Заметные |
| Время запуска | Миллисекунды | Секунды |
Средняя строка объясняет всё остальное: приложение в container'е обращается к тому же ядру, что и host. Уязвимость в обработчике системного вызова доступна изнутри container'а.
Отсюда официальная формулировка Docker: container'ы не следует считать достаточной границей для запуска недоверенного кода без дополнительных мер.
Когда изоляции достаточно, а когда нет
| Сценарий | Достаточно ли container'а |
|---|---|
| Ваш код, ваша инфраструктура | Да, при базовом hardening |
| Код команды, общий кластер | Да, при hardening и политиках |
| Открытый код из репозитория | Условно: нужен hardening плюс сканирование |
| Код пользователей платформы | Нет — нужны gVisor, Kata или VM |
| Заведомо недоверенный код | Нет |
| Многопользовательская среда с враждебными арендаторами | Нет |
Три нижние строки — область, где применяют изоляцию уровня ядра-песочницы (gVisor), лёгкие виртуальные машины (Kata Containers, Firecracker) или обычные VM.
Для типичного случая — вы собираете и запускаете свой сервис — контейнеризации достаточно, и раздел посвящён именно ему.
Что контейнеризация защищает
| Защищает | Как |
|---|---|
| Файловую систему host от случайной записи | Своя mount namespace |
| Процессы host от вмешательства | Своя PID namespace |
| Сеть host от привязки к портам | Своя network namespace |
| Ресурсы узла от исчерпания | cgroups |
| Другие container'ы от прямого доступа | Совокупность namespaces |
Что не защищает
| Не защищает | Почему |
|---|---|
| От уязвимости в приложении | SQL-инъекция работает от любого UID |
| От уязвимости в ядре | Ядро общее |
| От избыточных привилегий | Вы сами их выдали |
| От доступа к Docker socket | Это доступ к демону, а тот — root |
| От вредоносного базового образа | Он часть вашего образа |
| От утечки данных через API приложения | Приложение имеет доступ по определению |
| От компрометации при сборке | Сборка происходит до изоляции |
Строка про socket — важнейшая практически (урок 12.2).
Процедура построения модели угроз
Модель угроз — не документ на сто страниц, а таблица на страницу, отвечающая на четыре вопроса.
| Шаг | Вопрос |
|---|---|
| 1 | Что защищаем? (активы) |
| 2 | От кого? (нарушители) |
| 3 | Как могут добраться? (векторы) |
| 4 | Что делаем? (меры и остаточный риск) |
Шаг 1: активы. Не «сервис», а конкретные вещи с ценностью:
| Актив | Почему ценен |
|---|---|
| Персональные данные пользователей | Регуляторные требования, репутация |
| Учётные данные к базе | Доступ ко всем данным |
| Исходный код | Интеллектуальная собственность |
| Вычислительные ресурсы | Майнинг, рассылка |
| Доверие к домену | Фишинг от вашего имени |
| Доступ к внутренней сети | Плацдарм для дальнейшего продвижения |
Два последних забывают чаще прочих — а именно они делают взлом второстепенного сервиса началом большой проблемы.
Шаг 2: нарушители. Реалистичные, а не гипотетические:
| Нарушитель | Возможности | Мотив |
|---|---|---|
| Внешний, без доступа | Запросы к публичному API | Данные, ресурсы |
| Пользователь сервиса | Аутентифицированные запросы | Чужие данные, повышение прав |
| Скомпрометированная зависимость | Выполнение кода при сборке или работе | Что угодно |
| Сотрудник с доступом к CI | Изменение образа | Зависит от роли |
| Сосед по узлу | Другой container | Побег и продвижение |
Шаг 3: векторы. Как нарушитель добирается до актива:
внешний ──► уязвимость приложения ──► выполнение кода в container
│
├──► данные в базе (актив)
├──► секреты в окружении (актив)
├──► сеть: соседние сервисы
└──► попытка побега на host
Шаг 4: меры и остаточный риск. Для каждого вектора — что делаем и что остаётся:
| Вектор | Мера | Остаточный риск |
|---|---|---|
| Выполнение кода в container | non-root, read-only, cap-drop=ALL | Данные, доступные приложению, скомпрометированы |
| Чтение секретов из окружения | Secrets файлами | Файл читается тем же процессом |
| Продвижение по сети | Сегментация сетей | Доступное приложению остаётся доступным |
| Побег на host | seccomp, отказ от --privileged | Уязвимость ядра остаётся |
| Вредоносная зависимость | Фиксация версий, сканирование | Атака на цепочку до публикации |
Колонка «остаточный риск» — самая полезная. Она не даёт создать иллюзию, что меры устраняют угрозу полностью.
Иерархия векторов по частоте
По данным разборов инцидентов порядок примерно такой:
| Место | Вектор | Комментарий |
|---|---|---|
| 1 | Уязвимость в приложении | SQL-инъекция, десериализация, SSRF |
| 2 | Утёкшие учётные данные | В коде, в образе, в логах |
| 3 | Уязвимая зависимость | Известная CVE без обновления |
| 4 | Ошибка конфигурации | Открытый порт, отсутствие аутентификации |
| 5 | Избыточные привилегии | --privileged, socket, root |
| 6 | Побег через уязвимость ядра | Редко, требует высокой квалификации |
Практический вывод: hardening container'а закрывает пункты 5 и частично 6, но не трогает 1–4. Это не повод его не делать — это повод не считать его достаточным.
Границы доверия
Граница — там, где меняется уровень доверия:
интернет
│ ◄── граница 1: недоверенный ввод
reverse proxy
│
┌──┴──────────── граница 2: сеть приложения ────────┐
│ api ──► worker │
│ │ │ │
│ └────────┴──► граница 3: сеть данных (internal) │
│ db, cache │
└────────────────────────────────────────────────────┘
| Граница | Что проверяется при пересечении |
|---|---|
| 1. Интернет → proxy | TLS, ограничение частоты, размер тела |
| 2. Proxy → приложение | Аутентификация, авторизация, валидация |
| 3. Приложение → данные | Параметризованные запросы, минимальные права в БД |
Каждая граница — место, где ввод проверяется заново. Пропуск проверки на любой из них означает, что дальше данные считаются доверенными без основания.
Соразмерность мер
Меры имеют цену: время, сложность, вероятность ошибки. Модель угроз нужна, чтобы тратить усилия там, где они дают эффект.
| Мера | Цена | Эффект | Когда оправдана |
|---|---|---|---|
| non-root | Низкая | Высокий | Всегда |
cap-drop=ALL | Низкая | Высокий | Всегда |
| read-only корень | Средняя | Высокий | Почти всегда |
| Сегментация сетей | Средняя | Высокий | При нескольких сервисах |
| Свой профиль seccomp | Высокая | Средний | При конкретной угрозе |
| gVisor или Kata | Высокая | Очень высокий | Недоверенный код |
| Подпись образов | Средняя | Средний | Публичные образы, требования |
Первые четыре строки — обязательный минимум: цена низкая, эффект существенный. Остальное — по результатам модели угроз.
Внутренний механизм
Почему namespace не является границей безопасности
Namespace изменяет то, что процесс видит, но не то, к чему он обращается. Системный вызов идёт в то же ядро; namespace лишь подменяет контекст.
Уязвимость в обработчике ioctl, io_uring или любой другой подсистеме доступна изнутри container'а так же, как с host. Именно поэтому seccomp — существенная мера: он сокращает число достижимых обработчиков (урок 12.5).
Что реально ограничивает побег
| Механизм | Что даёт |
|---|---|
| User namespace | UID 0 в container ≠ UID 0 на host |
| seccomp | Меньше достижимых системных вызовов |
| Capabilities | Меньше привилегированных операций |
| AppArmor/SELinux | Ограничение доступа к путям и объектам |
| Read-only корень | Нет места для закрепления |
Каждый механизм по отдельности обходится. Вместе они образуют защиту в глубину: побег требует цепочки из нескольких уязвимостей.
Команды и примеры
Container — это процесс host
mkdir -p /tmp/threat && cd /tmp/threat
docker run -d --name proc-demo python:3.13-slim sleep 300 > /dev/null
sleep 2
echo "═══ что видит container ═══"
docker exec proc-demo sh -c '
echo " PID внутри: $(cat /proc/self/stat | cut -d" " -f4 > /dev/null; echo $$)"
echo " процессов: $(ls -d /proc/[0-9]* | wc -l)"
'
echo "═══ что видит host ═══"
pid="$(docker inspect proc-demo --format '{{.State.Pid}}')"
printf ' PID на host: %s\n' "$pid"
printf ' команда: %s\n' "$(ps -o args= -p "$pid" 2>/dev/null | head -1)"
printf ' владелец: %s\n' "$(ps -o user= -p "$pid" 2>/dev/null)"
echo "═══ ядро одно и то же ═══"
printf ' host: %s\n' "$(uname -r)"
printf ' container: %s\n' "$(docker exec proc-demo uname -r)"
echo "═══ namespaces процесса ═══"
sudo ls -l "/proc/$pid/ns/" 2>/dev/null | awk '{print " " $9 " -> " $11}' | grep -v '^ ->' \
|| ls -l "/proc/$pid/ns/" 2>/dev/null | head -8 | sed 's/^/ /'
docker rm -f proc-demo > /dev/null
Ожидаемый вывод:
═══ что видит container ═══
PID внутри: 1
процессов: 3
═══ что видит host ═══
PID на host: 58412
команда: sleep 300
владелец: root
═══ ядро одно и то же ═══
host: 6.8.0-88-generic
container: 6.8.0-88-generic
═══ namespaces процесса ═══
cgroup -> cgroup:[4026533456]
ipc -> ipc:[4026533389]
mnt -> mnt:[4026533387]
net -> net:[4026533392]
pid -> pid:[4026533390]
user -> user:[4026531837]
uts -> uts:[4026533388]
Три наблюдения складываются в модель.
PID 1 внутри — это PID 58412 на host: один и тот же процесс с двумя номерами.
Версия ядра совпадает: изоляции ядра нет, оно общее.
Список namespaces показывает, что user не изолирован — его идентификатор 4026531837 совпадает с host. Именно поэтому root в container'е по умолчанию — это root на host (урок 12.3).
Что видно из container'а о host
cd /tmp/threat
echo "═══ информация о host, доступная изнутри ═══"
docker run --rm python:3.13-slim sh -c '
echo " ядро: $(uname -a | cut -c1-60)"
echo " CPU: $(grep -c ^processor /proc/cpuinfo) шт. (это CPU HOST)"
echo " память: $(awk "/MemTotal/{printf \"%.1f ГиБ\", \$2/1048576}" /proc/meminfo) (это память HOST)"
echo " загрузка: $(cut -d" " -f1-3 /proc/loadavg) (это загрузка HOST)"
echo " uptime: $(awk "{printf \"%.0f ч\", \$1/3600}" /proc/uptime) (это uptime HOST)"
'
echo "═══ что это означает ═══"
cat <<'TXT'
procfs не виртуализирован по железу: container видит характеристики
и загрузку host. Это не уязвимость, но это информация о среде,
которую атакующий использует для разведки.
TXT
Ожидаемый вывод:
═══ информация о host, доступная изнутри ═══
ядро: Linux 3f8a91c2e7d4 6.8.0-88-generic #88-Ubuntu SMP PREEMPT_D
CPU: 8 шт. (это CPU HOST)
память: 15.6 ГиБ (это память HOST)
загрузка: 0.42 0.38 0.35 (это загрузка HOST)
uptime: 142 ч (это uptime HOST)
═══ что это означает ═══
procfs не виртуализирован по железу: container видит характеристики
и загрузку host. Это не уязвимость, но это информация о среде,
которую атакующий использует для разведки.
Версия ядра — первое, что интересует атакующего: она определяет, какие известные уязвимости применимы.
Это же объясняет проблему из урока 6.13: Python видит CPU и память host, а не лимиты.
Активы: что именно можно потерять
cd /tmp/threat
cat > assets.py <<'PY'
"""Инвентаризация активов, доступных скомпрометированному процессу."""
from __future__ import annotations
import json
import os
import socket
from pathlib import Path
def check_env_secrets() -> list[str]:
"""Секреты в переменных окружения — первое, что смотрит атакующий."""
patterns = ("PASSWORD", "SECRET", "TOKEN", "KEY", "CREDENTIAL", "DSN")
return [k for k in os.environ if any(p in k.upper() for p in patterns)]
def check_mounted_secrets() -> list[str]:
d = Path("/run/secrets")
return [p.name for p in d.iterdir()] if d.is_dir() else []
def check_docker_socket() -> str:
p = Path("/var/run/docker.sock")
if not p.exists():
return "нет"
return "ЕСТЬ — это доступ к демону, то есть к root на host"
def check_reachable(targets: list[tuple[str, int]]) -> list[str]:
reachable = []
for host, port in targets:
s = socket.socket()
s.settimeout(0.5)
try:
s.connect((host, port))
reachable.append(f"{host}:{port}")
except OSError:
pass
finally:
s.close()
return reachable
def check_host_mounts() -> list[str]:
"""Смонтированные каталоги host — путь к данным вне container."""
found = []
try:
for line in Path("/proc/self/mountinfo").read_text().splitlines():
parts = line.split()
if len(parts) > 4 and parts[4] not in ("/", "/proc", "/sys", "/dev"):
fstype = line.split(" - ")[1].split()[0] if " - " in line else "?"
if fstype in ("ext4", "xfs", "btrfs", "overlay", "9p", "virtiofs"):
found.append(parts[4])
except OSError:
pass
return sorted(set(found))
report = {
"uid": os.getuid(),
"секреты в окружении": check_env_secrets(),
"секреты в /run/secrets": check_mounted_secrets(),
"docker socket": check_docker_socket(),
"смонтировано с host": check_host_mounts(),
"доступные сервисы": check_reachable([("db", 5432), ("cache", 6379), ("127.0.0.1", 8000)]),
}
print(json.dumps(report, ensure_ascii=False, indent=2))
PY
echo "═══ типичная небезопасная конфигурация ═══"
docker run --rm \
-e DATABASE_PASSWORD=secret123 \
-e API_TOKEN=tok-abc \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /etc:/host-etc:ro \
-v "$PWD/assets.py:/a.py:ro" \
python:3.13-slim python /a.py
Ожидаемый вывод:
═══ типичная небезопасная конфигурация ═══
{
"uid": 0,
"секреты в окружении": [
"DATABASE_PASSWORD",
"API_TOKEN"
],
"секреты в /run/secrets": [],
"docker socket": "ЕСТЬ — это доступ к демону, то есть к root на host",
"смонтировано с host": [
"/host-etc"
],
"доступные сервисы": []
}
Отчёт составлен обычным Python-скриптом за доли секунды — ровно то, что выполнит атакующий первым делом после получения выполнения кода.
Четыре находки, каждая из которых расширяет последствия компрометации: работа от root, два секрета в окружении, доступ к Docker socket и смонтированный /etc host.
Та же проверка на защищённой конфигурации
cd /tmp/threat
mkdir -p sec && printf 'пароль-в-файле' > sec/db_password
echo "═══ конфигурация с базовыми мерами ═══"
docker run --rm \
--user 10001:10001 \
--cap-drop=ALL \
--security-opt=no-new-privileges \
--read-only --tmpfs /tmp:size=8m \
-v "$PWD/sec/db_password:/run/secrets/db_password:ro" \
-e DB_PASSWORD_FILE=/run/secrets/db_password \
-v "$PWD/assets.py:/a.py:ro" \
python:3.13-slim python /a.py
Ожидаемый вывод:
═══ конфигурация с базовыми мерами ═══
{
"uid": 10001,
"секреты в окружении": [
"DB_PASSWORD_FILE"
],
"секреты в /run/secrets": [
"db_password"
],
"docker socket": "нет",
"смонтировано с host": [],
"доступные сервисы": []
}
Различие принципиальное. В окружении теперь только путь к секрету, а не значение. Socket недоступен, каталоги host не смонтированы, работа от непривилегированного UID.
Но обратите внимание на честный остаточный риск: файл /run/secrets/db_password процессу доступен — он должен быть доступен, иначе приложение не подключится к базе. Компрометация процесса по-прежнему означает компрометацию пароля базы.
Меры сократили поверхность, но не устранили угрозу — именно это фиксирует колонка «остаточный риск» в модели.
Модель угроз как таблица
cd /tmp/threat
cat > threat-model.md <<'TXT'
# Модель угроз: сервис обработки заказов
## Активы
| Актив | Ценность | Где хранится |
|---|---|---|
| Персональные данные клиентов | Высокая: регуляторные требования | PostgreSQL |
| Учётные данные к базе | Критическая: доступ ко всем данным | Compose secret |
| Токен платёжного шлюза | Критическая: финансовые операции | Compose secret |
| Доступ во внутреннюю сеть | Высокая: плацдарм для продвижения | Сетевая конфигурация |
| Вычислительные ресурсы | Средняя: майнинг, рассылка | Лимиты cgroup |
## Нарушители
| Нарушитель | Возможности | Вероятность |
|---|---|---|
| Внешний, без доступа | Запросы к публичному API | Высокая |
| Аутентифицированный пользователь | Запросы от своего имени | Средняя |
| Скомпрометированная зависимость PyPI | Выполнение кода при сборке или работе | Низкая, ущерб высокий |
| Сосед по узлу | Другой container на том же host | Низкая |
## Векторы и меры
| # | Вектор | Мера | Остаточный риск |
|---:|---|---|---|
| 1 | Инъекция через API → выполнение кода | Валидация ввода, параметризованные запросы | Уязвимость нулевого дня |
| 2 | Выполнение кода → чтение секретов | Secrets файлами, не в окружении | Процесс читает файл сам |
| 3 | Выполнение кода → запись веб-шелла | read-only корень, tmpfs без noexec-исключений | Работа в памяти процесса |
| 4 | Выполнение кода → продвижение к базе | Сеть internal, минимальные права в БД | Данные, доступные приложению |
| 5 | Выполнение кода → побег на host | non-root, cap-drop=ALL, seccomp по умолчанию | Уязвимость ядра |
| 6 | Вредоносная зависимость | Фиксация версий, сканирование, lock-файл | Атака до публикации пакета |
| 7 | Утечка секрета в образ | BuildKit secret mounts, проверка history в CI | Секрет в логах приложения |
## Принятые решения
- **Не применяем gVisor/Kata**: код собственный, недоверенный код не запускается.
Пересмотр — при появлении плагинов от сторонних разработчиков.
- **Не применяем свой профиль seccomp**: профиль по умолчанию покрывает
известные векторы. Пересмотр — при обнаружении конкретной угрозы.
- **Не подписываем образы**: реестр внутренний, доступ ограничен.
Пересмотр — при публикации образов наружу.
## Что НЕ покрыто мерами
- Уязвимость в самом приложении: закрывается тестами и код-ревью, не изоляцией.
- Утечка данных через легитимный API: требует авторизации и аудита.
- Компрометация CI: отдельная модель угроз.
TXT
echo "═══ структура модели ═══"
grep -E '^#|^\| [0-9#]' threat-model.md | head -20 | sed 's/^/ /'
echo
echo "═══ проверка полноты ═══"
python3 - <<'PY'
import pathlib
import re
text = pathlib.Path("threat-model.md").read_text()
required = {
"Активы": r"## Активы",
"Нарушители": r"## Нарушители",
"Векторы и меры": r"## Векторы и меры",
"Остаточный риск": r"Остаточный риск",
"Принятые решения": r"## Принятые решения",
"Что НЕ покрыто": r"## Что НЕ покрыто",
}
for name, pattern in required.items():
mark = "✓" if re.search(pattern, text) else "✗"
print(f" {mark} {name}")
vectors = len(re.findall(r"^\| \d+ \|", text, re.M))
print(f" векторов описано: {vectors}")
PY
Ожидаемый вывод:
═══ структура модели ═══
# Модель угроз: сервис обработки заказов
## Активы
## Нарушители
## Векторы и меры
| # | Вектор | Мера | Остаточный риск |
| 1 | Инъекция через API → выполнение кода | Валидация ввода, параметризованные запросы | Уязвимость нулевого дня |
| 2 | Выполнение кода → чтение секретов | Secrets файлами, не в окружении | Процесс читает файл сам |
...
## Принятые решения
## Что НЕ покрыто мерами
═══ проверка полноты ═══
✓ Активы
✓ Нарушители
✓ Векторы и меры
✓ Остаточный риск
✓ Принятые решения
✓ Что НЕ покрыто
векторов описано: 7
Модель умещается на страницу и отвечает на все четыре вопроса. Два последних раздела — «Принятые решения» и «Что НЕ покрыто» — превращают её из формальности в рабочий документ: через полгода будет видно, что решили и почему.
Соразмерность: цена и эффект мер
cd /tmp/threat
cat > measures.py <<'PY'
"""Оценка мер по цене и эффекту — вход в решение о применении."""
from __future__ import annotations
MEASURES = [
# (мера, цена 1-5, эффект 1-5, закрывает векторы)
("non-root (USER в образе)", 1, 4, "5"),
("--cap-drop=ALL", 1, 4, "5"),
("no-new-privileges", 1, 3, "5"),
("read-only корень + tmpfs", 2, 4, "3"),
("secrets файлами вместо ENV", 2, 4, "2"),
("сегментация сетей", 3, 4, "4"),
("фиксация версий + сканирование", 2, 4, "6"),
("BuildKit secret mounts", 2, 4, "7"),
("свой профиль seccomp", 5, 3, "5"),
("подпись образов", 4, 3, "6"),
("gVisor / Kata Containers", 5, 5, "5"),
]
def priority(cost: int, effect: int) -> str:
ratio = effect / cost
if ratio >= 2.0:
return "делать сразу"
if ratio >= 1.0:
return "делать"
return "по модели угроз"
print(f" {'мера':<32} {'цена':>5} {'эффект':>7} {'вектор':>7} решение")
print(" " + "─" * 72)
for name, cost, effect, vectors in sorted(MEASURES, key=lambda m: -m[2] / m[1]):
print(f" {name:<32} {cost:>5} {effect:>7} {vectors:>7} {priority(cost, effect)}")
PY
python3 measures.py
Ожидаемый вывод:
мера цена эффект вектор решение
────────────────────────────────────────────────────────────────────────
non-root (USER в образе) 1 4 5 делать сразу
--cap-drop=ALL 1 4 5 делать сразу
no-new-privileges 1 3 5 делать сразу
read-only корень + tmpfs 2 4 3 делать сразу
secrets файлами вместо ENV 2 4 2 делать сразу
фиксация версий + сканирование 2 4 6 делать сразу
BuildKit secret mounts 2 4 7 делать сразу
сегментация сетей 3 4 4 делать
gVisor / Kata Containers 5 5 5 делать
подпись образов 4 3 3 по модели угроз
свой профиль seccomp 5 3 5 по модели угроз
Семь мер с отношением эффекта к цене выше двух — их делают без обсуждения. Всё остальное требует обоснования моделью угроз.
Обратите внимание на gVisor: эффект максимальный, но и цена тоже. Он попадает в «делать» только когда модель угроз содержит недоверенный код.
Границы доверия в конфигурации
cd /tmp/threat
cat > compose.trust.yaml <<'EOF'
name: trust
services:
# Граница 1: интернет → приложение
proxy:
image: python:3.13-slim
command: ["sleep", "300"]
networks: [edge]
ports:
- "127.0.0.1:8950:8000"
# Граница 2: приложение → данные
api:
image: python:3.13-slim
command: ["sleep", "300"]
networks: [edge, data]
worker:
image: python:3.13-slim
command: ["sleep", "300"]
networks: [data]
db:
image: python:3.13-slim
command: ["sleep", "300"]
networks: [data]
networks:
edge:
data:
internal: true # граница 3: наружу хода нет
EOF
docker compose -f compose.trust.yaml up -d > /dev/null 2>&1
sleep 3
echo "═══ карта границ доверия ═══"
printf ' %-8s %-34s %s\n' "сервис" "сети" "выход наружу"
for svc in proxy api worker db; do
cid="$(docker compose -f compose.trust.yaml ps -q $svc)"
nets="$(docker inspect "$cid" --format '{{range $n, $v := .NetworkSettings.Networks}}{{$n}} {{end}}' | sed 's/trust_//g')"
routes="$(docker exec "$cid" sh -c 'ip route 2>/dev/null | grep -c "^default"' 2>/dev/null || echo '?')"
printf ' %-8s %-34s %s\n' "$svc" "$nets" \
"$([ "$routes" = "0" ] && echo 'нет' || echo 'есть')"
done
echo "═══ проверка изоляции ═══"
probe() {
docker compose -f compose.trust.yaml exec -T "$1" python - "$2" <<'PY' 2>/dev/null
import socket, sys
try:
print(f" {sys.argv[1]}: {socket.gethostbyname(sys.argv[1])}")
except socket.gaierror:
print(f" {sys.argv[1]}: имя не разрешается")
PY
}
printf ' из proxy:\n'; probe proxy db | sed 's/^/ /'
printf ' из api:\n'; probe api db | sed 's/^/ /'
docker compose -f compose.trust.yaml down > /dev/null 2>&1
cd /tmp && rm -rf /tmp/threat
Ожидаемый вывод:
═══ карта границ доверия ═══
сервис сети выход наружу
proxy edge есть
api data edge есть
worker data нет
db data нет
═══ проверка изоляции ═══
из proxy:
db: имя не разрешается
из api:
db: 172.35.0.4
Три границы видны в одной таблице. proxy не может даже разрешить имя базы; worker и db не имеют выхода наружу.
api состоит в обеих сетях — он и есть точка пересечения границы, и именно поэтому валидация ввода в нём критична (урок 8.2).
Практическое упражнение
Задание. Постройте модель угроз для сервиса и подтвердите соответствие конфигурации.
Требования:
- Модель содержит активы, нарушителей, векторы, меры и остаточный риск для каждого вектора.
- Есть раздел принятых решений: что сознательно не делаем и почему.
- Есть раздел «что не покрыто мерами».
- Скрипт инвентаризации показывает, что доступно скомпрометированному процессу.
- Показаны две конфигурации: до применения мер и после; различие измерено.
- Меры оценены по цене и эффекту; выбор обоснован.
Подсказки
Подсказка 1
Остаточный риск обязателен для каждого вектора: мера, устраняющая угрозу полностью, — редкость.
Подсказка 2
Скрипт инвентаризации должен проверять то, что проверил бы атакующий: окружение, socket, монтирования, доступные сервисы.
Подсказка 3
Пункт 5 нагляднее, если обе конфигурации проверяются одним и тем же скриптом.
Решение
Показать решение
mkdir -p /tmp/tmodel/{sec,app} && cd /tmp/tmodel
cat > app/inventory.py <<'PY'
"""Инвентаризация: что доступно процессу после компрометации.
Выполняет то же, что сделал бы атакующий, получивший выполнение кода:
осматривается и оценивает, куда можно продвинуться.
"""
from __future__ import annotations
import json
import os
import socket
import sys
from pathlib import Path
SECRET_HINTS = ("PASSWORD", "SECRET", "TOKEN", "KEY", "CREDENTIAL", "DSN", "PRIVATE")
SENSITIVE_PATHS = ("/var/run/docker.sock", "/root", "/etc/shadow", "/host")
def read_status(field: str) -> str:
try:
for line in Path("/proc/self/status").read_text().splitlines():
if line.startswith(field + ":"):
return line.split(maxsplit=1)[1].strip()
except OSError:
pass
return "?"
def env_secrets() -> dict[str, str]:
"""Значения секретов в окружении — не только имена."""
out = {}
for key, value in os.environ.items():
upper = key.upper()
if any(h in upper for h in SECRET_HINTS) and not upper.endswith("_FILE"):
out[key] = f"{value[:4]}…({len(value)} симв.)" if value else "(пусто)"
return out
def secret_files() -> list[str]:
d = Path("/run/secrets")
return sorted(p.name for p in d.iterdir()) if d.is_dir() else []
def sensitive_access() -> dict[str, str]:
out = {}
for path in SENSITIVE_PATHS:
p = Path(path)
if not p.exists():
continue
try:
if p.is_dir():
list(p.iterdir())
out[path] = "доступен на чтение"
else:
p.read_bytes()[:1]
out[path] = "доступен на чтение"
except OSError as exc:
out[path] = f"отказ: {exc.strerror}"
return out
def writable_paths() -> list[str]:
out = []
for path in ("/", "/app", "/usr/local/bin", "/etc", "/tmp"):
probe = Path(path) / ".probe-write"
try:
probe.write_text("x")
probe.unlink()
out.append(path)
except OSError:
pass
return out
def reachable(targets: list[tuple[str, int]]) -> list[str]:
out = []
for host, port in targets:
s = socket.socket()
s.settimeout(0.5)
try:
s.connect((host, port))
out.append(f"{host}:{port}")
except OSError:
pass
finally:
s.close()
return out
def score(report: dict[str, object]) -> int:
"""Грубая оценка поверхности: чем выше, тем хуже."""
points = 0
points += 3 if report["uid"] == 0 else 0
points += 3 * len(report["секреты_в_окружении"])
points += 5 if "доступен" in str(report["чувствительные_пути"].get("/var/run/docker.sock", "")) else 0
points += 2 * len([p for p in report["записываемые_пути"] if p != "/tmp"])
points += 1 if report["capabilities"] != "0000000000000000" else 0
return points
if __name__ == "__main__":
report: dict[str, object] = {
"uid": os.getuid(),
"capabilities": read_status("CapEff"),
"no_new_privs": read_status("NoNewPrivs"),
"секреты_в_окружении": env_secrets(),
"секреты_файлами": secret_files(),
"чувствительные_пути": sensitive_access(),
"записываемые_пути": writable_paths(),
"доступные_сервисы": reachable([("db", 5432), ("127.0.0.1", 8000)]),
}
report["оценка_поверхности"] = score(report)
print(json.dumps(report, ensure_ascii=False, indent=2))
sys.exit(0)
PY
printf 'пароль-базы-для-модели' > sec/db_password
chmod 600 sec/db_password
cat > THREAT-MODEL.md <<'TXT'
# Модель угроз: сервис обработки заказов
Дата составления: 2026-07-31. Пересмотр: при изменении состава сервисов
или появлении недоверенного кода.
## 1. Активы
| Актив | Ценность | Где находится |
|---|---|---|
| Персональные данные клиентов | Высокая: регуляторные требования | PostgreSQL |
| Учётные данные к базе | Критическая: доступ ко всем данным | Compose secret |
| Токен платёжного шлюза | Критическая: финансовые операции | Compose secret |
| Доступ во внутреннюю сеть | Высокая: плацдарм для продвижения | Сетевая конфигурация |
| Вычислительные ресурсы | Средняя: майнинг, рассылка спама | Лимиты cgroup |
| Доверие к домену | Средняя: фишинг от нашего имени | Вне контура container |
## 2. Нарушители
| Нарушитель | Возможности | Вероятность | Ущерб |
|---|---|---|---|
| Внешний, без доступа | Запросы к публичному API | Высокая | Высокий |
| Аутентифицированный пользователь | Запросы от своего имени | Средняя | Средний |
| Скомпрометированная зависимость | Выполнение кода при сборке и работе | Низкая | Критический |
| Сосед по узлу | Другой container на том же host | Низкая | Высокий |
| Сотрудник с доступом к CI | Изменение образа | Очень низкая | Критический |
## 3. Векторы, меры, остаточный риск
| # | Вектор | Мера | Остаточный риск |
|---:|---|---|---|
| 1 | Инъекция через API → выполнение кода | Параметризованные запросы, валидация ввода | Уязвимость нулевого дня в зависимости |
| 2 | Выполнение кода → чтение секретов | Secrets файлами, не в переменных окружения | Процесс читает файл сам — устранить нельзя |
| 3 | Выполнение кода → закрепление | read-only корень, tmpfs только для /tmp | Работа целиком в памяти процесса |
| 4 | Выполнение кода → продвижение к базе | Сеть internal, отдельный пользователь БД с минимальными правами | Данные, доступные приложению, скомпрометированы |
| 5 | Выполнение кода → побег на host | non-root, cap-drop=ALL, no-new-privileges, seccomp по умолчанию | Уязвимость ядра: остаётся |
| 6 | Выполнение кода → исчерпание ресурсов узла | Лимиты памяти, CPU, PID | Деградация в пределах лимита |
| 7 | Вредоносная зависимость PyPI | Фиксация версий, lock-файл, сканирование | Атака на пакет до публикации |
| 8 | Утечка секрета в слой образа | BuildKit secret mounts, проверка history в CI | Секрет в логах приложения |
| 9 | Доступ к Docker socket | Socket не монтируется ни в один container | Доступ к socket на самом host |
## 4. Принятые решения
### НЕ применяем gVisor или Kata Containers
**Обоснование:** запускается только собственный код и код известных зависимостей.
Недоверенный код пользователей платформы не выполняется.
**Условие пересмотра:** появление плагинов от сторонних разработчиков,
выполнение пользовательских скриптов, переход на многопользовательскую модель.
### НЕ создаём собственный профиль seccomp
**Обоснование:** профиль Docker по умолчанию блокирует около 44 вызовов,
включая известные векторы побега. Свой профиль требует сопровождения
и ломается при обновлении зависимостей.
**Условие пересмотра:** обнаружение конкретной угрозы, не покрытой умолчанием.
### НЕ подписываем образы
**Обоснование:** реестр внутренний, доступ на запись только у CI.
Подмена образа требует компрометации CI, которая закрывается отдельно.
**Условие пересмотра:** публикация образов наружу, требование аудита.
## 5. Что НЕ покрыто мерами изоляции
- **Уязвимость в самом приложении.** Закрывается тестами, код-ревью
и статическим анализом. Изоляция ограничивает последствия, а не вероятность.
- **Утечка данных через легитимный API.** Требует авторизации на уровне
объектов и аудита обращений.
- **Компрометация конвейера сборки.** Отдельная модель угроз.
- **Атака на цепочку поставок до публикации пакета.** Частично закрывается
фиксацией версий, полностью — нет.
- **Уязвимость нулевого дня в ядре.** Принимается как остаточный риск;
снижается своевременным обновлением ядра host.
TXT
cat > measures.py <<'PY'
"""Оценка мер: цена, эффект, закрываемые векторы."""
from __future__ import annotations
MEASURES = [
("USER 10001 в образе", 1, 4, [5]),
("--cap-drop=ALL", 1, 4, [5]),
("--security-opt no-new-privileges",1, 3, [5]),
("read-only + tmpfs", 2, 4, [3]),
("secrets файлами", 2, 4, [2, 8]),
("лимиты памяти/CPU/PID", 1, 3, [6]),
("сеть internal для данных", 3, 4, [4]),
("фиксация версий + сканирование", 2, 4, [7]),
("BuildKit secret mounts", 2, 4, [8]),
("socket не монтируется", 1, 5, [9]),
("свой профиль seccomp", 5, 3, [5]),
("подпись образов", 4, 3, [7]),
("gVisor / Kata", 5, 5, [5]),
]
def decision(cost: int, effect: int) -> str:
ratio = effect / cost
if ratio >= 2.0:
return "делать сразу"
if ratio >= 1.0:
return "делать"
return "по модели угроз"
if __name__ == "__main__":
print(f" {'мера':<34} {'цена':>4} {'эфф.':>5} {'векторы':>9} решение")
print(" " + "─" * 74)
applied = []
for name, cost, effect, vectors in sorted(MEASURES, key=lambda m: -m[2] / m[1]):
d = decision(cost, effect)
print(f" {name:<34} {cost:>4} {effect:>5} {str(vectors):>9} {d}")
if d == "делать сразу":
applied.append(name)
print()
print(f" применяем безусловно: {len(applied)} мер")
covered = sorted({v for n, c, e, vs in MEASURES if e / c >= 2.0 for v in vs})
print(f" закрываемые векторы: {covered}")
print(f" вектор 1 (уязвимость приложения) изоляцией НЕ закрывается")
PY
cat > compose.yaml <<'EOF'
name: tmodel
services:
# Конфигурация ДО применения мер
before:
image: python:3.13-slim
command: ["sleep", "300"]
environment:
DATABASE_PASSWORD: "пароль-в-переменной"
PAYMENT_API_TOKEN: "tok-live-abcdef123456"
volumes:
- ./app/inventory.py:/inventory.py:ro
- /var/run/docker.sock:/var/run/docker.sock
networks: [everything]
# Конфигурация ПОСЛЕ применения мер
after:
image: python:3.13-slim
command: ["sleep", "300"]
user: "10001:10001"
read_only: true
tmpfs:
- /tmp:size=8m,mode=1777,uid=10001,gid=10001
cap_drop: [ALL]
security_opt:
- no-new-privileges:true
pids_limit: 64
environment:
DB_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
volumes:
- ./app/inventory.py:/inventory.py:ro
networks: [isolated]
deploy:
resources:
limits:
memory: 128M
cpus: "0.5"
networks:
everything:
isolated:
internal: true
secrets:
db_password:
file: ./sec/db_password
EOF
fail=0
ok() { printf ' ✓ %s\n' "$1"; }
bad() { printf ' ✗ %s\n' "$1"; fail=1; }
printf '\n═══ Требования 1-3: структура модели ═══\n'
python3 - <<'PY'
import pathlib, re
text = pathlib.Path("THREAT-MODEL.md").read_text()
checks = {
"раздел «Активы»": r"## 1\. Активы",
"раздел «Нарушители»": r"## 2\. Нарушители",
"векторы с остаточным риском": r"Остаточный риск",
"раздел «Принятые решения»": r"## 4\. Принятые решения",
"раздел «Что НЕ покрыто»": r"## 5\. Что НЕ покрыто",
"условия пересмотра": r"Условие пересмотра",
}
missing = []
for name, pat in checks.items():
found = bool(re.search(pat, text))
print(f" {'✓' if found else '✗'} {name}")
if not found:
missing.append(name)
vectors = re.findall(r"^\| (\d+) \| (.+?) \| (.+?) \| (.+?) \|$", text, re.M)
print(f" векторов: {len(vectors)}")
empty_residual = [v[0] for v in vectors if not v[3].strip() or v[3].strip() == "—"]
print(f" векторов без остаточного риска: {len(empty_residual)}")
import sys
sys.exit(1 if missing or empty_residual or len(vectors) < 5 else 0)
PY
[ $? -eq 0 ] && ok "модель полна, у каждого вектора указан остаточный риск" \
|| bad "модель неполна"
printf '\n═══ Требование 6: оценка мер ═══\n'
python3 measures.py
printf '\n═══ Требования 4-5: инвентаризация ДО и ПОСЛЕ ═══\n'
docker compose up -d > /dev/null 2>&1
sleep 3
for svc in before after; do
printf '\n ── конфигурация %s ──\n' "$svc"
docker compose exec -T "$svc" python /inventory.py 2>/dev/null \
| python3 -c "
import json, sys
r = json.load(sys.stdin)
print(f\" uid: {r['uid']}, capabilities: {r['capabilities']}, no_new_privs: {r['no_new_privs']}\")
print(f\" секреты в окружении: {list(r['секреты_в_окружении']) or 'нет'}\")
print(f\" секреты файлами: {r['секреты_файлами'] or 'нет'}\")
print(f\" чувствительные пути: {r['чувствительные_пути'] or 'нет доступа'}\")
print(f\" записываемые пути: {r['записываемые_пути']}\")
print(f\" ОЦЕНКА ПОВЕРХНОСТИ: {r['оценка_поверхности']}\")
"
done
score_before="$(docker compose exec -T before python /inventory.py 2>/dev/null \
| python3 -c "import json,sys; print(json.load(sys.stdin)['оценка_поверхности'])")"
score_after="$(docker compose exec -T after python /inventory.py 2>/dev/null \
| python3 -c "import json,sys; print(json.load(sys.stdin)['оценка_поверхности'])")"
printf '\n оценка до: %s, после: %s\n' "$score_before" "$score_after"
[ "$score_before" -gt "$score_after" ] && ok "поверхность атаки сокращена измеримо" \
|| bad "различия нет: до=$score_before после=$score_after"
[ "$score_after" -le 3 ] && ok "остаточная поверхность минимальна" \
|| bad "остаточная оценка высока: $score_after"
printf '\n═══ Остаточный риск: что осталось доступным ═══\n'
docker compose exec -T after sh -c 'cat /run/secrets/db_password' 2>/dev/null | sed 's/^/ пароль базы читается процессом: /'
cat <<'TXT'
Это НЕ дефект конфигурации: приложение обязано читать пароль,
иначе не подключится к базе. Мера сократила поверхность
(пароля нет в окружении и в docker inspect), но не устранила
угрозу — что и зафиксировано в модели как остаточный риск вектора 2.
TXT
ok "остаточный риск продемонстрирован, а не скрыт"
printf '\n═══ ИТОГ ═══\n'
[ "$fail" -eq 0 ] && echo " все требования выполнены" || echo " ЕСТЬ ПРОВАЛЫ"
docker compose down > /dev/null 2>&1
cd /tmp && rm -rf /tmp/tmodel
exit "$fail"
Ожидаемый вывод:
═══ Требования 1-3: структура модели ═══
✓ раздел «Активы»
✓ раздел «Нарушители»
✓ векторы с остаточным риском
✓ раздел «Принятые решения»
✓ раздел «Что НЕ покрыто»
✓ условия пересмотра
векторов: 9
векторов без остаточного риска: 0
✓ модель полна, у каждого вектора указан остаточный риск
═══ Требование 6: оценка мер ═══
мера цена эфф. векторы решение
──────────────────────────────────────────────────────────────────────────
socket не монтируется 1 5 [9] делать сразу
USER 10001 в образе 1 4 [5] делать сразу
--cap-drop=ALL 1 4 [5] делать сразу
--security-opt no-new-privileges 1 3 [5] делать сразу
лимиты памяти/CPU/PID 1 3 [6] делать сразу
read-only + tmpfs 2 4 [3] делать сразу
secrets файлами 2 4 [2, 8] делать сразу
фиксация версий + сканирование 2 4 [7] делать сразу
BuildKit secret mounts 2 4 [8] делать сразу
сеть internal для данных 3 4 [4] делать
gVisor / Kata 5 5 [5] делать
подпись образов 4 3 [7] по модели угроз
свой профиль seccomp 5 3 [5] по модели угроз
применяем безусловно: 9 мер
закрываемые векторы: [2, 3, 5, 6, 7, 8, 9]
вектор 1 (уязвимость приложения) изоляцией НЕ закрывается
═══ Требования 4-5: инвентаризация ДО и ПОСЛЕ ═══
── конфигурация before ──
uid: 0, capabilities: 00000000a80425fb, no_new_privs: 0
секреты в окружении: ['DATABASE_PASSWORD', 'PAYMENT_API_TOKEN']
секреты файлами: нет
чувствительные пути: {'/var/run/docker.sock': 'доступен на чтение', '/root': 'доступен на чтение'}
записываемые пути: ['/', '/app', '/usr/local/bin', '/etc', '/tmp']
ОЦЕНКА ПОВЕРХНОСТИ: 23
── конфигурация after ──
uid: 10001, capabilities: 0000000000000000, no_new_privs: 1
секреты в окружении: нет
секреты файлами: ['db_password']
чувствительные пути: нет доступа
записываемые пути: ['/tmp']
ОЦЕНКА ПОВЕРХНОСТИ: 0
оценка до: 23, после: 0
✓ поверхность атаки сокращена измеримо
✓ остаточная поверхность минимальна
═══ Остаточный риск: что осталось доступным ═══
пароль базы читается процессом: пароль-базы-для-модели
Это НЕ дефект конфигурации: приложение обязано читать пароль,
иначе не подключится к базе. Мера сократила поверхность
(пароля нет в окружении и в docker inspect), но не устранила
угрозу — что и зафиксировано в модели как остаточный риск вектора 2.
✓ остаточный риск продемонстрирован, а не скрыт
═══ ИТОГ ═══
все требования выполнены
Все требования выполнены.
Обратите внимание на последний блок: он показывает, что пароль по-прежнему читается процессом. Скрыть это было бы соблазнительно — оценка поверхности равна нулю, все галочки зелёные. Но модель угроз ценна ровно тем, что фиксирует остаточный риск, а не создаёт впечатление полной защищённости.
Три решения, определяющие качество.
Инвентаризация выполняется одним скриптом на обеих конфигурациях. Разные проверки для «до» и «после» позволили бы подогнать результат. Один скрипт, два запуска, числовая оценка — сравнение становится объективным.
Проверка модели требует остаточного риска у каждого вектора. Строка векторов без остаточного риска: 0 — не формальность: пустая ячейка означала бы утверждение «эта угроза устранена полностью», которое почти всегда неверно. Автоматическая проверка не даёт написать такое по невнимательности.
Оценка мер печатает, какие векторы не закрываются. Строка «вектор 1 изоляцией НЕ закрывается» важнее списка закрытых: она предотвращает главную ошибку — считать, что hardening решает проблему безопасности приложения.
Чего решение не делает. Числовая оценка поверхности — грубая эвристика с произвольными весами; она годится для сравнения двух конфигураций, но не для абсолютных выводов. Модель не покрывает угрозы уровня платформы: компрометацию узла, атаку на реестр, доступ администратора. Не проверяется и реальная эксплуатируемость: конфигурация «после» может содержать уязвимость приложения, которую никакая изоляция не закроет.
Проверка результата
docker run --rm python:3.13-slim sh -c '
echo "ядро container: $(uname -r)"
echo "ядро host: см. uname -r на host — совпадёт"
echo "user namespace: $(readlink /proc/self/ns/user)"
'
readlink /proc/self/ns/user
Ожидается совпадение идентификатора user namespace — подтверждение, что он не изолирован.
Типичные ошибки
| Ошибка | Причина | Исправление |
|---|---|---|
| Считают container изолированным как VM | Похожее применение | Ядро общее; это не граница безопасности |
| Запускают недоверенный код в обычном container | «Он же изолирован» | Нужны gVisor, Kata или VM |
| Модель угроз не составляют | Кажется формальностью | Меры применяются наугад |
| В модели нет остаточного риска | Неудобно признавать | Создаётся иллюзия полной защиты |
| Hardening считают защитой приложения | Логично предположить | Не закрывает инъекции и утечки через API |
| Забывают активы «сеть» и «ресурсы» | Думают только о данных | Взлом второстепенного сервиса — плацдарм |
| Применяют дорогие меры до дешёвых | Кажутся серьёзнее | Сначала то, где эффект на цену выше |
| Границы доверия не определены | Не задумывались | Ввод считается доверенным без основания |
Контрольные вопросы
На понимание:
- Почему изоляция container не равна границе безопасности?
- Назовите три вещи, от которых контейнеризация не защищает.
- Из каких четырёх частей состоит модель угроз?
- Зачем в модели нужна колонка «остаточный риск»?
- Какие векторы hardening не закрывает и почему?
На применение:
- Как определить границы доверия в своей системе?
- Как выбрать, какие меры применять, а какие нет?
- Что проверит атакующий первым делом после получения выполнения кода?
На диагностику:
- Приложение скомпрометировано. Что оценивать в первую очередь?
- Требуется запустить код пользователей платформы. Достаточно ли hardening?
Краткое резюме
- Container — процесс host, ограниченный namespaces, cgroups и capabilities того же ядра.
- Ядро общее: уязвимость в нём доступна изнутри container'а.
- Для собственного кода контейнеризации достаточно; для недоверенного нужны gVisor, Kata или VM.
- Контейнеризация защищает от случайного воздействия, но не от уязвимости приложения.
- Модель угроз отвечает на четыре вопроса: активы, нарушители, векторы, меры.
- Колонка «остаточный риск» обязательна: она не даёт создать иллюзию полной защиты.
- Активами являются и доступ во внутреннюю сеть, и вычислительные ресурсы.
- По частоте инцидентов лидируют уязвимость приложения и утёкшие учётные данные.
- Hardening закрывает избыточные привилегии и частично побег, но не первые четыре вектора.
- Границы доверия — места, где ввод проверяется заново.
- Меры выбирают по отношению эффекта к цене; первые четыре обязательны всегда.
- Разделы «принятые решения» и «что не покрыто» превращают модель в рабочий документ.
Официальные источники
| Источник | Ссылка | Что подтверждает |
|---|---|---|
| Docker: security | https://docs.docker.com/engine/security/ | Модель безопасности, границы применимости |
| Docker: security non-events | https://docs.docker.com/engine/security/#conclusions | Позиция о недоверенном коде |
| NIST SP 800-190 | https://csrc.nist.gov/pubs/sp/800/190/final | Руководство по безопасности container'ов |
| OWASP: Docker Security | https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html | Практики и приоритеты |
| OWASP: Threat Modeling | https://cheatsheetseries.owasp.org/cheatsheets/Threat_Modeling_Cheat_Sheet.html | Процедура построения модели |
| CIS Docker Benchmark | https://www.cisecurity.org/benchmark/docker | Проверяемые требования |
| gVisor | https://gvisor.dev/docs/ | Изоляция для недоверенного кода |
| Kata Containers | https://katacontainers.io/ | Лёгкие виртуальные машины |
Навигация
← Вернуться к разделу
Следующий материал → Daemon и socket
Главное оглавление