Думаю многие слышали о такой системе хранения секретов Vault от Hashicorp. Кроме Vault Hashicorp выпустила еще много востребованных решений - Boundary, Terraform, которые предоставляют возможности управления PAM (Privileged Access Management) и IAS(Infrastructure As a Code) соответственно и многие другие.

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

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

При выполнении кода имейте ввиду, что разные версии Vault не всегда имеют обратно совместимое API, код приведенный ниже написан для версии v1.12. Так же для скачивания исходников Vault необходимо использовать VPN. Ниже по тексту будут приведено взаимодействие с Vault как с помощью HTTP API, так и с помощью CLI.

1. Secrets engines и auth methods

Принципиально Vault делится на две части: secrets engines и auth methods.

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

Типы secrets engines и auth methods заточены под мир современной разработки и DevOps, поэтому там можно увидеть такие платформы как AWS, GoogleCloud, Azure и другие.
В Enterprise версии есть функционал namespaceов, который позволяет создавать и удалять namespace. В базовой версии мы имеем только корневой namespace и все, что мы создаем хранится исключительно в нем. C помощью вложенных namespace мы можем
организовать Vault внутри Vault, дочерние namespace будут лежать внутри родительского.

При запуске сервера Vault, который изначально содержит только рутовый namespace и не имеет ни одного secrets engine и auth method, кроме токена (работает по дефолту). Все с чем будет работать Vault сначала должно быть примонтировано, то есть, условно, мы не можем создавать файлы в папке, которой нет. Далее мы можем активировать движки и методы с помощью команды mount, в которую передаем путь, куда хотим примонтировать движок и тип используемого движка. Так же есть возможность создавать собственные движки секретов и движки аутенфикации.

API Vault довольно структурировано и хорошо описано в документации. В нем есть основные методы POST, GET, DELETE и метод LIST, который является оберткой над GET для получения списка.

В качестве способа хранения данных Vault может использовать различные кластера и отдельные БД в терминах Vault это называется storage backend (storage). Для тестовых целей можно использовать in-memory хранилище, но после перезапуска сервера информация будет утеряна. Так же есть возможность создавать свои собственные плагины как для secrets engine, так и для auth methods. Плагины работают через библиотеку go-plugin (https://github.com/hashicorp/go-plugin), которая работает посредством rpc. Туториалы для создания плагинов есть на официальном сайте в соответствующем разделе документации.

2. Login

Операция логина в Vault нужна, чтобы вернуть рутовый токен, подставляя который в заголовок http запроса иметь доступ к серверу Vault. То есть если работать с Vault локально и знать рутовый токен (предоставляется при инициализации вместе с unseal ключами) операция логина не нужна. Рассмотрим в качестве примера auth метод github, так как данный ресурс всем знаком.

Для начала нужно активировать данный метод вызвав команду vault auth enable github, текущая команда активирует движок по дефолтному пути github/, для использования кастомного пути можно вызвать команду с ключом -path. Далее сконфигурируем github метод для связывания его с нашим аккаунтом, для этого будем использовать HTTP API. Выполним POST запрос по пути /auth/github/config и передадим в него следующие параметры:

{   
  "organization": "alekstet"
  "base_url": "https://github.com/alekstet" 
}

Для логина выполним POST запрос по пути /auth/github/login, в тело которого необходимо передать personal accsess token от github. Как его сгенерировать можно посмотреть здесь https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token.

После выполнения запроса в теле ответа вернется рутовый токен от Vault, поле auth.client_token, который дальше можно использовать для получения доступа к серверу.

"auth": {
    "client_token": "64d2a8f2-2a2f-5688-102b-e6088b76e344",
    "accessor": "18bb8f89-826a-56ee-c65b-1736dc5ea27d",
    "policies": ["default"],
    "metadata": {
      "username": "fred",
      "org": "acme-org"
    },
},

3. Wrapping-unwrapping

Так же must have решением является удобный механизм wrapping-unwrapping секретов, который позволяет хранить в скриптах развертывания не сам секрет, а токен, предоставляющий доступ к нему. Это позволяет иметь доступ только к определенным данным, а не ко всему ресурсу плюс накладывает TTL, после истечении которого секрет становится не валидным. Напишем функцию, которая возвращаем токен для предоставления доступа к секрету.

Стоит отметить, что в Vault есть такой термин как num_uses, то есть количество использований какого-либо секрета. Для wrapped токенов num_uses равно 1, то есть после однократного использования токен становится не валидным.

Так же есть команда rewrap, которая обновляет токен и выпускает новый токен (действует только для валидного токена). Для получения секрета из токена необходимо использовать команду unwrap. Ниже приведен фрагмент кода для wrappingга данных и получения wrapped токена. Запросы на другие эндпоинты Vault имеют похожий вид, отличие только в методе и пути.

package test

import (
	"bytes"
	"encoding/json"
	"io/ioutil"
	"net/http"
	"path"
	"time"
)

const (
	vaultAdr   = "http://127.0.0.1:8200"
	vaultToken = "your_root_token"
)

type DefaultResponse struct {
	RequestId     string      `json:"request_id"`
	LeaseId       string      `json:"lease_id"`
	LeaseDuration int         `json:"lease_duration"`
	Renewable     bool        `json:"renewable"`
	Data          interface{} `json:"data"`
	Warnings      interface{} `json:"warnings"`
	WrapInfo      *WrapInfo   `json:"wrap_info"`
}

type WrapInfo struct {
	Token        string    `json:"token"`
	Ttl          int       `json:"ttl"`
	CreationTime time.Time `json:"creation_time"`
	CreationPath string    `json:"creation_path"`
}

func WrapSecret() (string, error) {
	secret := make(map[string]string)
	secret["key1"] = "value1"

	postBody, err := json.Marshal(secret)
	if err != nil {
		return "", err
	}

	request, err := http.NewRequest("POST", path.Join(vaultAdr, "v1", "sys/wrapping/wrap"), bytes.NewBuffer(postBody))
	if err != nil {
		return "", err
	}

	request.Header.Set("X-Vault-Wrap-TTL", "60")
	request.Header.Set("X-Vault-Token", vaultToken)

	response, err := http.DefaultClient.Do(request)
	if err != nil {
		return "", err
	}

	defer response.Body.Close()

	byteBody, err := ioutil.ReadAll(response.Body)
	if err != nil {
		return "", err
	}

	var vaultResponse DefaultResponse
	err = json.Unmarshal(byteBody, &vaultResponse)
	if err != nil {
		return "", err
	}

	return vaultResponse.WrapInfo.Token, nil
}

После получения wrapped токен необходимо выполнить POST запрос по пути sys/wrapping/unwrap, передав в теле токен и в ответе в поле data будет лежать запакованный секрет.

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


  1. Kornevnv
    02.11.2022 19:09
    +2

    Статья выглядит "слегка" не законченной. Про устройство почти ничего, да и из полезных функций только две одна...