В конце курса «Внедрение и работа в DevSecOps» студентов ждёт выполнение проекта. Это самостоятельная работа, необходимая для закрепления полученных знаний. Предлагаем вашему вниманию проект Сергея Кушнерчука, одного из выпускников курса.

Данный проект является одним из микросервисов реальной системы, реализующей оказание юридических услуг, поэтому это практическая, а не теоретическая работа.

Postmortem.

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

Было принято решение внедрить этапы безопасной разработки в весь жизненный цикл приложения, начав с разработки, тестирования и сборки приложения.

План работы по внедрению этапов DevSecOps.
  1. Анализ состояния вопросов безопасности в проекте.
  2. определение технологического стека приложения;
  3. предварительная оценка состояния кодовой базы;
  4. предварительная оценка текущего цикла сборки и тестирования приложения.
  5. Подготовка и внедрение в конвейер этапов безопасной разработки (первая итерация)
  6. определение этапов, которые будут внедрены;
  7. выбор инструментов;
  8. реализация выбранных решений и подготовка нового конвейера.
  9. Оценка результатов работы
  10. Предварительное планирование следующих этапов по внедрению DevSecOps в жизненный цикл приложений.

1. Анализ состояния вопросов безопасности в проекте.

Определение технологического стека приложения.

Технологический стек, используемый в работе приложения:

  • Python 3.10
  • Docker
  • MariaDB
  • MongoDB
  • FluentD
  • RabbitMQ
  • MinIO
  • Debian 10

Все операции по разработке, сборке и тестированию на момент написания работы выполнялись в GitLab EE 16.2.4 Ultimate.

На момент принятия проекта в работу какие-либо меры безопасной разработки в проекте отсутствовали. По результатам первичного анализа (без использования средств автоматизации) был обнаружен ряд грубых нарушений, таких как:

Наличие в исходном коде чувствительных данных и необоснованное использование повышенных привилегий:secret_data-20219-d5af7d.png

Запуск образа приложения от пользователя 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. Подготовка и внедрение в конвейер этапов безопасной разработки.

Планирование внедряемых этапов.

Для реализации поставленных целей на начальном этапе мы планируем реализовать:

  1. Настройку проекта средствами GitLab.
  2. Определение наличия чувствительных данных в кодовой базе.
  3. Проверку зависимостей.
  4. SAST
  5. Проверку исходного кода линтерами.
  6. Сканирование итоговых образов на наличие уязвимостей.
  7. Защиту итогового конвейера от изменений.
Выбор инструментов

Мы будем использовать на первой итерации только штатные средства, которые предоставляет нам GitLab — его арсенал достаточно обширен и покрывает все наши потребности на данном этапе. Однако, если вы не являетесь владельцем ultimate-версии GitLab, то вам придется искать другие варианты решения спланированных задач, а также будут недоступны штатные инструменты, формирующие отчеты о безопасности.

Реализация выбранных решений и подготовка конвейера.

Настройка проекта.

Push rules: Settings -> Repository -> Push rules

Выставляем нужные настройки, как указано на изображении ниже:

push_rules-20219-19a5a2.png

Основным флажком, который точно должен быть установлен, является Prevent pushing secret files, остальные настройки выставляются в соответствии с регламентом разработки.

Защищенная ветка: Settings -> Repository -> Protected branches

Ветка, которая содержит стабильный код и из которой выпускается код в production, должна быть защищена от случайных изменений. В данном случае только maintainer проекта принимает решение о слиянии кода из остальных веток в нее, а прямая заливка запрещена вообще всем:

protected_branch-20219-c94911.png

Merge requests: Settings -> Merge requests -> Merge requests

Для предотвращения слияния реквестов, которые еще не разрешены, или имеют не успешные конвейеры, необходимо выставить параметры:

merge_requests-20219-016b69.png

Определение наличия чувствительных данных в проекте.

Задача поиска утечек паролей, токенов, сертификатов и других чувствительных данных в коде проекта задача не совсем простая — высока вероятность как пропуска реальных данных, так и ложноположительных срабатываний, поэтому этот этап не должен являться блокирующим для выпуска приложения. Он должен быть включен в регламент 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-20219-edffda.png

Однако это ложные срабатывания, которые мы можем пометить как *#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 уязвимости:

depends-20219-a7de43.png

SAST

Добавим этап SAST:

variables:
  SAST_EXPERIMENTAL_FEATURES: "true"  
include:
  - template: Security/SAST.gitlab-ci.yml

После запуска конвейера мы получим 104 найденные проблемы SAST:

