Главная/Security/Практика

Раздел 12. Практические задания

Задания выполняются только на собственной учебной машине. Часть из них демонстрирует векторы атаки; цель каждого — понять механизм и уметь его закрыть.

Проверка результата описана в каждом задании. Формулировка «показать, что…» означает: выполнить команду и привести её вывод, а не пересказать ожидаемое.

Как проверять себя

ФормулировкаЧто требуется
«показать, что…»Команда и её фактический вывод
«обосновать»Сценарий риска, а не ссылка на правило
«исправить»Работающая конфигурация плюс проверка, что уязвимость закрыта

Задание 1. Docker socket равен root на host

Тип: обязательное
Материал: 12.2

Показать на учебной машине, что доступ к Docker socket позволяет прочитать файл, недоступный вашему пользователю.

Требования:

  1. Убедиться, что ваш пользователь не может прочитать /etc/shadow напрямую.
  2. Прочитать первую строку /etc/shadow через container, имеющий доступ к socket.
  3. Объяснить, почему при этом не потребовался sudo.
  4. Показать, что членство в группе docker даёт тот же результат, что и sudo.
  5. Назвать три способа сократить риск и указать, что каждый не решает.

Проверка

bash
# 1. Прямое чтение недоступно
cat /etc/shadow 2>&1 | head -1

# 2. Через socket — доступно
docker run --rm -v /:/host:ro alpine:3.21 head -1 /host/etc/shadow

# 3. Права вашего пользователя
id | tr ' ' '\n' | grep -o 'docker' || echo "не в группе docker"

Критерий

Обе команды выполнены, вывод второй содержит строку из /etc/shadow. Дан ответ, почему sudo не понадобился, и перечислены три меры с указанием их границ.

Ориентир для самопроверки

Ключевые пункты:

  • монтирование / в container даёт доступ ко всей файловой системе host;
  • daemon работает от root, и всё, что он делает по вашей команде, делается от root;
  • членство в группе docker — это право отдавать команды этому daemon, то есть эквивалент root без записи в журнал sudo;
  • меры: rootless mode (меняет модель, но не решает для того, кто в группе), прокси к API с фильтрацией команд (сложно настроить полно), отказ от монтирования socket в container'ы (не влияет на доступ с host).

Ни одна мера не отменяет главного: если пользователь может отдавать команды daemon, он равен root.


Задание 2. Извлечение секрета из образа

Тип: обязательное
Материал: 12.6

Извлечь секрет, переданный через --build-arg, из готового образа.

Требования:

  1. Собрать образ, передав секрет через --build-arg.
  2. Извлечь его из готового образа, не имея команды сборки.
  3. Собрать второй образ с секретом через ENV и извлечь его другим способом.
  4. Собрать третий с COPY секрета и последующим RUN rm; извлечь третьим способом.
  5. Составить таблицу: способ передачи → канал утечки → команда извлечения.

Проверка

bash
docker history --no-trunc ОБРАЗ | grep -i token
docker inspect ОБРАЗ --format '{{range .Config.Env}}{{println .}}{{end}}'
docker save ОБРАЗ -o img.tar && tar -xf img.tar -C unpacked && grep -r СЕКРЕТ unpacked

Критерий

Все три секрета извлечены, для каждого приведена команда. Таблица содержит три строки и правильно сопоставляет способ передачи каналу.


Задание 3. Сборка без утечки секрета

Тип: обязательное
Материал: 12.6, 5.9

Переделать сборку из задания 2 на RUN --mount=type=secret и доказать отсутствие секрета.

Требования:

  1. Переписать Dockerfile на secret mount.
  2. Показать в журнале сборки, что секрет был использован (например, вывести его длину).
  3. Доказать отсутствие секрета во всех трёх каналах: ENV, history, слои.
  4. Показать, что копирование /run/secrets/x в обычный путь возвращает утечку.
  5. Сформулировать правило одним предложением.

Проверка

bash
docker build --secret id=token,src=./token.txt -t safe:1 .
for check in \
    "docker inspect safe:1 --format '{{range .Config.Env}}{{println .}}{{end}}'" \
    "docker history --no-trunc safe:1"; do
    eval "$check" | grep -c СЕКРЕТ
done
docker save safe:1 | tar -t > /dev/null && echo "слои проверить распаковкой"

Критерий

Ноль совпадений по всем трём каналам. Пункт 4 воспроизведён: показан образ, где секрет присутствует несмотря на secret mount. Правило сформулировано.

Ориентир для самопроверки

Правило: секрет используют внутри RUN, но не сохраняют — механизм защищает точку монтирования, а не значение.


Задание 4. Минимальный набор capabilities

Тип: обязательное
Материал: 12.4, 11.2

Запустить приложение с --cap-drop=ALL и определить минимально необходимый набор.

Требования:

  1. Взять приложение из раздела 6 или написать простое на FastAPI.
  2. Запустить с --cap-drop=ALL и зафиксировать результат.
  3. Если что-то не работает — определить, какая операция отказала.
  4. Добавлять capabilities по одной, проверяя после каждой.
  5. Показать, что часть потребностей снимается изменением операции, а не выдачей capability.
  6. Записать итоговый набор и обоснование каждой capability в нём.

