Привет! На связи Олег Казаков из Spectr.  

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

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

В первой статье поговорим о теории DevSecOps и подробно рассмотрим Pre-commit Checks.

Интро

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

Главный минус в том, что эта энтропия и большое количество частностей накладывают на нас ограничения в плане применимости тех или иных инструментов и практик, ведь нам приходится учитывать, например, следующие факторы:

  • в разных проектах (а иногда и в рамках одного проекта) может использоваться разный стек;

  • структура и код-стайл отличаются от проекта к проекту;

  • разные команды со своими процессами на разных проектах;

  • разная инфраструктура на разных проектах с уникальным набором инструментов и микросервисов.

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

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

Вкратце о DevSecOps

Если попытаться дать этому процессу краткое определение, то получается примерно следующее. 

DevSecOps — это методика интеграции в привычный DevOps различных стадий проверки безопасности во все этапы процесса CI/CD. Как правило, речь идет о следующих стадиях: Pre-commit Checks, Commit-time Checks, Post-build Checks, Test-time Checks, Deploy-time Checks.

  • Pre-commit Checks. Проверка кода на наличие конфиденциальной информации (пароли, секреты, токены и т. д.), которая не должна попасть в историю Git.

  • Commit-time Checks. Проверки, которые запускаются при выполнении коммита для контроля корректности и безопасности кода в репозитории.

  • Post-build Checks. Проверки, которые выполняются после сборки приложения и включают в себя тестирование артефактов сборки (например, docker-образов).

  • Test-time Checks. Этап тестирования развернутого приложения на наличие уязвимостей (например, для сканирования API на предмет популярных уязвимостей).

  • Deploy-time Checks. Выполняются при развертывании приложения и проверяют инфраструктуру на предмет уязвимостей.

В рамках первой статьи сосредоточимся на Pre-commit Checks и рассмотрим практические примеры внедрения этого в процесс разработки.

DevSecOps и GitLab

Общеизвестный факт состоит в том, что GitLab впереди планеты всей в плане наличия встроенных инструментов для DevSecOps, и в статье мы будем рассматривать именно его.

Но есть нюанс: существенная часть встроенных инструментов для работы с DevSecOps входит в Premium и Ultimate версии GitLab, а это нас не совсем устраивает. В рамках статей будем рассматривать решения, которые можно применить на любой версии.

Pre-commit Checks

Суть этапа: просканировать код на наличие «захардкоженой» в нем конфиденциальной информации (паролей, токенов, API-ключей) до того, как она попадет в историю Git. 

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

Пример 1. Утечка персональных данных 57 млн клиентов и водителей в Uber

Пример 2. Взлом и внедрение вредоносного ПО в SolarWinds

Пример 3. Взлом Codecov путем извлечения учетных данных из Docker-образа

Пример 4. Ежедневно через проекты GitHub утекают тысячи новых API или ключей

Как можно предотвратить попадание секретов в Git? Глобально есть три способа: использовать локальные Git Hooks, использовать Git server hooks, сканировать код в CI.

Локальные Git Hooks

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

Наш опыт показывает, что при использовании локальных гитхуков возникает много проблем:

  • Хуки нужно устанавливать и настраивать каждому разработчику локально. Это требует написания инструкций, что достаточно трудоемко.

  • Сложно поддерживать актуальность и полноту инструкций, т. к. часто у разработчиков разное ПО и ОС.

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

  • Существует человеческий фактор (например, разработчик случайно забыл включить гитхуки тимлид не заметил секрет в реквесте).

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

Сканирование кода в CI

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

Сканирование кода в CI

Надо сделать оговорку, что сканирование кода в CI – не совсем Pre-commit стадия, ведь де-факто конфиденциальные данные на момент выполнения проверок уже оказываются в истории. Тем не менее, описанный ниже подход позволяет откатить эти изменения и так или иначе конфиденциальные данные не станут доступны широкому кругу лиц.

Одно из  популярных решений для сканирования кода — Gitleaks

Gitleaks — Open-source-инструмент для статического анализа кода на предмет наличия в нем конфиденциальных данных, таких как пароли или ключи API, которые были включены в исходный код в явном виде без использования переменных или конфигурационных файлов.

Во всех версиях GitLab есть готовое решение Secret Detection, которое под капотом использует Gitleaks. Рассмотрим детально процесс его настройки.

Настройка Secret Detection в GitLab

Будем следовать инструкции по настройке Secret Detection в GitLab и подробно разберем все, что происходит под капотом. 

Для начала достаточно подключить встроенный template Security/Secret-Detection.gitlab-ci.yml и настроить стадию test в вашем .gitlab-ci.yml.

