Храним секреты приложения в Vault

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

Уже чуть позже, вгугливая, какие вообще есть инструменты для управления секретами, я наткнулся на HashiCorp Vault, и немного прифигел от того, как много вещей, оказывается, надо организовать и знать, чтобы эту задачу решить. Будучи всё ещё под впечатлением, сегодня я решил пройтись по основам Vault и, если получится, показать, насколько интересные решения умные люди нашли на этом поприще.

Что такое Vault

Vault — это утилита командной строки в комплекте с RESTful сервисом, которая отвечает за управление секретами — логинами, паролями, ключами, сертификатами и прочими таинствами. «Управление» включает в себя как хранение, так и выдачу секретов конкретным приложениям с пометкой у себя в журнале, кому и когда это произошло.

В довесок ко всему vault можно интегрировать со сторонними поставщиками секретов (вроде AWS, PKI или базами данных) и, например, генерировать временные логины и пароли на лету. Ну и конечно все права доступа к хранилищу секретов настраиваются, всё шифруется, и любой запрос логгируются. Всё по-взрослому.

Установка

Практически всё, что делает HashiCorp, можно скачать в виде архива и просто запустить. Вот бы все так делали. Свой vault я скачал на Mac, так что все примеры будут с лёгким ароматом линуксовой командной строки. Тем, кто любит виндовые ароматы, расстраиваться на стоит, потому что vault поддерживает и их. Морально и платформенно.

«Hello world!»

Для начала создадим что-нибудь простое, рабочее и бесполезное. Чтобы просто посмотреть, как vault работает. Так как в него входит и утилита, и RESTful сервер, то последний нужно как-то запустить. Для игр и экспериментов это можно сделать вот такой командой:

В dev режиме сервер работает по HTTP, что обязательно приведёт в ступор vault клиента. Но его можно успокоить, разъяснив через переменную среды ( VAULT_ADDR ), что HTTPS на сегодня отменяется. Что самое приятное, vault server –dev даже вывел команду, которая может это сделать. Так что CTRL+C, CTRL+V — и всё работает:

Теперь мы можем посохранять какие-нибудь секретные данные. Например, представим, что у нас есть staging база данных, логин и пароль к которой (“sa”, “1”) нельзя показывать посторонним. Сохранить эти параметры можно под ключом secret/db-staging (как путь в файловой системе):

Вообще просто. Прочитать их назад ещё проще:

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

Для авторизации vault использует секретные токены и у нас даже есть один такой — для root. Его выводила всё та же команда запуска сервера (vault server –dev output). Им и воспользуемся:

В жизни примерно так всё и будет происходить. Кто-то, например администратор, разложит в хранилище секреты и раздаст приложениям токены, а они уже будут запрашивать данные по HTTP(S).

Правда, то, что мы сейчас сотворили, не сильно впечатляет. Всё-таки простейшее Consul хранилище с настроенными правами доступа сделало бы абсолютно то же самое. Чтобы как следует впечатлиться, нужно копнуть немного глубже.

Более реалистичный пример

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

Запускаем сервер

Как говорил дружище Боромир,

Нельзя просто так

Для начала нужно сводить его на ужин скормить ему конфигурацию, а в минимальном файле конфигурации хотя бы указать, где физически хранить секреты, и по какому IP и порту из выпрашивать. Хотя перманентных хранилищ для Vault хоть пруд-пруди (Consul, S3, PostgreSQL, Azure и остальные), мой vault будет хранить данные прямо в файловой системе. IP и порт же я сделаю как и в —dev режиме.

Кстати, HashiCorp изобрела свой собственный язык для конфигурации, который вы только что увидели, и назвала его HCL (HashiCorp Configuration Language). HCL немного похож на плод неудавшейся любви JSON и YAML, но с юридической точки зрения является надмножеством JSON. В мире действительно катастрофически не хватает языков программирования и форматов, так что я рад, что HashiCorp включилась в его спасение. Следующим шагом, я надеюсь, они создадут свой текстовый редактор и какой-нибудь JavaScript-овый фреймуорк. Клон Ангуляра, например.

Но я отвлёкся. Запустим-ка сервер и двинемся дальше:

Распечатываем хранилище

Идея с распечатыванием просто прекрасна. По-умолчанию vault всегда запечатан (sealed). Как сейф в банке. Все секреты лежат в зашифрованном хранилище (у нас — с файле), и даже vault не знает, что с ними делать. Но есть ещё одно состояние — распечатанное (unsealed). В нём vault получает ключ для расшифровки, загружает секреты себе в память, и готовится общаться со шпионами. То, как происходит процесс распечатывания, впечатляет меня до сих пор.

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

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

Теперь, имея на руках три ключа, мне нужно вызвать vault unseal хотя бы два раза (с разными ключами, есс-на):

В результате vault распакован, и его можно порасспрашивать на предмет гостайны.

Аутентификация

Разумеется, без паспорта секреты нам никто не покажет. Роль идентификатора в vault играют токены, и как минимум один у нас уже есть — для root (сгенерирован во время vault init). Попробуем представиться.

