Привет {{ habra_user.name }}!

Не так давно Яндекс Облако вышли в релиз с сервисом хранения секретов Yandex Lockbox. Хочу поделиться опытом его использования в своем личном проекте. Пригодится тем, кто думает о том, как уйти от использования «.env» файлов и доверить хранение секретов облачному провайдеру.

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

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

Начнем

1) Регистрируемся на Яндекс Облако, создаем платежный аккаунт и директорию проекта, после чего переходим в раздел Все сервисы -> Безопасность -> Lockbox:

2) Создаем свой первый секрет.

Секрет это набор пар ключ-значение, где в качестве ключа используется строка (имя той самой переменной окружения, которую мы прописывали обычно в .env файле), а в качестве значения – собственно строка с секретом или файл.

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

При создании секрета задаем имя, при желании описание/метку. Также рекомендую поставить switch «Запретить удаление секрета», что защитит вас от случайного удаление, если Вы потом второпях случайно «кликнете не туда». Удалить секрет в последующем можно, но для начала надо будет зайти в его параметры и выключить этот switch.

В разделе «Версия» можете указать описание к версии и, собственно, создать необходимое Вам кол-во пар ключ/значение, которое вам будет необходимо:

И тут Вас ждет приятный сюрприз. Да, один секрет может хранить «все ваши секреты», ведь «на выдаче» это будет один JSON! Тут и вправду для меня был приятный сюрприз, ибо у меня этих пар добрых три десятка и, когда сервис выходил из стадии Preview, я получил от Яндекса письмо что теперь за каждый секрет надо будет платить по 18 рублей в месяц.

Я было умножил 18 на 30 и жаба начала душить, но потом понял что это один секрет и, осознав что хранение всех секретных значений будет обходиться для меня 60 копеек в день, продолжил использовать сервис в своем личном проекте)

И так, я задал две пары TEST_SECRET и TEST_SECRET2. Далее кликаем “Создать” и идем дальше.

3) После создания копируем получившийся идентификатор секрета, он нам потом пригодится в запросе:

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

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

После создания новой версии секрета, в том же меню у старой версии появляется кнопка «запланировать удаление», которая позволяет удалить старую версию спустя заданный промежуток времени.

Этот временной лаг крайне необходим, ведь Вы можете вдруг вспомнить что вам еще нужен некий пароль/ключ из старой версии, поэтому удалять сразу – не всегда хорошее решение. Также в меню появится пункт для отката секрета, на случай «я что-то нажал и все пропало».

Если кликнуть по версии, то можно просмотреть её содержимое. Я создал новую версию секрета, где ключ TEST_SECRET будет содержать ответ на главный вопрос жизни, вселенной и всего такого, а TEST_SECRET2 будет содержать пароль от почты величайшего шпиона всех времен и народов:

Привет Христо!
Привет Христо!

Также слева есть блок «права доступа», в котором Вы можете предоставить права конкретному аккаунту для доступа к указанному секрету. Можете например добавить в облако отдельный аккаунт коллеги-разработчика и предоставить ему право получать секрет (эта роль называется lockbox.payloadViewer):

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

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

Ну а мы, понимая, что токен живет всего 12 часов, при этом мы не планируем его никуда сохранять, да и проект у нас личный с невысоким уровнем ответственности, пойдем дальше используя свой личный аккаунт=)

Я использую секреты везде, даже на виртуалке для DEV-нужд, которая крутится у меня на домашнем компьютере, поэтому мы рассматриваем использование секрета на машинах за пределами Яндекс облака. Если Вы будете использовать секрет внутри машины на Яндекс облаке, то используйте сервисный аккаунт, который прикрепите к машине при её создании.

Я буду показывать пример использования секрета в связке с Docker, Docker-compose и Flask, для чего я создал простейший проект, который вы можете взять из репозитория. Если у вас другой стек в проекте, то не волнуйтесь, получать секреты мы будет из-под bash, так что читайте дальше и все поймете.