Проверка

bash
docker run --rm --cap-drop=ALL ОБРАЗ
docker run --rm --cap-drop=ALL python:3.13-slim sh -c 'grep CapEff /proc/self/status'

Критерий

Для типичного веб-приложения итоговый набор — пустой. Если получилось иначе, для каждой capability приведено обоснование сценарием, а не «иначе не работает».

Ориентир для самопроверки

Самая частая «нужная» capability — NET_BIND_SERVICE для порта 80. Она не нужна: слушайте 8000 и публикуйте -p 80:8000. Порты ниже 1024 привилегированы на стороне процесса, а не на стороне публикации.


Задание 5. Сравнение обычного и privileged container

Тип: обязательное
Материал: 12.4

Сравнить привилегии обычного и --privileged container по всем признакам.

Требования:

  1. Сравнить количество capabilities.
  2. Сравнить состояние seccomp.
  3. Сравнить число узлов в /dev и доступность диска host.
  4. Сравнить доступ к /sys на запись.
  5. Добавить третью колонку — --cap-add=ALL — и объяснить, что она показывает.
  6. Назвать четыре независимых ослабления, которые вносит --privileged.

Проверка

bash
for cfg in "" "--cap-add=ALL" "--privileged"; do
    docker run --rm $cfg python:3.13-slim sh -c '
        grep CapEff /proc/self/status
        grep Seccomp: /proc/self/status
        ls /dev | wc -l
        test -e /dev/sda && echo "sda: есть" || echo "sda: нет"
    '
done

Критерий

Таблица из трёх колонок заполнена. Объяснено, зачем нужна колонка --cap-add=ALL: она отделяет вклад capabilities от остальных ослаблений --privileged.


Задание 6. Настройка userns-remap

Тип: дополнительное
Материал: 12.3

Настроить userns-remap и показать, что root в container отображается на непривилегированный UID host.

Задание меняет конфигурацию daemon и требует его перезапуска. Выполняйте только на учебной машине. Существующие образы и container'ы после включения станут недоступны — они хранятся в отдельном каталоге.

Требования:

  1. Зафиксировать состояние до: содержимое /proc/self/uid_map в container.
  2. Включить userns-remap в /etc/docker/daemon.json.
  3. Перезапустить daemon, убедиться, что он запустился.
  4. Показать uid_map после и объяснить разницу.
  5. Создать файл в смонтированном каталоге и показать его владельца на host до и после.
  6. Назвать три ограничения userns-remap.
  7. Вернуть исходную конфигурацию.

Проверка

bash
docker run --rm alpine:3.21 cat /proc/self/uid_map
cat /etc/subuid | grep dockremap
ls -ln СМОНТИРОВАННЫЙ_КАТАЛОГ

Критерий

uid_map до включения содержит 0 0 4294967295; после — начинается с не-нулевого второго поля. Файл, созданный от root в container, принадлежит на host непривилегированному UID. Названы три ограничения.

Ориентир для самопроверки

Ограничения: несовместимость с --pid=host и --network=host; невозможность использовать --privileged в полном объёме; сложности с bind mount, где нужны конкретные UID; отдельное хранилище образов, требующее повторного скачивания.


Задание 7. Свой профиль seccomp

Тип: дополнительное
Материал: 12.5

Написать профиль, блокирующий конкретный системный вызов, и проверить его.

Требования:

  1. Выбрать вызов, разрешённый профилем по умолчанию, — например chown или socket.
  2. Написать профиль на основе профиля Docker с запретом только этого вызова.
  3. Показать, что операция работает без профиля и не работает с ним.
  4. Показать, что остальное приложение продолжает работать.
  5. Оценить, что произойдёт с профилем при обновлении базового образа.
  6. Обосновать, стоит ли применять этот профиль в вашем проекте.

Проверка

bash
docker run --rm --security-opt seccomp=./profile.json ОБРАЗ КОМАНДА
docker run --rm ОБРАЗ КОМАНДА     # для сравнения

Критерий

Профиль блокирует ровно выбранный вызов. Пункт 6 содержит вывод, а не описание — для большинства проектов обоснованный ответ отрицательный.


Задание 8. Приоритизация находок сканера

Тип: дополнительное
Материал: 12.7, 11.6

Проверить образ сканером и обосновать приоритет каждой находки.

Если сканер (Trivy, Grype, docker scout) недоступен — отметьте это явно и выполните доступную часть по метаданным образа. Не имитируйте вывод сканера.

Требования:

  1. Просканировать образ вашего проекта.
  2. Для находок уровня critical и high определить: есть ли исправление, достижим ли код, применим ли обходной путь.
  3. Обновить базовый образ и просканировать повторно.
  4. Показать, сколько находок закрылось одним этим действием.
  5. Для оставшихся указать конкретное действие или обоснование бездействия.

Проверка

