Недавно я тут общался с одним из наших экспертов по безопасности и пытался выбить у него наш супер-секретный сертификат для одного из своих приложений. Но выбить не просто так, а чтобы приложение могло достучаться до сертификата, а я — нет. Вдруг я на тёмную сторону силы подамся. Нетривиальная задача, скажу я вам.
Уже чуть позже, вгугливая, какие вообще есть инструменты для управления секретами, я наткнулся на HashiCorp Vault, и немного прифигел от того, как много вещей, оказывается, надо организовать и знать, чтобы эту задачу решить. Будучи всё ещё под впечатлением, сегодня я решил пройтись по основам Vault и, если получится, показать, насколько интересные решения умные люди нашли на этом поприще.
Что такое Vault
Vault — это утилита командной строки в комплекте с RESTful сервисом, которая отвечает за управление секретами — логинами, паролями, ключами, сертификатами и прочими таинствами. «Управление» включает в себя как хранение, так и выдачу секретов конкретным приложениям с пометкой у себя в журнале, кому и когда это произошло.
В довесок ко всему vault можно интегрировать со сторонними поставщиками секретов (вроде AWS, PKI или базами данных) и, например, генерировать временные логины и пароли на лету. Ну и конечно все права доступа к хранилищу секретов настраиваются, всё шифруется, и любой запрос логгируются. Всё по-взрослому.
Установка
Практически всё, что делает HashiCorp, можно скачать в виде архива и просто запустить. Вот бы все так делали. Свой vault я скачал на Mac, так что все примеры будут с лёгким ароматом линуксовой командной строки. Тем, кто любит виндовые ароматы, расстраиваться на стоит, потому что vault поддерживает и их. Морально и платформенно.
«Hello world!»
Для начала создадим что-нибудь простое, рабочее и бесполезное. Чтобы просто посмотреть, как vault работает. Так как в него входит и утилита, и RESTful сервер, то последний нужно как-то запустить. Для игр и экспериментов это можно сделать вот такой командой:
1 2 3 4 5 6 7 8 9 10 11 12 |
vault server -dev #==> Vault server configuration: #... #The only step you need to take is to set the following environment variables: # # export VAULT_ADDR='http://127.0.0.1:8200' #... #Unseal Key: JMe7A0cECXKLnSP8pQmKIrku3if1NWzgrTiLSeRQcPA= #Root Token: 18924580-71bd-f5e0-a218-50bfada86741 # #==> Vault server started! Log data will stream in below: #... |
В dev режиме сервер работает по HTTP, что обязательно приведёт в ступор vault
клиента. Но его можно успокоить, разъяснив через переменную среды (
VAULT_ADDR ), что HTTPS на сегодня отменяется. Что самое приятное, vault server –dev
даже вывел команду, которая может это сделать. Так что CTRL+C, CTRL+V — и всё работает:
1 2 |
export VAULT_ADDR='http://127.0.0.1:8200' # writes nothing |
Теперь мы можем посохранять какие-нибудь секретные данные. Например, представим, что у нас есть staging база данных, логин и пароль к которой (“sa”, “1”) нельзя показывать посторонним. Сохранить эти параметры можно под ключом secret/db-staging
(как путь в файловой системе):
1 |
vault write secret/db-staging name=sa password=1 |
Вообще просто. Прочитать их назад ещё проще:
1 2 3 4 5 |
vault read secret/db-staging #Key Value #... #name sa #password 1 |
В реальном юз-кейсе, конечно, пришлось бы сначала залогиниться, но сервер, запущенный в режиме -dev
, работает без этих условностей. Но если бы мы попробовали прочитать секрет через HTTP, как, например, действовало бы настоящее приложение, то представиться всё же пришлось бы:
1 2 |
curl http://127.0.0.1:8200/v1/secret/db-staging # {"errors":["missing client token"]} |
Для авторизации vault использует секретные токены и у нас даже есть один такой — для root. Его выводила всё та же команда запуска сервера (vault server –dev
output). Им и воспользуемся:
1 2 3 4 5 6 7 8 9 |
curl -s \ --header "X-Vault-Token: 18924580-71bd-f5e0-a218-50bfada86741" \ http://127.0.0.1:8200/v1/secret/db-staging \ | jq ‘.data’ #{ # "name": "sa", # "password": "1" #} |
В жизни примерно так всё и будет происходить. Кто-то, например администратор, разложит в хранилище секреты и раздаст приложениям токены, а они уже будут запрашивать данные по HTTP(S).
Правда, то, что мы сейчас сотворили, не сильно впечатляет. Всё-таки простейшее Consul хранилище с настроенными правами доступа сделало бы абсолютно то же самое. Чтобы как следует впечатлиться, нужно копнуть немного глубже.
Более реалистичный пример
Попробуем сделать тот же самый пример, но уже по-взрослому. Ну или хотя бы как подростки.
Запускаем сервер
Как говорил дружище Боромир,
Для начала нужно сводить его на ужин скормить ему конфигурацию, а в минимальном файле конфигурации хотя бы указать, где физически хранить секреты, и по какому IP и порту из выпрашивать. Хотя перманентных хранилищ для Vault хоть пруд-пруди (Consul, S3, PostgreSQL, Azure и остальные), мой vault будет хранить данные прямо в файловой системе. IP и порт же я сделаю как и в —dev
режиме.
1 2 3 4 5 6 7 8 |
storage "file" { path = "./secrets" } listener "tcp" { address = "127.0.0.1:8200" tls_disable = 1 } |
Кстати, HashiCorp изобрела свой собственный язык для конфигурации, который вы только что увидели, и назвала его HCL (HashiCorp Configuration Language). HCL немного похож на плод неудавшейся любви JSON и YAML, но с юридической точки зрения является надмножеством JSON. В мире действительно катастрофически не хватает языков программирования и форматов, так что я рад, что HashiCorp включилась в его спасение. Следующим шагом, я надеюсь, они создадут свой текстовый редактор и какой-нибудь JavaScript-овый фреймуорк. Клон Ангуляра, например.
Но я отвлёкся. Запустим-ка сервер и двинемся дальше:
1 |
vault server -config config.hcl |
Распечатываем хранилище
Идея с распечатыванием просто прекрасна. По-умолчанию vault всегда запечатан (sealed). Как сейф в банке. Все секреты лежат в зашифрованном хранилище (у нас — с файле), и даже vault не знает, что с ними делать. Но есть ещё одно состояние — распечатанное (unsealed). В нём vault получает ключ для расшифровки, загружает секреты себе в память, и готовится общаться со шпионами. То, как происходит процесс распечатывания, впечатляет меня до сих пор.
Когда мы впервые инициализируем хранилище, оно создаёт мастер-ключ для расшифровки и дробит его на несколько частей. Сам ключ уничтожается. Чтобы его восстановить, нужно собрать хотя бы несколько частей ключа назад. Если эти части хранятся у разных людей, то нам придётся подкупить сразу кучу народа, чтобы это храниличе втихаря распечатать. Запечатать же хранилище назад можно и в одиночку.
Вот, как это выглядит. Сейчас я инициализирую хранилище, разобью мастер-ключ на 3 части, и как минимум две из них потребуются назад, чтобы восстановить ключ и хранилище распечатать:
1 2 3 4 5 6 7 |
vault init -key-shares 3 -key-threshold 2 #... #Unseal Key 1: tQatbtCzOeX5f2mL4Mbd30drim5/CdIOODdBlZ0TxLQ/ #Unseal Key 2: cHCFu9rvz6vwk9jH6dtg+7Ct0EvjN02FRFCPI/dFVFlO #Unseal Key 3: r3jt+sDaW1UYg1psAQcHdoOAGmEEPlikVmHt+YRqVu4e #Initial Root Token: 5565017e-4b50-bb5f-073e-3def713ba4f1 #... |
Теперь, имея на руках три ключа, мне нужно вызвать vault unseal
хотя бы два раза (с разными ключами, есс-на):
1 2 3 4 5 6 7 8 9 10 |
vault unseal #Key (will be hidden): #Sealed: true #Key Shares: 3 #Key Threshold: 2 #Unseal Progress: 1 #Unseal Nonce: 4fd58cbe-222a-2b05-57f3-5b3736676540 vault unseal #... |
В результате vault распакован, и его можно порасспрашивать на предмет гостайны.
Аутентификация
Разумеется, без паспорта секреты нам никто не покажет. Роль идентификатора в vault играют токены, и как минимум один у нас уже есть — для root (сгенерирован во время vault init
). Попробуем представиться.
1 2 |
vault auth 18924580-71bd-f5e0-a218-50bfada86741 #Successfully authenticated! You are now logged in. |
vault нас узнал, так что можно добавить ему важных секретов (потом пригодятся).
1 2 |
vault write secret/db-production login="1" password="1" vault write secret/db-staging login="dev" password="pass" |
Создаём полиси и новый токен
Использовать root токен в vault — точно такой же плохой тон, как и логиниться под root в Убунту. Но вот какая проблема: если создать не-root токен будучи рутом (других-то ведь нет ещё), мы сделаем дочерний токен, который унаследует все права батюшки и.. тоже станет рутом. Генетика-с. Чтобы этого избежать, потомству вместе с генами нужно передать полиси с ограниченными правами.
Полиси — это ещё один HCL файл, который просто указывает, из каких ключей можно читать, в какие ключи можно записывать, а от каких стоит держаться подальше. Если бы я хотел дать чайлдовому токену право на чтение из secret/db-staging
и запретить трогать всё остальное, то сотворил бы что-нибудь вроде такого:
1 2 3 |
path "secret/db-staging" { capabilities = ["read"] } |
Сохраняем шедевр в файл dev-policy.hcl
, скармливаем в vault через vault policy-write development dev-policy.hcl
, и можно приступать к созданию нового, ограниченного во всех смыслах токена:
1 2 3 4 5 6 |
vault token-create -policy development #Key Value #--- ----- #token 77ab8047-34e2-34ac-1245-ce98c00d9c12 #... #token_policies [default development] |
Токен готов, и проверить, что он ограничен в правах, проще простого:
1 2 3 4 5 6 7 8 9 10 11 12 |
vault auth 77ab8047-34e2-34ac-1245-ce98c00d9c12 vault read secret/db-staging #Key Value #... #login dev #password pass vault read secret/db-production #Error reading secret/db-production: Error making API request. #... #* permission denied |
Используем кастомный провайдер секретов (secret backend)
До настоящего момента мы использовали дефолтного провайдера секретов типа ключ-значение. Vault монтировал его как secrets
, поэтому нам и приходилось начинать все секретные пути с secrets/
. Но есть и другие провайдеры.
Представьте, как было бы здорово, если бы вместо статических логина и пароля, которые кто-то ввёл в хранилище руками и теперь должен отслеживать и обновлять, мы бы генерировали временные для каждого приложения. Представили? Вот кастомный провайдер такое может устроить.
Раз уж мы начали с примера про базы данных и их пароли, то можно подключить встроенный бэкэнд секретов под названием database и начать генерировать логины-пароли к, например, MySQL базе на лету. Чтобы совсем весело было, пусть сгенерированные аккаунты будут иметь право только на чтение. Обзовём их ролью viewer, и будет нам счастье.
Но прежде, чем счастье произойдёт, надо переключиться обратно на root токен.
1 |
vault auth 18924580-71bd-f5e0-a218-50bfada86741 |
Теперь примонтируем database
:
1 2 |
vault mount database # Successfully mounted 'database' at 'database'! |
А вот теперь нам нужен какой-нибудь MySQL для экспериментов. Так как у меня на машине есть Docker, то заполучить рабочий MySQL сервер можно за считанные секунды:
1 2 3 4 |
docker run --rm -d \ -p3306:3306 \ -e MYSQL_ROOT_PASSWORD=rootpwd \ mysql |
Эта команда так же откроет порт 3306 и задаст рутовый MySQL пароль — rootpwd. И то, и другое нам потребуется для дружбы с vault.
Теперь можно настроить database провайдера и рассказать ему, как создавать viewer аккаунты:
1 2 3 4 5 6 7 8 |
vault write database/config/mysql \ plugin_name=mysql-database-plugin \ connection_url="root:rootpwd@tcp(127.0.0.1:3306)/" \ allowed_roles=viewer vault write database/roles/viewer \ db_name=mysql \ creation_statements="CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}';GRANT SELECT ON *.* TO '{{name}}'@'%';" |
По сути обращение к vault за логином и паролем сводится к CREATE USER запросу, в котором явно прописано, что кроме “SELECT” viewer роль уметь ничего не должна. Можно было бы ещё указать, через сколько времени этот аккаунт самоуничтожится, но vault и так поставит какой-нибудь ограничитель.
Ну что, попробуем запросить пароль к базе:
1 2 3 4 5 6 7 8 |
vault read database/creds/viewer #Key Value #--- ----- #lease_id database/creds/viewer/d9ec67ae-07c9-c6ba-ae5a-9e49f74a8c78 #lease_duration 768h0m0s #lease_renewable true #password A1a-9xstpvu59txsx6qw #username v-root-viewer-pxutp8uzw7t1z53w97 |
А теперь протестируем этот логин и пароль на самом MySQL:
1 2 3 4 5 6 7 |
mysql -u v-root-viewer-pxutp8uzw7t1z53w97 -pA1a-9xstpvu59txsx6qw #mysql: [Warning] Using a password on the command line interface can be insecure. #Welcome to the MySQL monitor. Commands end with ; or \g. #Your MySQL connection id is 6 #Server version: 5.7.19 MySQL Community Server (GPL) # mysql > |
Всё работает! Ну не здорово ли? Теперь каждое приложение, каждый процесс будет получать свой собственный логин и пароль, которые нет смысла воровать (они же временные), и которые всегда можно отследить из-за вездесущего логгинга и мониторинга.
Практически заключение
К vault можно подключать и других провайдеров: для генерации секретов, аутентификации (например, AWS или github) и даже для аудита.
Чем я впечатлён до сих пор, так это как много можно узнать, просто изучив новый инструмент. Алгоритмы, концепции, идеи, пример грамотного разделения приложения на базовую логику и систему плагинов. Даже если бы сегодня был последний день, когда я использовал vault, время, потраченное на его изучение, того стоило. И день явно ведь был не последний. В общем, я доволен.
Спасибо.
Про HCL — прикольно, но на самом деле он хорош, когда готовишь в соусе Terraform 0.12, потом окунаешь в yaml- или jsonencode() и подаешь с Ansible на блюдечке 🙂
Отличная статья, спасибо, прояснились некоторые непонятные моменты
Какой смысл в этом хранилище, если я все равно в итоге знают секрет? Спрятать секрет от того, кто имеет токен не получится. Получается знание конечного секрета подменяется промежуточным шагом — запросом секрета. Какой в этом смысл?
Тут скорее решается проблема, как этот секрет передать приложению наиболее безопасным способом. Не избавиться от секрета, а только передать. Хотя если избавиться — то было бы вообще шикарно (например, перевести сервисные аккаунты из логин/паролей на managed identities). Но я отвлёкся.
Например, есть у нас контейнер где-нибудь в кластере (или лямбда функция, или ещё чего — почти всё в наши дни превратилось в контейнер), и нам нужно в него передать connection string для какого-нибудь mongodb.
Какие у нас опции есть для этого? Можно закоммитить прямо в исходный код приложения, но это определённо смертный грех. Можно положить в environment variables, и это лучше, но всё равно не очень, потому что переменные окружения легко подсмотреть, их видят все приложения (был какой-то exploit в nodejs пакете когда-то, который тупо слал все переменные окружения нужным людям) и никаких чётких логов от этого не останется.
А можно вместо connection string в env-vars (или k8s configmap, или как угодно) положить токен, и это немного меняет правила игры.
— Во-первых, токен скорее всего автосгенерирован и автоматически добавлен к контейнеру, так что девелопер этот токен вряд ли добровольно увидит.
— Во-вторых, даже если я подсмотрю токен, кто сказал, что у меня будет сетевой доступ к vault? Скорее всего vault будет сидеть где-то внутри кластера, с внутренними айпишками, и какой-нибудь сердобольный админ через istio или другой service mesh включил ему mTLS, и мне, человеку, сделать запрос в vault будет совсем тяжело. Не невозможно — хакнуть можно всё, что угодно. Но тяжело. Потому что обычных людей внутрь привилегированных сетей или кластеров так просто не пускают. А если и пустят, то попытка стырить и обменять токен на секрет оставит море следов со всеми вытекающими. А с современным разделением ролей быть и админом кластера, и админом аудит-логов, да ещё и чёрным хакером в душе — это очень маловероятное стечение обстоятельств.
Супер ! Особенно юмор! Браво!