Далее я подразумеваю что на хосте у нас установлен Docker и Docker-compose, при этом наш пользователь является членом группы Docker, соответственно мы можем работать с Docker/Docker compose без прав суперпользователя. Но обратите внимание что тут рекомендуют настроить Rootless mode.

Также отмечу, что на тестовой ВМ я использую Ubuntu server 22, что означает что команды для Debian будут аналогичные, а на других *nix дистрибутивах команды могут отличаться, в частности может быт другой менеджер пакетов и собственно их наименование.
И так у нас есть рабочий стек, который имеет следующую структуру:

- habrasecret
 ⊦ Dockerfile
 ⊦ docker-compose.yml
 ⊦ requirements.txt
 ⊦ app.py
 ⊦ show_me_secret.sh

Кейс простой:
Для запуска веб-сервера мы из-под bash запускаем скрипт show_me_secret.sh, который будет получать у lockbox наш секрет в виде JSON, парсить его в переменные окружения, передавать дальше, в нашем случае, в docker-compose, который собирает нам образ контейнера с Flask и простейшим веб-приложением, выводящем нам долгожданный секрет!
Рассмотрим наш стек без секрета и результат, который он выдает (в репозитории первый коммит):

Скрипт show_me_secret.sh – сейчас он просто запускает наш стек, описанный в файле docker-compose.yml:

#!/bin/bash
docker-compose up 

docker-compose.yml - тут описан наш стек из одного контейнера и без использования секретов вообще:

version: '3.9'

services:
# наш единственный контейнер с Flask внутри
  habr-top-secret-app:
# блок параметров сборки
    build:
# контекст при сборке – та же директория, где и лежит docker-compose.yml  
      context: .	
# Ссылка на Docker-файл для этого контейнера	
      dockerfile: Dockerfile
    ports:
# пробрасываем порт 80 (HTTP) на стандартный порт Flask - 5000
      - "80:5000"		
    volumes:
# монтируем файл с приложением с хоста непосредственно в контейнер, чтобы
# можно было его редактировать и видеть изменения, не пересобирая каждый раз контейнер.
      - ./app.py:/app/app.py	

Dockerfile – тут инструкции по сборке контейнера:

# в качестве исходного образа используем образ с python:3.11.2
FROM python:3.11.2-slim-bullseye
# создаем в контейнере директорию для приложения
RUN mkdir app
# и указываем ее в качестве рабочей
# (т.е. все последующие пути при сборке будут 
# рассчитываться относительно этой директории)
WORKDIR /app			
# копируем текстовый файл со списком зависимостей нашего проекта
COPY requirements.txt .	
# командуем pip установить все зависимости проекта из только что скопированного файла
RUN pip install -r requirements.txt
# открываем порт 5000
EXPOSE 5000 
# выполняем команду python app.py в bash контейнера
CMD ["python", "app.py"]

requirements.txt – файл с зависимостями, там у нас одна строка:
Flask==2.2.3

app.py - собственно наше приложение на Python:

import os
from flask import Flask
app = Flask(__name__)

# декоратор, который сделает эту функцию обработчиком запроса
# к нашему веб-серверу по пути <hostname_or_ip>/
@app.route('/')			
def hello_world():
    # тут, собственно, мы возвращаем в качестве ответа строку,
    # в которую пытаемся подставить секрет из переменной окружения «TEST_SECRET»
    return f'Hello, Habr! This is top secret: {os.environ.get("TEST_SECRET", default="******")}!!!'

if __name__ == "__main__":
    app.run(debug=True, host='0.0.0.0')

Запускаем (в bash хоста) наш веб-сервер и переходим по адресу, ссылающемуся на наш сервер:

./show_me_secret.sh
(если увидите ошибку прав доступа при запуске, то не забудьте выполнить sudo chmod +x ./show_me_secret.sh, пометив тем самым наш скрипт как исполняемый файл)

Видим, что вместо секрета у нас выводится значение по умолчанию, которое мы указали в app.py, т.к. TEST_SECRET у нас отсутствует в качестве переменной окружения.

