
В прошлой части цикла мы подготовили фундамент будущего РБПО: развернули виртуальные машины, настроили Ubuntu Server, сеть, брандмауэр и Docker. Другими словами, построили площадку, на которой теперь можно начинать возводить сам конвейер безопасной разработки.
Теперь пора наполнять наши виртуальные машины полезным содержимым. Если продолжать сказочную аналогию — заселим наше царство безопасной разработки первыми жителями: хранителем секретов, смотрителем артефактов, летописцем уязвимостей и прочими полезными персонажами. То есть установим и настроим GitLab, Nexus, HashiCorp Vault, DefectDojo и Dependency-Track, а также подготовим отдельную виртуальную машину с набором CLI-инструментов для анализа безопасности.
Большая часть сервисов будет работать в Docker-контейнерах, поэтому заодно разберемся с настройкой постоянного хранения данных, Docker Compose и некоторыми особенностями конфигурации отдельных компонентов.
Что ж, глаза боятся, а руки команды в терминале набирают. Начнем с GitLab.
Дисклеймер
Важно сразу условиться, Путник!
Данный цикл статей не заменяет требования ГОСТ, OWASP SAMM, BSIMM и других серьезных методологий безопасной разработки. Это скорее практический пример того, как можно организовать конвейер безопасной разработки ПО с минимальным вложением средств.
Всё описанное в цикле — исключительно мой личный опыт, набитые шишки и результаты долгих вечеров за клавиатурой. Это не официальная позиция моего текущего или бывших работодателей.
Воспринимать статью как руководство к бездумному копированию тоже не стоит. Проверяй всё на тестовых стендах, делай бэкапы и подходи к экспериментам с холодной головой.
Ошибки возможны, и не нужно бояться их совершать. Главное — делать выводы и постепенно строить процессы, которые будут надежнее вчерашних.
GitLab
«GitLab — наше всё», — подумал я и решил: ставим Community Edition, потому что денег на платную версию у царя стартаподелающего нет (да и у меня тоже). К счастью, GitLab CE ставится чуть ли не одной командой.
Сначала добавим официальный репозиторий GitLab, чтобы наш пакетный менеджер знал, откуда качать:
curl -sS https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.deb.sh | sudo bash
Потом одной командой установим пакет:
sudo apt install -y gitlab-ce
После установки нужно указать адрес, по которому будет доступен GitLab. Для этого редактируем конфиг /etc/gitlab/gitlab.rb. Нужно указать внешний URL (можно использовать IP или домен, но на сейчас я оставлю http://192.168.0.201).
sudo nano /etc/gitlab/gitlab.rb
Ищем строчку external_url и правим ее, вписывая свой IP. Порт 8080 использовать не стоит — его в нашем стенде занимает встроенный сервер Puma.
external_url ‘http:// 192.168.0.201:9090’;
Применяем конфигурацию (нужно будет немного подождать):
sudo gitlab-ctl reconfigure
Проверяем статус, чтобы увидеть весь список запущенных сервисов:
sudo gitlab-ctl status
После завершения GitLab будет доступен по адресу http://192.168.0.201:9090.
Пробуем войти. Логин и пароль по умолчанию:
Логин — root;
Пароль — временный пароль, сгенерированный при установке, хранится на виртуальной машине. Доступен по команде:
sudo cat /etc/gitlab/initial_root_password

Nexus
Привычную загрузку дистрибутива Nexus через wgetс официального сайта затруднительно реализовать в силу блокировок, поэтому я решил пойти другим путем — развернуть Nexus в Docker. Чтобы данные не потерялись при пересоздании контейнера, сразу настроим постоянное хранилище на хосте.
Да-да, знаю, некоторые скажут: «Небезопасно, ох небезопасно!». Но если хакер уже залез на виртуалку, то ему всё равно, где данные лежат — в контейнере или рядом. А вот в условиях нашего лабораторного стенда такой подход вполне оправдан. Поэтому создаем файловую систему, подготавливаем папки и запускаем docker-compose.
Сначала создадим директорию для данных Nexus и дадим правильные права пользователю с UID 200 — это внутренний пользователь контейнера. Затем напишем docker-compose.yml с пробросом порта 8081 и примонтируем том. Запустим — и готово.
Создаю папку для данных:
sudo mkdir -p /opt/nexus-data
Важно! Nexus внутри контейнера работает от пользователя с UID 200. Нужно дать права на папку этому пользователю.
sudo chown -R 200:200 /opt/nexus-data
Создаю файл docker-compose.yml:
version: '3' services: nexus: image: sonatype/nexus3:latest container_name: nexus restart: unless-stopped ports: - "8081:8081" volumes: - nexus-data:/nexus-data volumes: nexus-data: # Указываю конкретную папку: driver_opts: type: none device: /opt/nexus-data o: bind
Запускаю Nexus командой:
docker-compose up -d
Проверяю работу в браузере по адресу http://192.168.0.202:8081/.
Получаю окно логина. Ура, у нас есть хранилище артефактов и хранилище внешних зависимостей!

Теперь получаю пароль для входа в сервис:
docker exec nexus cat /nexus-data/admin.password
Проверка сохранения данных. Меняю пароль для входа в сервис. Торможу сервис и удаляю контейнер командами:
docker stop nexus docker rm nexus
Запускаю docker-compose:
docker-compose up -d
Логинюсь с тем паролем, на который я заменил стандартный. Если он подходит, значит, хранилище смонтировано корректно.
Всё работает! Огонь, погнали дальше.
HashiCorp Vault
В любом уважающем себя царстве есть сокровищница. В нашем стенде ее роль будет выполнять HashiCorp Vault, где будут храниться пароли, токены и другие секреты.
Как и в случае с Nexus, для развертывания будем использовать docker-compose и постоянное хранилище на хосте. Надо же когда-то привыкать к контейнеризации.
Для начала подготовим структуру каталогов и конфигурацию Vault. Для лабораторного стенда я выбрал хранилище типа file: кластерный режим мне пока не нужен, а ресурсы хочется расходовать экономно.
Но помните! Для продакшена берите raft, как в табличке ниже.
Немного информации:
Используйте file, если вы разворачиваете Vault для ознакомления, разработки или в сценариях, где допустимы простои и не требуется масштабирование.
Используйтеraft для production-окружений, где важна непрерывность работы. Даже если у вас только один узел, raft можно настроить в режиме одного сервера, при этом вы получите возможность легко добавить узлы в будущем.
Тип хранилища |
file |
raft |
Режим работы |
Только один узел |
Кластер (3+ узлов) |
Высокая доступность |
Нет |
Да (автоматическая смена лидера) |
Репликация данных |
Отсутствует |
Полная репликация между узлами |
Требования к инфраструктуре |
Локальный диск |
Ничего внешнего (кроме сети между узлами) |
Простота настройки |
Очень простая |
Требуется конфигурация кластера |
Использование в продакшене |
Не рекомендуется |
Рекомендуется |
Пример конфигурации |
storage "file" { path = "/vault/data" } |
storage "raft" { path = "/vault/raft" node_id = "node1" } |
Создаю директорию ~/vault, внутри — папки для данных, логов, сертификатов. Пишу простой конфиг с файловым хранилищем. Затем поднимаю контейнер через docker-compose и выполня. инициализацию Vault внутри контейнера. Не забудьте сохранить ключи распечатки и root-токен!
Создаю следующую структуру директорий и раздаю права:
mkdir -p ~/vault/ cd ~/vault mkdir -p data logs certs init-scripts cd data
Выдаю права:
sudo chmod 755 data logs certs init-scripts sudo chown -R 100:1000 data logs
Для домашней лаборатории буду использовать хранилище типа file.
Создаю файл конфигурации Vault /vault/config/config.hcl:
storage "file" { path = "/vault/data" } listener "tcp" { address = "0.0.0.0:8200" tls_disable = true } ui = true disable_mlock = true # обязательно для работы в контейнере
Создаю файл docker-compose (docker-compose.yml). Главное — не забыть заменить http://vault:8200 на свой IP...
version: '3.8' services: vault: image: hashicorp/vault:latest container_name: vault restart: unless-stopped ports: - "8200:8200" volumes: - ./data:/vault/data:rw - ./config/config.hcl:/vault/config/config.hcl:ro cap_add: - IPC_LOCK environment: - VAULT_ADDR=http://127.0.0.1:8200 command: vault server -config=/vault/config/config.hcl
Запускаю Vault. Всё работает — хранилище секретов готово.
docker-compose up -d
Инициализирую Vault. Первый запуск требует ряд манипуляций внутри контейнера.
docker exec -it vault sh export VAULT_ADDR=http://127.0.0.1:8200
Инициализация (не забудьте сохранить ВСЕ выведенные ключи!):
vault operator init
Пример вывода:
# Unseal Key 1: YOUR_UNSEAL_KEY # Initial Root Token: s.YOUR_ROOT_TOKEN
Проверяю статус командой:
vault status
Выхожу из контейнера:
exit
Тестирую сохранение данных:
docker exec -it vault sh export VAULT_ADDR=http://127.0.0.1:8200 vault login YOUR_ROOT_TOKE
Включаю KV движок:
vault secrets enable -path=secret kv-v2
Создаю тестовый секрет:
vault kv put secret/test message="Hello Persistent Storage" created_by="docker-compose"
Проверяю:
vault kv get secret/test
Выхожу из контейнера:
exit
Перезапускаю контейнер с Vault:
docker-compose restart vault
Проверяю, что секрет на месте:
docker exec -it vault sh export VAULT_ADDR=http://127.0.0.1:8200
Vault будет запечатан после перезапуска! Сначала нужно его распечатать:
vault operator unseal YOUR_UNSEAL_KEY
Проверяю данные:
vault login YOUR_ROOT_TOKEN vault kv get secret/test
Все секреты на месте. Выхожу из контейнера:
exit

DefectDojo и Dependency-Track
«Одним махом — двух зайцев», — решил я. На одной виртуальной машине поселю сразу двух помощников: DefectDojo — для сбора всех результатов сканирований, и Dependency-Track — для анализа зависимостей. Они на разных портах сидят, не мешают друг другу. Установку выполним снова через docker-compose, с хранилищем, чтобы при перезапуске ничего не терялось.
DefectDojo оказался заметно сложнее предыдущих сервисов. Помимо самого приложения, для его работы используются PostgreSQL, Redis, Nginx, uWSGI и Celery. Чтобы не потеряться в этом зоопарке контейнеров, кратко разберем роль каждого компонента.
Компонент |
Основная роль в DefectDojo |
Зачем он нужен |
Nginx |
Обратный прокси-сервер и сервер для статики |
Принимает все входящие HTTP-запросы, обрабатывает HTTPS, отдает статические файлы (CSS, JS, изображения) и, самое главное, перенаправляет запросы к вашему приложению внутреннему серверу uWSGI. |
uWSGI |
Сервер приложений (Application Server) |
Это сервер, на котором непосредственно запущен код DefectDojo (Django). Он обрабатывает динамические запросы от Nginx, обращается к базе данных и возвращает готовые HTML-страницы. Nginx и uWSGI работают в паре. |
PostgreSQL |
Реляционная база данных |
Основное хранилище всех данных: информация о продуктах, тестах, найденных уязвимостях, пользователях, настройках и т.д. PostgreSQL — надежная и производительная СУБД, официально поддерживаемая DefectDojo. |
Redis |
Брокер сообщений (Message Broker) и кеш |
Выступает в роли диспетчера для отложенных задач. Когда приложению нужно выполнить что-то долгое (например, дедупликацию), оно отправляет задачу в Redis, откуда ее заберет Celery. Это позволяет веб-серверу не ждать и обрабатывать новые запросы. |
Celery (Worker & Beat) |
Фоновый обработчик и планировщик задач |
Worker запускает ресурсоемкие задачи в фоне, например, импорт результатов сканирования, дедупликацию уязвимостей или синхронизацию с Jira. Beat планирует выполнение периодических задач, например, отправку уведомлений о приближающихся сроках тестирования |
Чтобы данные не терялись при перезапуске контейнеров, настроим постоянное хранение для базы данных PostgreSQL и медиафайлов DefectDojo. Заодно изменим порт веб-интерфейса на 9090, чтобы избежать конфликта с Dependency-Track, который будет работать на той же виртуальной машине.
Для начала подготовим директории для DefectDojo и выдадим необходимые права:
mkdir ~/defectdojo cd ~/defectdojo
Создаю все возможные недостающие директории для DefectDojo и раздаю права.
mkdir -p docker/extra_settings mkdir -p docker/postgres mkdir -p docker/rabbitmq mkdir -p docker/data sudo mkdir -p /opt/defectdojo/media sudo chown -R 1000:1000 /opt/defectdojo/ sudo chmod -R 755 /opt/defectdojo/ chmod -R 755 docker/
Клонирую репозиторий с необходимыми файлами. В репозитории есть готовые примеры файлов для разных окружений. Копирую нужный файл для работы с docker-compose.
git clone https://github.com/DefectDojo/django-DefectDojo.git cd django-DefectDojo cp docker-compose.yml ../.
Дальше — настройка постоянного хранения. Редактирую docker-compose.yml. Прописываю внешнее хранилище, также не забываю сменить порт, чтобы избежать конфликта с Dependency-Track (я поставил 9090). Поскольку на этом этапе стенд будет работать по HTTP, закомментируем строки конфигурации, относящиеся к порту 8443.
Монтирую диски:
services: postgres: ... volumes: # Путь на хосте «:» путь в контейнере - /opt/defectdojo/postgres:/var/lib/postgresql/data ... uwsgi: ... volumes: # Для загруженных файлов - /opt/defectdojo/media:/app/media # Для кастомного nginx так как у нас на одной машине и DT и DD - ./nginx/conf.d:/etc/nginx/conf.d:ro ...
Добавляю в секцию uwsgi следующие переменные:
uwsgi: environment: DD_FORCE_SSL_REDIRECT: "False" DD_SECURE_SSL_REDIRECT: "False" DD_CSRF_COOKIE_SECURE: "False" DD_SESSION_COOKIE_SECURE: "False" DD_SITE_URL: "http://192.168.0.204:9090"
Комментирую строки, отвечающие за порт 8443:
ports: - “9090:8080” # - target: 8443 # published: ${DD_TLS_PORT:-8443} # protocol: tcp # mode: host
Создаю минимальный кастомный nginx-конфиг:
cd ~/defectdojo mkdir -p nginx/conf.d sudo nano nginx/conf.d/dojo.conf
Текст конфига:
upstream django { server uwsgi:9090; } server { listen 8080; server_name _; client_max_body_size 100M; location / { uwsgi_pass django; include /etc/nginx/uwsgi_params; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } location /static/ { alias /app/static/; } location /media/ { alias /app/media/; } }
Выполняю миграцию базы данных (на всякий случай, так как на форумах по разбору ошибок при установке я часто видел такую проблему):
docker compose exec uwsgi /bin/bash -c "python manage.py migrate"
Создаю суперпользователя:
docker compose exec uwsgi /bin/bash -c "python manage.py createsuperuser"
Получаю доступ к веб-интерфейсу. DefectDojo доступен по адресу http://192.168.0.204:9090/.

Установка OWASP Dependency-Track
Dependency-Track состоит из двух основных компонентов: API Server и Frontend. Официальный docker-compose.yml уже настроен на использование томов (volumes) для хранения данных базы данных и других файлов. Наша задача — сделать эти тома явными и при необходимости перенаправить их в удобную директорию на хосте.
Создаю директорию для Dependency-Track:
curl -sS https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.deb.sh | sudo bash
Редактирую файл для настройки томов (Persistent Volumes):
nano docker-compose.yml
Стандартный файл использует именованные тома Docker (например, dependency-track). Чтобы данные хранились в конкретной папке на моем сервере (например, /opt/dependency-track/data), я заменю их на свое хранилище.
Нахожу секцию volumes для сервиса dependency-track (это API сервер) и меняю ее. Я много помучался с этой историей, так как опыта в разворачивании dependency-track не было. Вот полный конфиг, который запустил сие творение на моей виртуальной машине (для наглядности удалил все стандартные закомментированные строки). Не забудьте поменять IP в API_BASE_URL: http://localhost:8081.
services: apiserver: image: dependencytrack/apiserver depends_on: postgres: condition: service_healthy environment: EXTRA_JAVA_OPTIONS: "-Xmx4G -Xms2G -Djava.io.tmpdir=/data/tmp" ALPINE_DATABASE_MODE: "external" ALPINE_DATABASE_URL: "jdbc:postgresql://postgres:5432/dtrack" ALPINE_DATABASE_DRIVER: "org.postgresql.Driver" ALPINE_DATABASE_USERNAME: "dtrack" ALPINE_DATABASE_PASSWORD: "dtrack" deploy: resources: limits: memory: 6g restart_policy: condition: on-failure ports: - '8081:8080' volumes: - "/opt/dependency-track/data:/data" - "/opt/dependency-track/tmp:/data/tmp" restart: unless-stopped frontend: image: dependencytrack/frontend depends_on: apiserver: condition: service_healthy environment: API_BASE_URL: "http://localhost:8081" ports: - "8080:8080" restart: unless-stopped postgres: image: postgres:17-alpine environment: POSTGRES_DB: "dtrack" POSTGRES_USER: "dtrack" POSTGRES_PASSWORD: "dtrack" POSTGRES_SHARED_BUFFERS: 256MB POSTGRES_EFFECTIVE_CACHE_SIZE: 512MB healthcheck: test: [ "CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}" ] interval: 5s timeout: 3s retries: 3 volumes: - "postgres-data:/var/lib/postgresql/data" restart: unless-stopped volumes: dtrack-data: {} postgres-data: {}
После настройки запускаю Dependency-Track:
docker compose up -d
Проверяю статус и дожидаюсь инициализации:
docker compose ps docker compose logs -f
Получаю доступ к веб-интерфейсу:
Frontend: http://ip_сервера:8080
Логин по умолчанию: admin / admin (система потребует сменить пароль при первом входе).

CLI-инструменты
На пятой виртуальной машине соберем набор утилит для анализа безопасности. Здесь будут жить инструменты для DAST-сканирования, анализа исходного кода и проверки секретов.
Поставим Checkov, Trivy, Gitleaks, OpenGrep, Nuclei и еще несколько инструментов. Часть — через APT, часть — руками из GitHub:
Для работы некоторых инструментов потребуется Node.js и npm. Установим их из стандартных репозиториев Ubuntu.
Checkov — через pip.
Trivy — через официальный репозиторий.
Gitleaks — скачиваем бинарник с GitHub.
OpenGrep — через скрипт установки, потом добавляем в PATH и клонируем правила.
Nuclei — скачиваем с GitHub и распаковываем.
Node
Устанавливаю:
sudo apt install nodejs
Проверяю версию:
node --version
NPM
Устанавливаю:
sudo apt install npm
Проверяю версию:
npm --version
Checkov
Устанавливаю:
sudo apt install -y python3-pip sudo pip install checkov
Проверяю версию:
checkov --version
Trivy
Установка вспомогательных пакетов:
sudo apt-get install -y wget apt-transport-https gnupg lsb-release
Импорт публичного GPG-ключа репозитория Trivy:
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add -
Добавление репозитория Trivy в список источников APT:
echo deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main | sudo tee -a /etc/apt/sources.list.d/trivy.list
Обновление списка пакетов и установка Trivy:
sudo apt update && sudo apt install -y trivy
Проверяю версию:
trivy --version
Gitleaks
Скачиваю архив с релизом Gitleaks:
wget -O gitleaks.tar.gz https://github.com/gitleaks/gitleaks/releases/download/v8.18.1/gitleaks_8.18.1_linux_x64.tar.gz
Распаковываю архив:
tar -xzf gitleaks.tar.gz
Перемещение бинарного файла в системный PATH:
sudo mv gitleaks /usr/local/bin/
Проверяю версию:
gitleaks version
OpenGrep
Установка чуть сложнее. Нужно установить и скачать правила. Для начала мы скачиваем скрипт установки:
curl -fsSL https://raw.githubusercontent.com/opengrep/opengrep/main/install.sh | bash
Даем права на выполнение самому бинарному файлу:
chmod +x /home/an/.opengrep/cli/v1.16.3/opengrep
Далее необходимо добавить ~/.local/bin в PATH (постоянно). Сначала проверяю, есть ли там бинарник:
ls -la ~/.local/bin/opengrep
Затем добавляю директорию в PATH, отредактировав ~/.bashrc:
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
Применяю изменения:
source ~/.bashrc
Проверяю версию установленного инструмента:
opengrep --version
Opengrep поддерживает все стандартные правила Semgrep, но у форка есть собственный репозиторий правил, который я обязательно использую. Клонирую официальный репозиторий правил Opengrep.
cd /opt sudo git clone https://github.com/opengrep/opengrep-rules.git
Очищаю от лишних файлов (git, CI/CD и т. д.):
cd opengrep-rules sudo rm -rf .git .github .pre-commit-config.yaml
Nuclei
Перед установкой необходимо скачать архив с последней версией Nuclei. Для этого используется инструмент wget:
Wget https://github.com/projectdiscovery/nuclei/releases/download/v3.1.10/nuclei_3.1.10_linux_amd64.zip unzip nuclei_3.1.10_linux_amd64.zip sudo mv nuclei /usr/local/bin/
Для Nuclei нужно добавить пути пользовательских бинарников в ~/.bashrc:
export PATH="$HOME/.local/bin:$PATH"
Применяю изменения:
source ~/.bashrc nuclei --version
Заключение
В результате у нас получилась базовая инфраструктура для стенда безопасной разработки. Мы развернули GitLab, Nexus, HashiCorp Vault, DefectDojo и Dependency-Track, а также подготовили отдельную виртуальную машину с набором CLI-инструментов для анализа безопасности.
Пока это лишь набор сервисов, каждый из которых работает сам по себе. Однако фундамент уже готов: есть система управления исходным кодом, хранилище артефактов, менеджер секретов, инструменты управления уязвимостями и средства автоматизированного анализа.
В следующей части научим жителей нашего царства безопасной разработки жить сообща — то есть перейдем к настройке и интеграции компонентов. Создадим репозитории в Nexus, настроим группы и раннеры в GitLab, подготовим аутентификацию в Vault, заведем продукты в DefectDojo и проекты в Dependency-Track. После этого можно будет переходить к построению полноценного DevSecOps-конвейера.

PURP — Telegram-канал, где кибербезопасность раскрывается с обеих сторон баррикад
t.me/purp_sec — инсайды и инсайты из мира этичного хакинга и бизнес-ориентированной защиты от специалистов Бастиона