Ниже приведен пример стартовой конфигурации из .gitlab-ci.yml.

stages:
 - test

include:
 - template: Security/Secret-Detection.gitlab-ci.yml
  
secret_detection:
 variables:
   SECRET_DETECTION_HISTORIC_SCAN: "true"
 tags:
   - docker

Посмотрим более подробно на некоторые моменты в данной конфигурации. 

У Gitleaks внутри GitLab есть параметр SECRET_DETECTION_HISTORIC_SCAN, который по умолчанию равен False. Это параметр контролирует, будет ли сканироваться вся история проекта или только текущее состояние. Если оставить False, это может привести к ситуации, когда секрет будет удален из текущего коммита, но при этом останется в истории. Мы рекомендуем включать сканирование всей истории сразу (ставить значение True).

Также можно увидеть тэг Docker. Одно из ограничений DevSecOps в GitLab в том, что раннеры должны быть на docker или kubernetes.

Если у вас в системе есть разные GitLab-раннеры, с разными исполнителями, то есть риск того, что какую-то задачу, которую должен был выполнить раннер с docker executor, выполнит раннер с shell executor, и тогда все упадет. Это проблему мы решаем с помощью тегов: тем раннерам, что на docker, мы проставляем соответствующий тег. Тем самым указываем, что нам нужно, чтобы эту задачу выполнял именно данный executor. 

Ниже приведен пример того, как подключенный шаблон этой задачи (Security/Secret-Detection.gitlab-ci.yml) выглядит в исходниках. 

Как мы видим, тут есть какие-то конфигурационные переменные и пример самой задачи — secret_detection. Secret Analyzer на выходе генерирует артефакт в виде JSON-файла. Задача Secret Detection содержит правила применения и сам скрипт запуска. Как говорилось ранее, внутри Secret Detection используется Gitleaks.

Как работает Gitleaks в GitLab

Ниже представлен пример того, как выглядит работа GitLeaks внутри пайплайна. В приведенном примере — все хорошо, секреты в коде не были обнаружены. 

Как мы видим, по итогам запуска команды, генерируется файл с результатами проверки — gl-secret-detection-report.json. Приводим пример его содержимого, когда проблемы при сканировании не были обнаружены.

Теперь посмотрим на пример поведения Gitleaks, когда в процессе сканирования были обнаружены какие-то секреты.

Как мы видим, GitLeaks обнаружил проблему, но при этом job отработал успешно и какие-то активные действия не были предприняты. Посмотрим на содержимое gl-secret-detection-report.json при обнаружении ошибок.

Видим, что тут есть детальная информация об обнаруженных проблемах. 

Тот факт, что даже в случае обнаружения проблем job внутри пайплайна не прервался — ограничение бесплатных версий GitLab. Оказывается, что в бесплатной версии очень скудный функционал для работы с Secret Detection: есть сканнер, отчет, настройки — и на этом все.

Активная реакция на обнаруженные секреты

Если мы хотим прерывать пайплайны при обнаружении Gitleaks-проблем, нужно дорабатывать приведенную выше конфигурацию.

stages:
 - test

include:
 - template: Security/Secret-Detection.gitlab-ci.yml

secret_detection:
 variables:
   SECRET_DETECTION_HISTORIC_SCAN: "true"
   GET_VULNERABILITY_COUNT: "cat gl-secret-detection-report.json | jq --raw-output '.vulnerabilities | length'"
 allow_failure: false
 tags:
   - docker
 script:
   - apk add jq
   - /analyzer run
   - exit $(eval "$GET_VULNERABILITY_COUNT")

Что изменилось:

  • Добавили переменную GET_VULNERABILITY_COUNT, в которую вытягиваем количество уязвимостей из отчета gl-secret-detection-report.json.

  • Добавили параметр allow_failure. В шаблоне Security/Secret-Detection.gitlab-ci.yml у него стоит значение true, мы же его установили в явном виде в false — так мы запрещаем выполнение последующих задач в случае падения нашего job. 

  • Доработали сам скрипт задачи: установили пакет, сделали вывод количества ошибок. 

Теперь при запуске и обнаружении проблем job будет падать, ниже приведен пример.

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

#!/bin/bash

vulnerability_count=$(eval "$GET_VULNERABILITY_COUNT")
if [ ${vulnerability_count} -gt 0 ];  then
 echo "|     name     |     severity     |     file     |     commit     |"
 _jq() {
  echo ${row} | base64 --decode | jq -r ${1}
 }
 for row in $(cat gl-secret-detection-report.json | jq -r '.vulnerabilities[] | @base64'); do
   echo '|' $(_jq ".name") '|' $(_jq ".severity") '|' $(_jq ".location.file") '|' $(_jq ".location.commit.sha") '|'
 done
