Чем дольше работаю, тем больше вижу разнообразных практик по работе с секретами. Некоторые рабочие, другие — нет, а от части просто берёт оторопь. В статье я разберу варианты и расскажу о плюсах и минусах разных подходов.

Поговорим мы о секретах в общем, но с практической точки зрения сфокусируемся именно на работе с секретами на машинах разработчиков: как их туда безопасно доставить и употребить.

Этот текст — продолжение серии CI/CD в каждый дом, в прошлый раз мы обсуждали, как организовать сборочный цех базовых docker-образов.

Б — Безопасность

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

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

Окружения

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

Продовые. Секреты, которыми пользуется продакшн окружения, например креды баз, токены АПИ, приватные ключи. Это самая важная группа секретов. В идеальном мире они не просто крайне защищены, а вообще не выходят за пределы безопасного контура (создание, хранение, доставка и потребление происходит внутри окружения, в которое нет доступа извне). Если такая возможность есть, доступа не должно быть ни у кого, ни на запись, ни даже на чтение.

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

Тестовые. То же самое, что и операционные, только теперь это стейджинг / тестовое окружение. Важно понимать, что они ни в коем случае не должны пересекаться с операционными. Даже если вы используете внешние продакшн-среды для тестовых окружений, правильно, чтобы это были разные приложения, которые имеют разные ключи. Но даже если такой возможности нет, постарайтесь хотя бы иметь разные ключи, которые можно контролировать и инвалидировать отдельно. Если эти правила соблюдены, то уже не так страшно давать доступ на чтение разработчикам — это позволит им отлаживаться и при необходимости лезть руками в кишки тестовых сред. В иных случаях секреты также не должны покидать безопасный контур.

CI/CD. Секреты, которыми мы пользуемся исключительно в рамках наших CI/CD-процессов, могут иметь пересечение с тестовыми для пайплайнов тестирования. Они, так же как и продовые, не должны покидать пределы безопасного контура и/или светиться на машине разработчика. Опять же, в идеальном мире ни у кого нет к ним даже доступа, кроме CI/CD. Утечка этих секретов порой может быть даже более опасна, чем продовых, поскольку они дают доступ не к приложению, а к управлению инфраструктурой.

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

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

Долголетие — наш враг

Важный критерий для секрета — время жизни:

Вечные. Секрет, время жизни которого ничем не ограничено. Настолько же часто встречается, насколько является небезопасным. Являет собой серьёзную угрозу безопасности, особенно если может быть хоть как-то скомпрометирован, поскольку даёт неограниченный по времени доступ.

Долгоживущие. Любые секреты, которые ограничены по времени, будь то месяц или год. Ротация таких секретов не обязательно должна быть автоматизирована, бывает, что это просто-напросто невозможно. Это не должно вас останавливать. Время жизни секрета не только добавляет некоторую безопасность в случае утечки секрета, ограничивая доступ по времени, но и заставляет проводить регулярную ревизию актуальности секретов.

Короткоживущие/сессионные. Это секреты, которые живут минуты, может часы, а иногда время их жизни ограничено некоторой сессией. Это самый безопасный способ предоставления секретов. Когда на стороне употребления есть какой-то долгоживущий ключ, для которого определены доступы, и секреты на них выписывают по необходимости (IAM). А после того как потребитель закончит с ними работу,  инвалидируются в случае сессионных.

Идеальный вариант для секретов на машине разработчика. Преимущество в том, что при утечке / подозрительной активности мы инвалидируем долгоживущий ключ, и потенциальный злоумышленник больше не имеет доступа к нашим секретам. А те, что он успел себе выписать раньше, больше не работают. Это намного практичнее, чем перевыпускать все секреты в случае утечки. Отлично подходит для локальных/персональных секретов, где высок риск компрометации. Плюс в этом случае долгоживущий ключ можно заменить на какой-нибудь TOTP, что практически исключает возможность украсть долгоживущий секрет, а лишь оставляет небольшое окно для кражи короткоживущих.

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

Хранение

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

Этот блок в целом находится вне рамок сегодняшнего обсуждения, но для понимания будем рассматривать примеры с использованием опенсорсного Hashicorp Vault и проприетарного Yandex Cloud Lockbox. Я в своих проектах пользуюсь в основном ими, но хватает и других, например из популярных — AWS Secrets Manager.

Хранение секретов в переменных среды, локальных файлах, не дай бог, в открытом виде в репозитории или вообще на стикере вашего монитора рассматривать не будем. Не хранение это, да и не секреты вовсе :)

Распространение

Передача секретов — одно из уязвимых мест в цепочках работы с секретами, благо набор правил достаточно прост. На самом деле оно единственное: секрет должен передаваться так, чтоб у потенциальных злоумышленников не было возможности его перехватить. Банально? Согласен. Следуют ли этому принципу? К сожалению, зачастую — нет.

