В конце курса «Внедрение и работа в DevSecOps» студентов ждёт выполнение проекта. Это самостоятельная работа, необходимая для закрепления полученных знаний. Предлагаем вашему вниманию проект Сергея Кушнерчука, одного из выпускников курса.
Данный проект является одним из микросервисов реальной системы, реализующей оказание юридических услуг, поэтому это практическая, а не теоретическая работа.
Postmortem.
Некоторое время назад произошел ряд инцидентов, связанных с информационной безопасностью, что привело к финансовым и репутационным потерям компании.
Было принято решение внедрить этапы безопасной разработки в весь жизненный цикл приложения, начав с разработки, тестирования и сборки приложения.
План работы по внедрению этапов DevSecOps.
- Анализ состояния вопросов безопасности в проекте.
- определение технологического стека приложения;
- предварительная оценка состояния кодовой базы;
- предварительная оценка текущего цикла сборки и тестирования приложения.
- Подготовка и внедрение в конвейер этапов безопасной разработки (первая итерация)
- определение этапов, которые будут внедрены;
- выбор инструментов;
- реализация выбранных решений и подготовка нового конвейера.
- Оценка результатов работы
- Предварительное планирование следующих этапов по внедрению DevSecOps в жизненный цикл приложений.
1. Анализ состояния вопросов безопасности в проекте.
Определение технологического стека приложения.
Технологический стек, используемый в работе приложения:
- Python 3.10
- Docker
- MariaDB
- MongoDB
- FluentD
- RabbitMQ
- MinIO
- Debian 10
Все операции по разработке, сборке и тестированию на момент написания работы выполнялись в GitLab EE 16.2.4 Ultimate.
На момент принятия проекта в работу какие-либо меры безопасной разработки в проекте отсутствовали. По результатам первичного анализа (без использования средств автоматизации) был обнаружен ряд грубых нарушений, таких как:
Наличие в исходном коде чувствительных данных и необоснованное использование повышенных привилегий:
Запуск образа приложения от пользователя root:
FROM python:3.10-slim RUN apt-get update && \ apt-get install -y --no-install-recommends curl build-essential gcc libssl-dev default-libmysqlclient-dev \ libc-dev libxml2-dev libxslt-dev wkhtmltopdf libmagic1 && \ curl -sSL https://install.python-poetry.org | POETRY_HOME=/opt/poetry python && \ cd /usr/local/bin && \ ln -s /opt/poetry/bin/poetry && \ poetry config virtualenvs.create false && \ apt-get remove curl -y && \ apt-get clean -y WORKDIR /app COPY ./app /app COPY ./docker-entrypoint.sh /app/ COPY ./wait-for-db.sh /app/ COPY pyproject.toml poetry.lock* /app/ ARG INSTALL_DEV=false RUN bash -c "if [ $INSTALL_DEV == 'true' ] ; then poetry install --no-root ; else poetry install --no-root --no-dev ; fi" ENV PYTHONPATH=/ ENTRYPOINT ["/app/docker-entrypoint.sh"]
Отсутствие проверок линтеров и каких-либо сканеров безопасности в цикле сборки образа приложения.
Различие docker-образов python, используемых для тестирования и сборки приложения:
- тестирование — python:3.8.5
- сборка итогового образа: python:3.10-slim
Предварительная оценка текущего цикла сборки и тестирования приложения.
Существующий конвейер — простой и прямолинейный, какие-либо меры безопасности отсутствуют полностью:
stages: - test - build # Значения переменных ниже убраны намеренно variables: MYSQL_ROOT_PASSWORD: MYSQL_USER: DB_USER: DB_PASSWORD: DB_HOST: DB_PORT: BASE_URL: AUTHJWT_SECRET_KEY: EMAILS_FROM_EMAIL: EMAIL_ANALYZE: STAGE_SERVER_IP: STAGE_SERVER_USER: Run tests: tags: - python stage: test image: python:3.8.5 services: - name: mariadb:10.3 command: [ 'mysqld', '--character-set-server=utf8mb4', '--collation-server=utf8mb4_unicode_ci' ] script: - curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | POETRY_HOME=/opt/poetry python - ln -s /opt/poetry/bin/poetry /usr/local/bin/poetry - poetry config virtualenvs.create false - poetry install --no-root - rm -f .dev.env only: - branches - tags when: manual Build stage image: stage: build tags: - python script: - docker build -t "$REGISTRY_IMAGE:latest" . - sleep 2 - docker push "$REGISTRY_IMAGE:latest" only: - development when: manual Build production image: stage: build tags: - python rules: - if: $CI_COMMIT_TAG =~ /^v\d+.\d+.\d+$/ when: manual - if: $CI_COMMIT_TAG =~ /^v\d+.\d+.\d+\-patch$/ when: manual script: - DATE=`date +%d-%m-%Y` - TAG=$DATE_$CI_COMMIT_TAG - docker build --pull -t "$REGISTRY_IMAGE:$TAG" . - docker push "$REGISTRY_IMAGE:$TAG" before_script: - set -xe - docker login -u "$REGISTRY_USER" -p "$REGISTRY_PASSWORD" $REGISTRY after_script: - docker logout $REGISTRY
Мы начнем подготовку нового конвейера с чистого листа, сначала внедрив некоторые наши этапы.
2. Подготовка и внедрение в конвейер этапов безопасной разработки.
Планирование внедряемых этапов.
Для реализации поставленных целей на начальном этапе мы планируем реализовать:
- Настройку проекта средствами GitLab.
- Определение наличия чувствительных данных в кодовой базе.
- Проверку зависимостей.
- SAST
- Проверку исходного кода линтерами.
- Сканирование итоговых образов на наличие уязвимостей.
- Защиту итогового конвейера от изменений.
Выбор инструментов
Мы будем использовать на первой итерации только штатные средства, которые предоставляет нам GitLab — его арсенал достаточно обширен и покрывает все наши потребности на данном этапе. Однако, если вы не являетесь владельцем ultimate-версии GitLab, то вам придется искать другие варианты решения спланированных задач, а также будут недоступны штатные инструменты, формирующие отчеты о безопасности.
Реализация выбранных решений и подготовка конвейера.
Настройка проекта.
Push rules: Settings -> Repository -> Push rules
Выставляем нужные настройки, как указано на изображении ниже:
Основным флажком, который точно должен быть установлен, является Prevent pushing secret files, остальные настройки выставляются в соответствии с регламентом разработки.
Защищенная ветка: Settings -> Repository -> Protected branches
Ветка, которая содержит стабильный код и из которой выпускается код в production, должна быть защищена от случайных изменений. В данном случае только maintainer проекта принимает решение о слиянии кода из остальных веток в нее, а прямая заливка запрещена вообще всем:
Merge requests: Settings -> Merge requests -> Merge requests
Для предотвращения слияния реквестов, которые еще не разрешены, или имеют не успешные конвейеры, необходимо выставить параметры:
Определение наличия чувствительных данных в проекте.
Задача поиска утечек паролей, токенов, сертификатов и других чувствительных данных в коде проекта задача не совсем простая — высока вероятность как пропуска реальных данных, так и ложноположительных срабатываний, поэтому этот этап не должен являться блокирующим для выпуска приложения. Он должен быть включен в регламент code review и являться основанием как для выдачи замечаний, так и чистки истории от попавших в репозиторий данных.
Существует несколько утилит для решения такой задачи, но мы пока ограничимся штатным средством, которое предоставляет GitLab, так как не хотим потерять возможности удобного встроенного средства анализа безопасности — Security dashboard и Vulnerability report, тем более что при необходимости мы можем добавить свои правила для поиска чувствительных данных — под капотом для решения этой задачи GitLab использует gitleaks
Добавим этот этап:
variables: SECURE_LOG_LEVEL: 'debug' SECRET_DETECTION_HISTORIC_SCAN: 'true' SEARCH_MAX_DEPTH: 1000 include: - template: Security/Secret-Detection.gitlab-ci.yml stages: - security secret_detection: stage: security artifacts: reports: sast: gl-secret-detection-report.json
После запуска конвейера видим, что помимо найденного ранее, обнаружено еще три места, где возможно есть чувствительные данные:
Однако это ложные срабатывания, которые мы можем пометить как *#gitleaks:allow, чтобы эти строки в будущем не учитывались при анализе:
def get_url(): user = os.getenv("DB_USER", "") password = os.getenv("DB_PASSWORD", "") server = os.getenv("DB_HOST", "") db = os.getenv("DB_NAME", "") print(f"mysql://{user}:{password}@{server}/{db}?charset=utf8mb4") # *#gitleaks:allow return f"mysql://{user}:{password}@{server}/{db}?charset=utf8mb4" # *#gitleaks:allow
Проверка зависимостей.
Добавим проверку используемых зависимостей на уязвимости (эта же задача сформирует нам отчет о лицензиях используемых зависимостей):
variables: LICENSE_MANAGEMENT_VERSION: 4 include: - template: Security/Dependency-Scanning.gitlab-ci.yml
В результате работы конвейера получаем список из 21 уязвимости:
SAST
Добавим этап SAST:
variables: SAST_EXPERIMENTAL_FEATURES: "true" include: - template: Security/SAST.gitlab-ci.yml
После запуска конвейера мы получим 104 найденные проблемы SAST:
Проверка исходного кода линтерами.
flake8, mypy
Эти линтеры уже были добавлены в проект ранее как зависимости, в проекте есть их конфигурационные файлы, однако в конвейерах не использовались. Сделаем так, чтобы пока эти этапы не влияли на конечный результат сборки, однако, в случае наличия проблем, это было визуально видно:
stages: - linters flake8: stage: linters script: | pip3 install flake8 && flake8 . allow_failure: true mypy: stage: linters script: | pip3 install mypy && pip3 install sqlalchemy-stubs && mypy . allow_failure: true
Сканирование итоговых образов на наличие уязвимостей.
Для реализации этапа сначала вернем в конвейер базовую сборку:
build: needs: - tests stage: build script: | docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY docker build -t $DEST_DOCKER_IMAGE . docker push $DEST_DOCKER_IMAGE docker logout $CI_REGISTRY only: - branches
Для проверки образов также будем использовать уже встроенную возможность GitLab (под капотом там trivy с некоторой оберткой)
include: - template: Security/Container-Scanning.gitlab-ci.yml variables: CS_IMAGE: $DEST_DOCKER_IMAGE CS_REGISTRY_USER: $CI_REGISTRY_USER CS_REGISTRY_PASSWORD: $CI_REGISTRY_PASSWORD CS_DISABLE_LANGUAGE_VULNERABILITY_SCAN: "true" stages: - post_security container_scanning: stage: post_security needs: - build
Для такой проверки мы сделаем отдельный этап post_security, который будет запускаться после сборки приложения:
Результат работы этого этапа представлен ниже:
Итак, конвейер, содержащий необходимые базовые операции по проверкам безопасности, а также тестированию и базовой сборке образа приложения выглядит так:
variables: BUILD_DOCKER_IMAGE: python:3.10-slim DEST_DOCKER_IMAGE: $CI_REGISTRY_IMAGE:$CI_PIPELINE_ID SECURE_LOG_LEVEL: 'debug' SECRET_DETECTION_HISTORIC_SCAN: "true" SEARCH_MAX_DEPTH: 1000 SAST_EXPERIMENTAL_FEATURES: "true" CS_IMAGE: $DEST_DOCKER_IMAGE CS_REGISTRY_USER: $CI_REGISTRY_USER CS_REGISTRY_PASSWORD: $CI_REGISTRY_PASSWORD CS_DISABLE_LANGUAGE_VULNERABILITY_SCAN: "true" LICENSE_MANAGEMENT_VERSION: 4 include: - template: Security/Secret-Detection.gitlab-ci.yml - template: Security/Dependency-Scanning.gitlab-ci.yml - template: Security/SAST.gitlab-ci.yml - template: Security/Container-Scanning.gitlab-ci.yml stages: - security - linters - test - build - post_security secret_detection: stage: security artifacts: reports: sast: gl-secret-detection-report.json dependency_scanning: stage: security artifacts: reports: sast: gl-dependency-scanning-report.json sast: stage: security flake8: image: $BUILDER_DOCKER_IMAGE stage: linters script: | pip3 install flake8 && flake8 . allow_failure: true mypy: stage: linters script: | pip3 install mypy && pip3 install sqlalchemy-stubs && mypy . allow_failure: true tests: stage: test services: - name: mariadb:10.3 command: [ 'mysqld', '--character-set-server=utf8mb4', '--collation-server=utf8mb4_unicode_ci' ] script: - pip3 install poetry - poetry config virtualenvs.create false - poetry install - python -m pytest --cov only: - branches build: needs: - tests stage: build script: | docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY docker build -t $DEST_DOCKER_IMAGE . docker push $DEST_DOCKER_IMAGE docker logout $CI_REGISTRY only: - branches container_scanning: stage: post_security needs: - build
В ходе работы были приняты несколько дополнительных решений:
- Сделать так, чтобы все эти этапы могли быть переиспользованы в других аналогичных сервисах с минимальными изменениями, или вовсе без них.
- Максимально освободить разработчиков от погружения в вопросы DevSecOps в процессе жизненного цикла приложения.
- Защитить конвейер от случайных или злонамеренных изменений, внесенных разработчиками, при этом оставив возможность простого управления им.
Для внедрения принятых решений мы будем использовать возможность GitLab выполнять включение конвейеров из других репозиториев. Для этого мы перенесем его в другой репозиторий той же группы GitLab, а в нашем проекте выполним его включение:
include: - project: 'services/ci' ref: master file: 'python-ci.yml'
Таким образом мы решаем первую и вторую поставленные задачи — разработчикам больше нет необходимости думать о настройке CI/CD — им достаточно взять уже готовый файл для размещения в уже существующих проектах, а для новых проектов можно подготовить шаблон, который уже будет содержать все необходимое.
Теперь мы можем выполнить слияние наших изменений в продуктовую ветку, и приступить к решению задачи по защите конвейера от изменений.
Защита итогового конвейера от изменений
На период внедрения и тестирования этапов DevSecOps было принято решение о запрете вмешательства разработчиков в подготовку и работу конвейера. Технически наиболее просто достичь этого можно двумя способами:
- Используя настройку Settings -> CI/CD -> General pipelines, в которой указать путь к файлу конвейера, например:
python-ci.yml@services/ci:master
- Установив блокировку на файл конвейера в проекте пользователем с максимальным уровнем привилегий (на весь этап работы это член команды DevSecOps) — это блокирует его во всех ветках проекта:
Первый вариант нам не подходит, так как мы хотим в будущем сделать вариативный конвейер, управление жизненным циклом которого будет осуществляться с помощью переменных, размещаемых в его файле, поэтому мы будем использовать блокировку файла конвейера, которая в будущем будет снята.
Оценка результатов работы.
Итогом первой итерации стал конвейер, который показывает, насколько сильно подвержено рискам безопасности приложение.
Также было принято решение отложить выпуск последующих релизов до устранения критических проблем, поэтому этап полной сборки и деплоя продуктового образа пока в конвейере не реализован. Сроки восстановления релизов на данном этапе не ясны — и это хорошо показывает, как халатное отношение к вопросам безопасности может навредить бизнесу.
4. Предварительное планирование следующих этапов по внедрению DevSecOps в жизненный цикл приложения.
Для устранения всех выявленных и недопущения новых недостатков мы можем спланировать следующие этапы нашей работы:
- Разработка и согласование плана для формирования пула задач, которые будут решаться командами разработки и DevSecOps приоритетно.
- Интеграция и использование разработчиками необходимых инструментов безопасности в их локальном окружении.
- Более тонкая настройка инструментов (или их замена на более совершенные) для исключения ложных и пропуска настоящих срабатываний, а также внедрение нового инструментария для решения других возникших задач.
- Разработка скриптов автоматизации (при необходимости).
- Определение и применение блок-факторов безопасности для сборки или выпуска релизов приложения (на данном этапе это критические проблемы для выпуска релизов, тестовые сборки будут продолжаться чтобы не блокировать работу команд разработки полностью).
- Внедрение автоматизированных проверок работающих docker-контейнеров на тестовых и продуктовых контурах на наличие открытых портов, правил монтирования файловых систем, установки ограничений используемых ресурсов и привилегий и т.п.
- Разделение итогового конвейера на составные части, чтобы этапы безопасности можно было подключать в других схожих проектах, но которые имеют отличающийся регламент разработки и дальнейшее развитие этих этапов.
- Отправка сведений об обнаруженных в ходе сборки или работы приложения проблемах безопасности в учетную систему.
Итоги.
Отсутствие этапов безопасной разработки в проекте в течение полутора лет его существования наглядно показало, к каким последствиям это может привести — конкретно в этом случае это стоило компании нескольких недель простоя, потери ряда клиентов и части репутации из-за произошедших инцидентов.
Из всего произошедшего были сделаны правильные выводы — теперь вопросы информационной безопасности закладываются уже на этапе формирования требований и технических заданий, с привлечением профильных специалистов, включая не только автоматические проверки на этапах сборки и тестирования, но и привлечения сторонних подрядчиков для проведения аудита как приложений, так и инфраструктуры компании.
Хотите знать больше про DevSecOps? Добро пожаловать на специализированный курс в Otus!