vault нас узнал, так что можно добавить ему важных секретов (потом пригодятся).

Создаём полиси и новый токен

Использовать root токен в vault — точно такой же плохой тон, как и логиниться под root в Убунту. Но вот какая проблема: если создать не-root токен будучи рутом (других-то ведь нет ещё), мы сделаем дочерний токен, который унаследует все права батюшки и.. тоже станет рутом. Генетика-с. Чтобы этого избежать, потомству вместе с генами нужно передать полиси с ограниченными правами.

Полиси — это ещё один HCL файл, который просто указывает, из каких ключей можно читать, в какие ключи можно записывать, а от каких стоит держаться подальше. Если бы я хотел дать чайлдовому токену право на чтение из secret/db-staging и запретить трогать всё остальное, то сотворил бы что-нибудь вроде такого:

Сохраняем шедевр в файл dev-policy.hcl, скармливаем в vault через vault policy-write development dev-policy.hcl, и можно приступать к созданию нового, ограниченного во всех смыслах токена:

Токен готов, и проверить, что он ограничен в правах, проще простого:

Используем кастомный провайдер секретов (secret backend)

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

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

Раз уж мы начали с примера про базы данных и их пароли, то можно подключить встроенный бэкэнд секретов под названием database и начать генерировать логины-пароли к, например, MySQL базе на лету. Чтобы совсем весело было, пусть сгенерированные аккаунты будут иметь право только на чтение. Обзовём их ролью viewer, и будет нам счастье.

Но прежде, чем счастье произойдёт, надо переключиться обратно на root токен.

Теперь примонтируем database:

А вот теперь нам нужен какой-нибудь MySQL для экспериментов. Так как у меня на машине есть Docker,  то заполучить рабочий MySQL сервер можно за считанные секунды:

Эта команда так же откроет порт 3306 и задаст рутовый MySQL пароль —  rootpwd. И то, и другое нам потребуется для дружбы с vault.

Теперь можно настроить database провайдера и рассказать ему, как создавать viewer аккаунты:

По сути обращение к vault за логином и паролем сводится к CREATE USER запросу, в котором явно прописано, что кроме “SELECT” viewer роль уметь ничего не должна. Можно было бы ещё указать, через сколько времени этот аккаунт самоуничтожится, но vault и так поставит какой-нибудь ограничитель.

Ну что, попробуем запросить пароль к базе:

А теперь протестируем этот логин и пароль на самом MySQL:

Всё работает! Ну не здорово ли? Теперь каждое приложение, каждый процесс будет получать свой собственный логин и пароль, которые нет смысла воровать (они же временные), и которые всегда можно отследить из-за вездесущего логгинга и мониторинга.

Практически заключение

К vault можно подключать и других провайдеров: для генерации секретоваутентификации (например, AWS или github) и даже для аудита.

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

5 комментариев для “Храним секреты приложения в Vault

  1. Спасибо.
    Про HCL — прикольно, но на самом деле он хорош, когда готовишь в соусе Terraform 0.12, потом окунаешь в yaml- или jsonencode() и подаешь с Ansible на блюдечке 🙂

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

    1. Тут скорее решается проблема, как этот секрет передать приложению наиболее безопасным способом. Не избавиться от секрета, а только передать. Хотя если избавиться — то было бы вообще шикарно (например, перевести сервисные аккаунты из логин/паролей на managed identities). Но я отвлёкся.

      Например, есть у нас контейнер где-нибудь в кластере (или лямбда функция, или ещё чего — почти всё в наши дни превратилось в контейнер), и нам нужно в него передать connection string для какого-нибудь mongodb.
      Какие у нас опции есть для этого? Можно закоммитить прямо в исходный код приложения, но это определённо смертный грех. Можно положить в environment variables, и это лучше, но всё равно не очень, потому что переменные окружения легко подсмотреть, их видят все приложения (был какой-то exploit в nodejs пакете когда-то, который тупо слал все переменные окружения нужным людям) и никаких чётких логов от этого не останется.
      А можно вместо connection string в env-vars (или k8s configmap, или как угодно) положить токен, и это немного меняет правила игры.
      — Во-первых, токен скорее всего автосгенерирован и автоматически добавлен к контейнеру, так что девелопер этот токен вряд ли добровольно увидит.
      — Во-вторых, даже если я подсмотрю токен, кто сказал, что у меня будет сетевой доступ к vault? Скорее всего vault будет сидеть где-то внутри кластера, с внутренними айпишками, и какой-нибудь сердобольный админ через istio или другой service mesh включил ему mTLS, и мне, человеку, сделать запрос в vault будет совсем тяжело. Не невозможно — хакнуть можно всё, что угодно. Но тяжело. Потому что обычных людей внутрь привилегированных сетей или кластеров так просто не пускают. А если и пустят, то попытка стырить и обменять токен на секрет оставит море следов со всеми вытекающими. А с современным разделением ролей быть и админом кластера, и админом аудит-логов, да ещё и чёрным хакером в душе — это очень маловероятное стечение обстоятельств.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *