opened image

HashiCorp Vault: безопасное хранение секретов на VPS

 

Хранить пароли базы данных, API-ключи и токены в .env-файлах на сервере - плохая практика. При компрометации сервера или случайном попадании файла в git все секреты утекают сразу. HashiCorp Vault решает эту задачу: секреты хранятся зашифрованными, доступ управляется через политики, каждое чтение секрета логируется. Разберём установку через Docker, инициализацию и интеграцию с приложениями.

 

 

Почему Vault, а не просто зашифрованный файл

 

Зашифрованный файл требует ключа для расшифровки - и этот ключ где-то нужно хранить. Vault решает проблему иначе:

  • Секреты зашифрованы ключами, которые никогда не хранятся на диске в открытом виде

  • Доступ к секретам управляется через токены и политики

  • Каждый доступ к секрету записывается в audit log

  • Поддерживается ротация секретов без перезапуска приложений

  • Секреты могут генерироваться динамически (временные credentials для БД)

 

Vault 1.16 - актуальная версия на момент написания статьи.

 

 

Установка через Docker

 

На VPS с Ubuntu 24.04 самый простой способ развернуть Vault - Docker.

# Установка Docker, если ещё нет
curl -fsSL https://get.docker.com | bash

# Создаём директории
mkdir -p /opt/vault/{config,data,logs}

 

Конфиг Vault /opt/vault/config/vault.hcl:

storage "file" {
  path = "/vault/data"
}

listener "tcp" {
  address     = "0.0.0.0:8200"
  tls_disable = "true"
}

api_addr = "http://127.0.0.1:8200"
cluster_addr = "http://127.0.0.1:8201"

ui = true

 

Запуск через Docker:

docker run -d \
  --name vault \
  --cap-add=IPC_LOCK \
  -p 127.0.0.1:8200:8200 \
  -v /opt/vault/config:/vault/config \
  -v /opt/vault/data:/vault/data \
  -v /opt/vault/logs:/vault/logs \
  --restart unless-stopped \
  hashicorp/vault:1.16 vault server -config=/vault/config/vault.hcl

docker ps | grep vault

 

Флаг --cap-add=IPC_LOCK нужен, чтобы Vault мог блокировать память и предотвращать запись секретов в swap.

 

 

 

 

Инициализация Vault

 

При первом запуске Vault находится в состоянии "sealed" (запечатан) - его нужно инициализировать.

# Устанавливаем переменную окружения
export VAULT_ADDR='http://127.0.0.1:8200'

# Инициализация: создаём 5 ключей, нужно 3 для unseal
docker exec vault vault operator init -key-shares=5 -key-threshold=3

 

Вывод будет выглядеть примерно так:

Unseal Key 1: abc123...
Unseal Key 2: def456...
Unseal Key 3: ghi789...
Unseal Key 4: jkl012...
Unseal Key 5: mno345...

Initial Root Token: hvs.CAESIJK...

 

Сохраните unseal-ключи и root token в надёжном месте. Без 3 из 5 ключей данные восстановить невозможно. В реальных проектах ключи распределяют по разным людям или сохраняют в разных хранилищах.

 

 

Unseal - разблокировка

 

После каждого рестарта Vault запечатан. Для разблокировки вводим 3 из 5 ключей:

docker exec -it vault vault operator unseal abc123...
docker exec -it vault vault operator unseal def456...
docker exec -it vault vault operator unseal ghi789...

# Проверяем статус
docker exec vault vault status

Статус после успешного unseal:

Key             Value
---             -----
Seal Type       shamir
Initialized     true
Sealed          false
Total Shares    5
Threshold       3
Version         1.16.0
HA Enabled      false

 

Автоматический unseal через переменные окружения (для разработки - не для продакшена!):

# В docker-compose.yml
environment:
  - VAULT_DEV_ROOT_TOKEN_ID=myroot
  - VAULT_DEV_LISTEN_ADDRESS=0.0.0.0:8200

 

В dev-режиме (vault server -dev) unseal не нужен, но данные хранятся только в памяти.

 

 

Работа с секретами KV

 

Авторизация

 

export VAULT_ADDR='http://127.0.0.1:8200'
export VAULT_TOKEN='hvs.CAESIJK...'

# Или через docker exec
docker exec -e VAULT_ADDR=http://127.0.0.1:8200 -e VAULT_TOKEN=hvs.CAESIJK... vault vault status

 

Включение KV secrets engine

 

# KV v2 поддерживает версионирование секретов
docker exec -e VAULT_ADDR=http://127.0.0.1:8200 -e VAULT_TOKEN=$ROOT_TOKEN \
  vault vault secrets enable -path=secret kv-v2

 

Запись секретов

 

# Записываем секреты для приложения
docker exec -e VAULT_ADDR=http://127.0.0.1:8200 -e VAULT_TOKEN=$ROOT_TOKEN \
  vault vault kv put secret/myapp/database \
    host="localhost" \
    port="5432" \
    name="myapp_db" \
    username="dbuser" \
    password="SuperSecretPass123"

# Записываем API-ключи
docker exec -e VAULT_ADDR=http://127.0.0.1:8200 -e VAULT_TOKEN=$ROOT_TOKEN \
  vault vault kv put secret/myapp/api \
    stripe_key="sk_live_..." \
    sendgrid_key="SG...."

 

Чтение секретов

 

docker exec -e VAULT_ADDR=http://127.0.0.1:8200 -e VAULT_TOKEN=$ROOT_TOKEN \
  vault vault kv get secret/myapp/database

# Только конкретное поле
docker exec -e VAULT_ADDR=http://127.0.0.1:8200 -e VAULT_TOKEN=$ROOT_TOKEN \
  vault vault kv get -field=password secret/myapp/database

# JSON-вывод
docker exec -e VAULT_ADDR=http://127.0.0.1:8200 -e VAULT_TOKEN=$ROOT_TOKEN \
  vault vault kv get -format=json secret/myapp/database | jq .data.data

 

 

 

 

Политики доступа

 

Root token даёт полный доступ - для приложений создают ограниченные политики.

 

Создаём файл политики /tmp/myapp-policy.hcl:

# Читать секреты конкретного приложения
path "secret/data/myapp/*" {
  capabilities = ["read"]
}

# Просматривать список (не значения)
path "secret/metadata/myapp/*" {
  capabilities = ["list"]
}

 

Применяем политику:

docker exec -e VAULT_ADDR=http://127.0.0.1:8200 -e VAULT_TOKEN=$ROOT_TOKEN \
  vault vault policy write myapp-read /tmp/myapp-policy.hcl

 

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

docker exec -e VAULT_ADDR=http://127.0.0.1:8200 -e VAULT_TOKEN=$ROOT_TOKEN \
  vault vault token create -policy=myapp-read -ttl=720h

# Вывод:
# token    hvs.CAESIHab...
# token_ttl 720h

 

Этот токен передаётся приложению. Он может только читать секреты из secret/data/myapp/ - ничего больше.

 

 

Интеграция с приложениями

 

Через переменные окружения (bash-скрипт)

 

Создаём /opt/myapp/load-secrets.sh:

#!/bin/bash
export VAULT_ADDR='http://127.0.0.1:8200'
export VAULT_TOKEN='hvs.CAESIHab...'

# Получаем секреты и экспортируем
DB_CREDS=$(vault kv get -format=json secret/myapp/database | jq -r '.data.data')

export DB_HOST=$(echo $DB_CREDS | jq -r '.host')
export DB_PASSWORD=$(echo $DB_CREDS | jq -r '.password')
export DB_NAME=$(echo $DB_CREDS | jq -r '.name')

# Запускаем приложение с переменными
exec "$@"

 

Использование:

chmod +x /opt/myapp/load-secrets.sh
/opt/myapp/load-secrets.sh node /opt/myapp/server.js

 

Через HTTP API

 

Любое приложение может получить секреты через REST API:

curl -s \
  -H "X-Vault-Token: hvs.CAESIHab..." \
  http://127.0.0.1:8200/v1/secret/data/myapp/database | jq .data.data

 

Python-пример

import hvac
import os

client = hvac.Client(
    url='http://127.0.0.1:8200',
    token=os.environ['VAULT_TOKEN']
)

secret = client.secrets.kv.v2.read_secret_version(
    path='myapp/database',
    mount_point='secret'
)

db_password = secret['data']['data']['password']
db_host = secret['data']['data']['host']

 

Node.js-пример

const fetch = require('node-fetch');

async function getSecret(path) {
  const response = await fetch(`http://127.0.0.1:8200/v1/secret/data/${path}`, {
    headers: { 'X-Vault-Token': process.env.VAULT_TOKEN }
  });
  const data = await response.json();
  return data.data.data;
}

const dbCreds = await getSecret('myapp/database');

 

 

Версионирование секретов KV v2

 

KV v2 хранит историю изменений:

# Получить версию 2
vault kv get -version=2 secret/myapp/database

# Список версий
vault kv metadata get secret/myapp/database

# Откатить к версии 1
vault kv rollback -version=1 secret/myapp/database

 

 

Автоматический unseal через systemd

 

Для автоматического запуска после перезагрузки без ручного unseal используют Transit auto-unseal или AWS KMS. Простой вариант для одиночного сервера - скрипт с ключами (не самый безопасный, но практичный):

cat > /opt/vault/unseal.sh << 'EOF'
#!/bin/bash
export VAULT_ADDR='http://127.0.0.1:8200'
sleep 5  # ждём запуска контейнера
vault operator unseal KEY1
vault operator unseal KEY2
vault operator unseal KEY3
EOF
chmod 600 /opt/vault/unseal.sh

Защитите этот файл - только root должен иметь к нему доступ.

 

 

Мониторинг и аудит

 

Включение audit log:

vault audit enable file file_path=/vault/logs/audit.log

 

Просмотр кто что читал:

cat /opt/vault/logs/audit.log | jq 'select(.type=="response") | {time: .time, path: .request.path, client: .request.remote_address}'

 

 

 

 

Итог

 

Vault решает задачу управления секретами через три механизма: хранилище с шифрованием, политики доступа по токенам и полный audit trail. Для небольшого VPS достаточно KV v2 через Docker с file-хранилищем. Главное - сразу создавать отдельные токены для каждого приложения с минимально необходимыми правами, а root token использовать только для администрирования. Регулярно ротируйте application-токены командой vault token renew или создавайте новые.