fi

.gitlab-ci.yml с вызовом этого скрипта будет выглядеть следующим образом:

stages:
 - test

include:
 - template: Security/Secret-Detection.gitlab-ci.yml

secret_detection:
 variables:
   SECRET_DETECTION_HISTORIC_SCAN: "true"
   GET_VULNERABILITY_COUNT: "cat gl-secret-detection-report.json | jq --raw-output '.vulnerabilities | length'"
 allow_failure: false
 tags:
   - docker
 script:
   - apk add jq
   - /analyzer run
   - bash .ci-scripts/secret-detection.sh
   - exit $(eval "$GET_VULNERABILITY_COUNT")

Как мы видим, теперь выводится информация о том, какой файл содержит секрет, в каком коммите, какая критичность и какое название у уязвимости.

Удаляем секреты из Git History

Даже если мы отловили секрет на этапе CI, он все равно остается в истории Git. Чтобы его не было в истории, нужно удалить коммиты и сделать force push. 

Но даже если мы удалим коммит из истории — при последующих запусках CI у нас могут быть ложные срабатывания из-за особенностей работы окружения в котором запускаются раннеры GitLab.

Исправить это нам помогут следующие параметры:

  • GIT_STRATEGY. Параметр отвечает за то, как мы достаем код для выполнения job. 

    • По умолчанию тут стоит fetch: Job в этом случае повторно использует локальную копию, то есть просто получает изменения. Данный способ работает быстрее, но в этом случае в локальной истории копятся те коммиты, которые ранее были удалены. 

    • Нам же нужно поменять GIT_STRATEGY на clone. В этом случае каждый раз при запуске job будет клонировать весь репозиторий и запускать проверку заново.

  • SECURE_LOG_LEVEL. Параметр отвечает за управление выводом. Обычно он установлен в info, но лучше сделать debug, чтобы видеть, какие вообще команды выполняются и понимать итоги работы того же Gitleaks.

Теперь наш .gitlab-ci.yml будет выглядеть следующим образом:

stages:
 - test

include:
 - template: Security/Secret-Detection.gitlab-ci.yml

secret_detection:
 variables:
   GIT_STRATEGY: "clone"
   SECURE_LOG_LEVEL: "debug"
   SECRET_DETECTION_HISTORIC_SCAN: "true"
   GET_VULNERABILITY_COUNT: "cat gl-secret-detection-report.json | jq --raw-output '.vulnerabilities | length'"
 allow_failure: false
 tags:
   - docker
 script:
   - apk add jq
   - /analyzer run
   - bash .ci-scripts/secret-detection.sh
   - exit $(eval "$GET_VULNERABILITY_COUNT")

Ниже мы видим итоговый вывод в debug-режиме, когда уже удалили секрет через force push.

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

Git server hooks

Некоторые читатели могут посмотреть на все это и задать логичный вопрос: зачем вообще такие сложности для борьбы с коммитами, которые уже попали в репозиторий, ведь можно использовать pre-receive git hook на стороне GitLab, чтобы отклонять коммиты, которые содержат секреты?

Все так. Но есть некоторые сложности, которые нужно учитывать:

  1. Процесс настройки требует доступа к файловой системе сервера GitLab. Это значит, например, что это не будет работать на облачной версии GitLab.

  2. Даже если пункт 1 не проблема, то их все равно сложно переиспользовать. Git server hooks — это не часть CI/CD-гитлаба, поэтому его нельзя переместить в код репозитория, а придется каждый раз прибегать к помощи администратора.

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

Если вы все же хотите уменьшить количество срабатываний задач в CI/CD (уменьшить количество выпиливаний коммитов из истории) и при этом не изобретать велосипед — можно использовать готовое решение, описанное в статье (мы проверили — работает отлично). Скрипт block_confidentials.sh не повторяет всей функциональности Gitleaks, но все-таки позволяет заблокировать некоторые коммиты с различными ключами. При этом внедрить это решение довольно просто.

Пример того, как при попытке запушить код с секретными данными, коммит отклоняется со стороны серверного хука:

Итоги

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

В следующей статье разберем Commit-time Checks, поговорим о SAST и Dependency Scanning и подробно остановимся на инструментах этого этапа.

Комментарии (2)


  1. jenki
    00.00.0000 00:00
    +1

    Спасибо вам за содержательную и полезную статью


    1. OlegSpectr Автор
      00.00.0000 00:00
      +1

      Спасибо за то, что поделились мнением)