Какие из популярных методов переноса секретов, которые можно часто встретить на практике, являются небезопасными? Давайте начнём с очевидных:

  • В мессенджерах / по почте и прочее. О, это моё любимое, если позволите, даже говорить не буду, тут и так всё ясно.

  • Буфер обмена. Вы заходите на сайт своего vault’а, открываете секрет визуально и перепечатываете или копируете его в буфер. В этот момент секрет может быть запросто украден или скомпрометирован. Начиная от банального вируса/keylogger’а, заканчивая тем, что вы можете этот секрет вставить куда-то не туда.

  • Передача по нешифрованному каналу. Скорее всего, на уже готовых хранилищах вы с этим не столкнётесь, но если вдруг решите сделать собственное хранилище с API, помните: любая передача должна осуществляться посредством https с обязательной валидацией истинности сервера ещё до передачи клиентского ключа, например посредством SSL-сертификата или подписью приватным ключом.

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

Употребление

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

Хранение в файлах. Это условно допустимая схема, но есть целый ряд «но»:

  • Они нарушают правило, описанное выше, о сессионном характере хранения на стороне потребления, поскольку можно запросто залениться / забыть почистить за собой.

  • Часто можно встретить файлы с неправильно настроенным доступом, вследствие чего все пользователи получают доступ на чтение.

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

Хранение в .bashrc, .zshrc и иже с ними. За гранью добра и зла. Доступ есть у любого приложения, запущенного из любого терминала. Просто нет. Не надо так.

Хранение в локальных хранилищах секретов. Примером какой-нибудь keyctl. Вполне валидная схема, предоставляющая некоторую безопасность, но:

  • С таким же успехом можно хранить секреты и во внешнем хранилище, из плюсов разве что: можно работать без доступа к интернету.

  • Требует дополнительной настройки на стороне пользователя.

  • Всё ещё нарушает сессионность.

Какие же есть тогда сессионные варианты? Их на самом деле много, все они сводятся к двум следующим:

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

Храним и подтягиваем секреты непосредственно в приложение. Ещё более безопасный метод, поскольку доступ к секретам будет не у всех дочерних процессов нашего терминала, а только у самого приложения. Из минусов: требует доработки непосредственно на стороне потребляющего приложения для взаимодействия с секретами. При использовании внешних приложений по сути невозможно.

Употребление на стороне продакшн/тестовых приложений и CI/CD-пайплайнов — это отдельная история. Как правило, всё гораздо проще, поскольку контур является защищённым. И компрометация секретов возможна только в случае доступа непосредственно к месту использования с рутовыми правами. К тому же чаще всего эта задача уже решена за вас, как, например, в случае k8s или GitHub Actions. Главное — никогда не храните секреты в образах, секреты должны всегда передаваться в контейнер исключительно на запуске уже внутри инфраструктуры.

П — Практика

Обычно я организовываю локальную работу с секретами по пути хранения в переменных окружениях терминала. На мой взгляд, это идеальный баланс между безопасностью и удобством. То есть мне не нужно каждый раз, пока я работаю над проектом, загружать секреты из источника, но и по закрытии терминала секретов не останется. Чтобы это организовать, понадобится немного знать bash, но самую малость, потому давайте разберём ключевые нюансы.

Ключевое слово export. Эта команда позволяет передавать переменную в дочерние процессы без явного указания. Это значит, что если мы заэкспортим какие-то переменные среды в терминале, то они будут доступны для использования во всех дочерних процессах.

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

Ключевое слово unset. Удаляет переменную из окружения. Будучи применённой с флагом -f, удаляет объявление функции, зачем нам это — будет ясно из примера.

Вызов с субституцией $(command). Просто вызов с заменой этого блока на результат выполнения.

Переменные окружения могут и будут разные в зависимости от вашей ситуации, но по сути все они будут передаваться внутрь терминала командой вида export KEY=" class="formula inline">(value-retrieving-command).

Приведу пару примеров для наглядности:

YC_LOCKBOX_SECRET_VALUE — секрет, хранимый в локбоксе, пример получения:

yc lockbox payload get \
          --profile example-profile \
          --folder-name example-folder \
          --name example-secret \
          --key example-key

VAULT_SECRET_VALUE - секрет из Hashicorp Vault:

vault kv get \
          -address=https://vault.example.org \
          -mount=example-mount \
          -field=example-key \
          example-secret

Как же в примитиве выглядит наш скрипт?