sast-20219-5be4d7.png

Проверка исходного кода линтерами.

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, который будет запускаться после сборки приложения:

pipeline-20219-0f38f4.png

Результат работы этого этапа представлен ниже:

trivy-20219-db53c8.png

Итак, конвейер, содержащий необходимые базовые операции по проверкам безопасности, а также тестированию и базовой сборке образа приложения выглядит так:

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

В ходе работы были приняты несколько дополнительных решений:

  1. Сделать так, чтобы все эти этапы могли быть переиспользованы в других аналогичных сервисах с минимальными изменениями, или вовсе без них.
  2. Максимально освободить разработчиков от погружения в вопросы DevSecOps в процессе жизненного цикла приложения.
  3. Защитить конвейер от случайных или злонамеренных изменений, внесенных разработчиками, при этом оставив возможность простого управления им.

Для внедрения принятых решений мы будем использовать возможность GitLab выполнять включение конвейеров из других репозиториев. Для этого мы перенесем его в другой репозиторий той же группы GitLab, а в нашем проекте выполним его включение:

include:
  - project: 'services/ci'
    ref: master
    file: 'python-ci.yml'

Таким образом мы решаем первую и вторую поставленные задачи — разработчикам больше нет необходимости думать о настройке CI/CD — им достаточно взять уже готовый файл для размещения в уже существующих проектах, а для новых проектов можно подготовить шаблон, который уже будет содержать все необходимое.

Теперь мы можем выполнить слияние наших изменений в продуктовую ветку, и приступить к решению задачи по защите конвейера от изменений.

Защита итогового конвейера от изменений

На период внедрения и тестирования этапов DevSecOps было принято решение о запрете вмешательства разработчиков в подготовку и работу конвейера. Технически наиболее просто достичь этого можно двумя способами:

  1. Используя настройку Settings -> CI/CD -> General pipelines, в которой указать путь к файлу конвейера, например:
python-ci.yml@services/ci:master

  1. Установив блокировку на файл конвейера в проекте пользователем с максимальным уровнем привилегий (на весь этап работы это член команды DevSecOps) — это блокирует его во всех ветках проекта:
lock-20219-491a77.png

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

Оценка результатов работы.

Итогом первой итерации стал конвейер, который показывает, насколько сильно подвержено рискам безопасности приложение.

result_pipeline-20219-a4a4f1.png
result-20219-dd26cf.png

Также было принято решение отложить выпуск последующих релизов до устранения критических проблем, поэтому этап полной сборки и деплоя продуктового образа пока в конвейере не реализован. Сроки восстановления релизов на данном этапе не ясны — и это хорошо показывает, как халатное отношение к вопросам безопасности может навредить бизнесу.

4. Предварительное планирование следующих этапов по внедрению DevSecOps в жизненный цикл приложения.

Для устранения всех выявленных и недопущения новых недостатков мы можем спланировать следующие этапы нашей работы:

  1. Разработка и согласование плана для формирования пула задач, которые будут решаться командами разработки и DevSecOps приоритетно.
  2. Интеграция и использование разработчиками необходимых инструментов безопасности в их локальном окружении.
  3. Более тонкая настройка инструментов (или их замена на более совершенные) для исключения ложных и пропуска настоящих срабатываний, а также внедрение нового инструментария для решения других возникших задач.
  4. Разработка скриптов автоматизации (при необходимости).
  5. Определение и применение блок-факторов безопасности для сборки или выпуска релизов приложения (на данном этапе это критические проблемы для выпуска релизов, тестовые сборки будут продолжаться чтобы не блокировать работу команд разработки полностью).
  6. Внедрение автоматизированных проверок работающих docker-контейнеров на тестовых и продуктовых контурах на наличие открытых портов, правил монтирования файловых систем, установки ограничений используемых ресурсов и привилегий и т.п.
  7. Разделение итогового конвейера на составные части, чтобы этапы безопасности можно было подключать в других схожих проектах, но которые имеют отличающийся регламент разработки и дальнейшее развитие этих этапов.
  8. Отправка сведений об обнаруженных в ходе сборки или работы приложения проблемах безопасности в учетную систему.

Итоги.

Отсутствие этапов безопасной разработки в проекте в течение полутора лет его существования наглядно показало, к каким последствиям это может привести — конкретно в этом случае это стоило компании нескольких недель простоя, потери ряда клиентов и части репутации из-за произошедших инцидентов.

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

Хотите знать больше про DevSecOps? Добро пожаловать на специализированный курс в Otus!