Теперь наладим получение секретов с Lockbox!

4) Готовим нашу ВМ для использования секрета.

Я для этих экспериментов поднял VPS, которую взял у ребят из TimeWeb. Машину взял простейшую – 1vCPU/1GB RAM, так что, если не откроется, то привет ХабраЭффект! Тыц

Установим необходимые пакеты на хост:

обновим индекс пакетов в системе

sudo apt update

установим пакет для парсинга JSON непосредственно в bash

sudo apt install -y jq

далее установим CLI Яндекс облака в три команды:

curl -sSL https://storage.yandexcloud.net/yandexcloud-yc/install.sh | bash
echo 'source ~/yandex-cloud/completion.zsh.inc' >>  ~/.zshrc
exec -l $SHELL

теперь инициализируем утилиту yc:

yc init

Видим в консоли приглашение перейти по ссылке, чтобы получить OAuth токен и приглашение его ввести:

Welcome! This command will take you through the configuration process.
Please go to https://oauth.yandex.ru/authorize?response_type=token&client_id=<тут будет ваш идентификатор> in order to obtain OAuth token.

Будучи авторизованным в яндексе под тем же аккаунтом, под которым создано облако, или аккаунтом, которому Вы назначили роль lockbox.payloadViewer, переходим по ссылке и видим наш токен авторизации:

Копируем его и вставляем в консоль.
Выбираем директорию проекта (1) и отказываемся выбрать зону по умолчанию для вычислений (no).

Поздравляю, теперь для Яндекс Облака наша ВМ на TimeWeb – "как своя" =)

Теперь попробуем получить IAM токен и, используя его, запросить секреты:
Для начала сохраним в переменную ID нашего секрета (не версии, а секрета!):

