Главная/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 ↗