bash
trivy image ОБРАЗ --severity CRITICAL,HIGH 2>/dev/null || echo "trivy недоступен"
docker scout cves ОБРАЗ 2>/dev/null || echo "docker scout недоступен"

Критерий

Каждая находка critical/high получила решение с обоснованием. Показано, что обновление базового образа закрывает большинство. Недоступность инструментов, если она есть, отмечена явно.


Задание 9. Диагностика: пять нарушений в compose.yaml

Тип: диагностическое
Материал: весь раздел

Дан файл с пятью нарушениями безопасности. Найти все, обосновать каждое сценарием риска, исправить.

yaml
name: vulnerable-stack

services:
  api:
    build: .
    privileged: true
    environment:
      DATABASE_PASSWORD: суперсекрет123
      API_TOKEN: ghp_EXAMPLEnotArealTokenAAAAAAAAAAAAAAAAAA
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /:/host
    ports:
      - "8000:8000"

  db:
    image: postgress/postgres:17
    environment:
      POSTGRES_PASSWORD: пароль
    volumes:
      - dbdata:/var/lib/postgresql/data

volumes:
  dbdata:

Требования:

  1. Найти все нарушения — их больше пяти.
  2. Для каждого описать конкретный сценарий: что сможет сделать атакующий.
  3. Расставить приоритеты: что исправлять первым и почему.
  4. Написать исправленный файл.
  5. Показать, что исправленный стек работает.

Критерий

Найдено не менее пяти нарушений. Для каждого приведён сценарий, а не ссылка на правило. Приоритет обоснован.

Ориентир: что искать

Порядок соответствует убыванию тяжести:

НарушениеПочему первое
Монтирование Docker socketПрямой root на host: атакующий запускает container с -v /:/host
privileged: trueВсе capabilities, все устройства, seccomp отключён
Монтирование / в /hostЧтение и запись любого файла host
image: postgress/postgres:17Опечатка в пространстве имён: сторонний образ вместо официального
Секреты в environmentВидны в docker inspect, наследуются потомками
Токен, похожий на настоящийЕсли он действующий — утёк в репозиторий
Слабый пароль БДпароль подбирается мгновенно
Нет cap_drop, read_only, no-new-privilegesОтсутствие базового hardening
Нет userРабота от root внутри container

Первые три образуют цепочку: любой из них по отдельности даёт root на host. Их исправляют первыми — остальное без этого не имеет значения.


Задание 10. Аудит собственного проекта ★

Тип: итоговое
Материал: весь раздел

Провести аудит проекта из раздела 10 по security checklist.

Требования:

  1. Пройти чек-лист по всем пунктам, зафиксировав фактическое состояние.
  2. Для каждого невыполненного пункта: сценарий риска, стоимость исправления, решение.
  3. Исправить всё, что относится к первым трём звеньям цепочки поставок.
  4. Написать проверку, автоматизирующую не менее пяти пунктов чек-листа.
  5. Составить сводку по звеньям с указанием слабейшего.
  6. Записать решение по каждому неисправленному пункту с условием пересмотра.

Проверка

Проверка из пункта 4 должна:

  • возвращать ненулевой код при нарушениях;
  • не выводить значения найденных секретов;
  • отличать «проверка не выполнена» от «проверка пройдена».

Критерий

Все пункты чек-листа имеют статус. Невыполненные — с обоснованным решением, а не пропущены. Проверка работает на вашем проекте и даёт разные коды возврата на нарушенной и исправленной конфигурации.

Ориентир для самопроверки

Признак хорошего аудита — наличие пунктов со статусом «не исправляем, потому что…». Аудит, где всё исправлено, обычно означает, что чек-лист прошли формально.

Признак хорошей автоматической проверки — она находит нарушение, которое вы внесли специально, и молчит на исправленной конфигурации. Проверьте оба случая.


Сводная таблица

№ЗаданиеТипОсновной материал
1Docker socket равен rootобяз.12.2
2Извлечение секрета из образаобяз.12.6
3Сборка без утечки секретаобяз.12.6
4Минимальный набор capabilitiesобяз.12.4
5Обычный против privilegedобяз.12.4
6Настройка userns-remapдоп.12.3
7Свой профиль seccompдоп.12.5
8Приоритизация находок сканерадоп.12.7
9Пять нарушений в compose.yamlдиаг.весь раздел
10Аудит собственного проекта★весь раздел

Что должно получиться

После заданий 1–5 у вас есть практическое подтверждение четырёх утверждений раздела:

  1. Доступ к Docker socket равен root на host — проверено на своей машине.
  2. Секрет, попавший в образ, извлекается тремя командами.
  3. Secret mount закрывает все три канала — при условии, что секрет не копируют.
  4. Типичному веб-приложению не нужна ни одна capability.

Задания 6–8 добавляют механизмы, применяемые по обоснованию, а не по умолчанию.

Задание 9 проверяет умение находить нарушения в чужой конфигурации; задание 10 — в своей.

Навигация

← Предыдущий материал
Вернуться к разделу
Следующий раздел: Observability и диагностика →
Главное оглавление

Markdown на GitHub ↗