export YC_LOCKBOX_SECRET_VALUE=$(yc lockbox payload get --profile example-profile --folder-name example-folder --name example-secret --key example-key)
export VAULT_SECRET_VALUE=$(vault kv get -address=https://vault.example.org -mount=example-mount -field=example-key example-secret)

Как выглядит работа:
Открыли терминал, выполнили source environment_activate. После этого секреты доступны во всех приложениях, которые запускаются из данного терминала. поработали, запустили тесты, что угодно, закрыли терминал. На этом базовая часть по сути заканчивается, дальше начинаются красивости.

Красивости

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

Консольный префикс. Удобно всегда знать, загружены ли переменные окружения проекта в терминал. Можно, конечно, проверять наличие значения или мотать историю, но для этого существует замечательный механизм префикса командной строки, который позволяет управлять значением того, что вы видите левее набираемой команды. Механизм, а точнее механика работа с цветами слегка отличаются между bash и zsh, как и в других shell’ах, но обычно я делаю именно для этих двух.

Запрет на добавление нескольких окружений. Лучше не грузить по несколько разных окружений в один терминал, сложнее будет уследить, что загружено, а что нет, да и зачем.

Ну и после того как мы всё это систематизируем по разным функциям и приукрасим консольным выводом, получится следующее:

#!/usr/bin/env bash

_EA_ENVIRONMENT_NAME=example

case $(basename "$SHELL") in
  "zsh")
    # shellcheck disable=SC2154,SC1087
    _EA_COLOR_GREEN="%{$fg[green]%}"
    # shellcheck disable=SC2154
    _EA_COLOR_NC="%{$reset_color%}"
  ;;
  *)
    _EA_COLOR_GREEN="\[\e[32m\]"
    _EA_COLOR_NC="\[\e[0m\]"
  ;;
esac

_ea_unset_script_variables() {
  unset _EA_ENVIRONMENT_NAME
  unset _EA_COLOR_GREEN
  unset _EA_COLOR_NC
}

_ea_export_local_variables() {
  echo "Setting up local env variables..."
  export YC_LOCKBOX_SECRET_VALUE=$(yc lockbox payload get --profile example-profile --folder-name example-folder --name example-secret --key example-key)
  export VAULT_SECRET_VALUE=$(vault kv get -address=https://vault.example.org -mount=example-mount -field=example-key example-secret)
}

_ea_unset_set_local_variables() {
  echo "Cleaning up local envs..."
  unset YC_LOCKBOX_SECRET_VALUE
  unset VAULT_SECRET_VALUE
}

_ea_set_console_prefix() {
  echo "Setting up console color and prefix..."
  _EA_PREVIOUS_PS1="${PS1}"
  PS1="${_EA_COLOR_GREEN}(${_EA_ENVIRONMENT_NAME})${_EA_COLOR_NC}${PS1}"
}

_ea_unset_console_prefix() {
  echo "Cleaning up console color and prefix..."
  PS1="${_EA_PREVIOUS_PS1}"
  unset _EA_PREVIOUS_PS1
}

_ea_set_active_environment() {
  export _EA_ACTIVE_ENVIRONMENT=$_EA_ENVIRONMENT_NAME
  echo ""
  echo "Environment $_EA_ENVIRONMENT_NAME is activated."
  echo "To deactivate: run 'environment_deactivate'."
}

_ea_unset_active_environment() {
  echo ""
  echo "Environment $_EA_ENVIRONMENT_NAME is deactivated."
  unset _EA_ACTIVE_ENVIRONMENT
}

_environment_activate() {
  if [ -n "$_EA_ACTIVE_ENVIRONMENT" ]; then
      echo "Active env is already set to $_EA_ACTIVE_ENVIRONMENT"
      echo "To deactivate, run 'environment_deactivate'"
      return
  fi

  _ea_export_local_variables
  _ea_set_console_prefix
  _ea_set_active_environment

  unset -f _ea_export_local_variables
  unset -f _ea_set_console_prefix
  unset -f _ea_set_active_environment
  unset -f _environment_activate
}

environment_deactivate() {
  if [ -z "$_EA_ACTIVE_ENVIRONMENT" ]; then
      echo "No active environment to deactivate."
      return
  fi

  _ea_unset_console_prefix
  _ea_unset_set_local_variables
  _ea_unset_script_variables
  _ea_unset_active_environment

  unset -f _ea_unset_console_prefix
  unset -f _ea_unset_set_local_variables
  unset -f _ea_unset_script_variables
  unset -f _ea_unset_active_environment
  unset -f environment_deactivate
}

_environment_activate

Точно не реклама

Совершенно не призыв к действию, но будет глупо не упомянуть, что для своих проектов я использую самописную библиотеку для передачи секретов secret-transfer. С ней описание секретов на большом объёме проектов становится несколько проще, а скрипт environment_activate перестаёт зависеть от конкретного проекта.

…
_ea_export_local_variables() {
  echo "Setting up local env variables..."
  $($_EA_SCRIPTS_FOLDER/.venv/bin/secret-transfer run -f "$_EA_SCRIPTS_FOLDER/secrets/local.yaml")
}
…

TL;DR

На dev-тачках должны светиться только персональные/локальные секреты. Короткоживущие секреты лучше вечных. Храним безопасно. Передаём шифрованно. Употребляем сессионно.

PS

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

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


  1. savostin
    30.03.2024 09:02

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

    Имхо с правами пользователя, от которого запускается приложение. Как пример, приложение запускается от пользователя nginx и файл внезапно становится доступным из браузера.