YAC_LOCKBOX_SID=e6qli9jrrs3lhmfk6j4u
YA_IAM_TOKEN=$(yc iam create-token)
secrets=$(curl -X GET -H "Authorization: Bearer ${YA_IAM_TOKEN}" https://payload.lockbox.api.cloud.yandex.net/lockbox/v1/secrets/$YAC_LOCKBOX_SID/payload)

и теперь выведем в консоль результат, что же нам в результате легло в secrets:

echo $secrets
{ "entries": [ { "key": "TEST_SECRET", "textValue": "42" }, { "key": "TEST_SECRET2", "textValue": "Москва4" } ], "versionId": "e6qib38mhj6d898g78sk" }

Мы видим что в переменною легла строка с JSON, содержащая наши секреты. Признаюсь честно, я долго голову ломал, чтобы понять как непосредственно в BASH это все распарсить, чтобы каждому контейнеру отдать только его секреты. Тут я должен поблагодарить IAmStoxe за этот Gist.

Разобравшись, что да как, вносим правки в наш сценарий, запускающий наш стек:
show_me_secret.sh

#!/bin/bash
# сообщаем bash что переменные необходимо экспортировать в среду последующих команд
set -a
# не забудьте указать тут ID Вашего секрета
YAC_LOCKBOX_SID=e6qli9jrrs3lhmfk6j4u
# получаем токен для авторизации
YA_IAM_TOKEN=$(yc iam create-token)
# запрашиваем секреты
secrets=$(curl -X GET -H "Authorization: Bearer ${YA_IAM_TOKEN}" https://payload.lockbox.api.cloud.yandex.net/lockbox/v1/secrets/$YAC_LOCKBOX_SID/payload)
# парсим JSON перебирая словари в списке, доступном по ключу entries
for row in $(echo "${secrets}" | jq -r '.entries | .[] | @base64'); do
    _jq() {
    echo "${row}" | base64 --decode | jq -r "${1}"
    }
    # имя переменной окружения с секретом у нас доступно в JSON  по ключу key
    name=$(_jq '.key')
    # значение доступно по ключу textValue
    value=$(_jq '.textValue')

    # экспортируем переменную окружения с секретом
    export $name=$value
done
# далее запускаем наш стек.
docker-compose up

После запуска вы изменений не увидите, т.к. нам необходимо пробросить переменные в наш контейнер. В нашем случае вносим правки в файл docker-compose.yml:

services:
# наш единственный контейнер с Flask внутри
  habr-top-secret-app:
# блок параметров сборки
    build:
# контекст при сборке – та же директория, где и лежит docker-compose.yml  
      context: .	
# Ссылка на Docker-файл для этого контейнера	
      dockerfile: Dockerfile
    ports:
# пробрасываем порт 80 (HTTP) на стандартный порт Flask - 5000
      - "80:5000"		
    volumes:
# монтируем файл с приложением с хоста непосредственно в контейнер, чтобы
# можно было его редактировать и видеть изменения, не пересобирая каждый раз контейнер.
      - ./app.py:/app/app.py
# в этом блоке пробрасываем переменные окружения с хоста в контейнер
    environment:
# слева наименование переменной окружения в контейнере, справа - на хосте
      TEST_SECRET:  ${TEST_SECRET}
      TEST_SECRET2: ${TEST_SECRET2} 

Ну и используем обе эти переменные в проекте, в файле app.py:

import os
from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
    return  'Hello, Habr! This is top secret:<br>' \
           f'The main answer: {os.environ.get("TEST_SECRET", default="******")}!<br>' \
           f'The most difficult password: {os.environ.get("TEST_SECRET2", default="******")}!'

if __name__ == "__main__":
    app.run(debug=True, host='0.0.0.0')

И вуаля:

Вот так мы научились хранить секреты в Яндекс Lockbox и использовать их на стороне, за пределами облака Яндекс, например на ВМ, развернутой в TimeWeb.Cloud.

Что касается сертификатов SSL то вам достаточно добавить их в Яндекс Certificate Manager и в том же скрипте с помощью yc загружать их по наименованию на хост, после чего можете их либо смонтировать, либо скопировать в контейнер, ну или передать в виде текста:

{
yc certificate-manager certificate content \
    --name your-cert-name-in-yandex-cloud \
    --chain ~/your/path/to/ssl/folder/www.youtopsecret.domain.crt \
    --key ~/ your/path/to/ssl/folder/www.youtopsecret.domain.key --no-user-output
} &> /dev/null

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

Вот репозиторий для всего вышенаписанного: https://gitlab.com/lepehovsv/habrasecret

Спасибо за внимание, надеюсь я Вам помог! Если у более опытных коллег возникнут замечания по теме, с удовольствием их прочту в комментариях, ведь для этого и пишу – передать и перенять опыт.

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


  1. gmtd
    21.04.2023 11:57

    А в чем преимущество перед .env файлами?

    Какие use cases?


    1. Lepekhov Автор
      21.04.2023 11:57
      -2

      В репозиторий можно не ложить ни файлы с секретами, ни шаблоны таких файлов. Добавился разработчик - вы либо дали ему право на чтение секрета, либо создали для него отдельную копию с другими данными, например. Либо вообще доступ не даете, понимая что он у себя как раз из .env файла свою редакцию секретов придумает, а при CI/CD они уже из Lockbox подтянутся - отдельная редакция для staging, отдельная для production.

      Ну и если машину уведут, то достаточно заблокировать аккаунт, авторизованный на ВМ в ЯО и чувствительные данные останутся в секрете.

      Вот в статье коллега интересную статистику приводит:

      Согласно недавнему исследованию ученых из Университета штата Северная Каролина, более 100 000 общедоступных репозиториев GitHub содержат открытые секреты приложений непосредственно в исходном коде. Это исследование — от частных токенов API до криптографических ключей — просканировало только около 13% общедоступных репозиториев GitHub — показывает, что надлежащая защита секретов приложений является одним из наиболее часто игнорируемых методов защиты информации в программном обеспечении.


      1. gmtd
        21.04.2023 11:57

        Если кто-то хранить секреты в коде открыто, то это его проблемы. И Гитхаб и другие для CI/CD позволяют хранить секреты скрытно и использовать их при деплое.

        С точки зрения безопасности скачивать секрет с облака и читать с .env файла - одно и тоже. Разве нет?


        1. Lepekhov Автор
          21.04.2023 11:57

          Нет, далеко не одно и то же. Env-файл у вас и на хосте и в контейнере и в его образе, не так ли? А иногда и в репозитории оказывается. Для CI/CD можно использовать что душе угодно - хочешь Gitlab, хочешь Vault, хочешь - услуги облачных провадеров - тот же Lockbox Яндекса, или Secret Manager от Google.
          В конечном счете секрет в любом случае должен оказаться в оперативной памяти, разве нет? Так вот use cases заключается в том чтобы он там оказался, не оставаясь нигде в других местах. И хранился в зашифрованном виде в соответствующем хранилище.
          Объясните конкретно - в чем я не прав, а не молча минусуйте карму, пожалуйста.


          1. gmtd
            21.04.2023 11:57
            +1

            Я ничего вам не минусовал

            Если злоумышленник попадает на сервер, то он может изменить код и прочитать/слогировать ваши секреты после того как вы их получили. Ничего сложного

            Если он не на сервере, то и .env файлы он не видит

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


            1. Lepekhov Автор
              21.04.2023 11:57

              Окей. Банальный пример: если у вас на машинах для разработки/тестирования используются секреты, то в случае ошибки программиста, ваш веб-сервер может выдать содержимое каталога и/или файлов в корне проекта и тот самый .env файл окажется в чужих руках, чего не будет в случае с секретами, полученными из хранилища.
              Поднимите веб-сервер с белым IP и, не пройдет суток, как в логах увидите работу бот-нетов и запрос http://..../.env
              Да масса примеров, доказывающих что c точки зрения безопасности скачивать секрет с облака и читать с .env файла - НЕ одно и тоже. Да и Github в конце-концов передает секрет на сервер при деплое, что, как Вы выразились, тоже называется "скачать секрет с облака". Вопрос лишь в конкретной реализации.
              Ну а насчет сетевых проблем думаю тут нет смысла и обсуждать. У вас банально реестр образов контейнеров тоже по сети доступен (или не доступен). Ну и в случае каких-либо проблем пайплайн не закончится, хелсчек не ответит с кодом 200 и, новая редакция сервера не заведется, соответственно старая - не заглушится.


  1. kiaplayer
    21.04.2023 11:57
    +2

    Это аналог HashiCorp Vault?


    1. Lepekhov Автор
      21.04.2023 11:57

      Не совсем, у HashiCorp Vault есть главное отличие и преимущество - он с открытыми исходниками, а что под капотом Lockbox - известно только разработчикам Яндекс Облако... Но функцинальное назначение то же, Вы правы.


  1. mirtov-alexey
    21.04.2023 11:57
    +2

    добрый день!

    1) если уж использовать yc cli то можно прямо с помощью него получать значение секрета вот так и не обязательно curl.

    2) секреты лучше разбивать на разные, а хранить все в одном это плохая практика. К ним как правило разные права доступа, разные задачи по ротации и тд

    3) секреты также можно шифровать с помощью kms

    4) про сравнение с vaul hashicorp есть тут

    5) лучше показывать способ получения секрета изнутри виртуальной машины путем назначения сервисного аккаунта на вм, получения токена из сервиса метаданных и не использовать oauth токен

    в остальном отличная статья ????


    1. Lepekhov Автор
      21.04.2023 11:57

      благодарю за рекомендации и тёплые слова, я старался ☺ по первым четырём пунктам вас понял, обязательно дополню материал. А вот по пятому надо попробовать, т.к. в ЯО на ВМ с этим проблем не было, а вот за пределами облака не получилось по началу с сервиса метаданных получить токен, просто не было ответа и все тут. Но попробую разобраться, тем не менее.


  1. DvoiNic
    21.04.2023 11:57
    +4

    "яндекс" и "секреты"???