Секреты в Go: переменные, Vault и облачные хранилища
Пароль от базы в коде — классика утечек. Разбираем три способа хранить секреты в Go-сервисе: переменные окружения, HashiCorp Vault и облачные Secrets Manager. Что выбрать под нагрузку и ротацию.

Секрет в исходном коде — самая частая причина утечки, которую находят автоматические сканеры GitHub. Строка db.Connect("postgres://user:hunter2@...") попадает в историю коммитов и остаётся там навсегда, даже если вы её удалите следующим коммитом. В Go нет встроенного менеджера секретов, поэтому выбор способа хранения — на вас. Разберём три подхода: переменные окружения, HashiCorp Vault и облачные хранилища (AWS Secrets Manager, GCP Secret Manager), и цену каждого.
Все примеры проверены на Go 1.22. Речь о серверном сервисе, который читает секреты при старте и в рантайме, а не о CLI-утилите для одноразового запуска.
Проблема, которую решаем
У сервиса есть пароль к базе, ключ к платёжному API и токен для внешнего сервиса. Требования к хранению обычно такие:
- секрета нет в репозитории и в собранном бинарнике;
- секрет можно поменять без пересборки и передеплоя приложения;
- доступ к секрету ограничен и логируется — кто и когда его прочитал;
- секрет живёт в памяти минимальное время и не утекает в логи.
Ни один подход не закрывает все четыре пункта бесплатно. Дальше — где каждый ломается.
Подход 1. Переменные окружения
Базовый способ, рекомендованный методологией 12-factor. Секрет передаётся процессу через окружение, код читает его через os.Getenv или os.LookupEnv.
package main
import (
"fmt"
"os"
)
// readSecret возвращает значение переменной или ошибку,
// если она не задана. Пустая строка тоже считается ошибкой:
// забытый секрет не должен молча превратиться в "".
func readSecret(name string) (string, error) {
value, ok := os.LookupEnv(name)
if !ok || value == "" {
return "", fmt.Errorf("секрет %s не задан", name)
}
return value, nil
}
func main() {
dbPassword, err := readSecret("DB_PASSWORD")
if err != nil {
// падаем на старте, а не в первом запросе к базе
panic(err)
}
fmt.Println("секрет прочитан, длина:", len(dbPassword))
}
Обратите внимание: os.Getenv для отсутствующей переменной вернёт пустую строку без ошибки. Поэтому для секретов используйте os.LookupEnv и проверяйте оба условия — иначе сервис попытается подключиться с пустым паролем и упадёт не там, где вы ожидаете.
Чем платите
Переменные окружения читаются любым процессом с тем же UID через /proc/<pid>/environ в Linux. Они попадают в дампы падения, если приложение логирует окружение, и в вывод отладочных инструментов. Ротация означает передеплой: чтобы поменять секрет, нужно перезапустить процесс с новым окружением. Аудита чтения нет.
Переменная окружения — не защита секрета, а способ не хранить его в коде. Это разные задачи. Первую она решает, вторую — нет.
Отдельная ловушка — файлы .env. Их удобно подключать через сторонние библиотеки в дев-окружении, но такой файл легко закоммитить. Добавьте .env в .gitignore первым же коммитом и не используйте его в проде.
Подход 2. HashiCorp Vault
Vault — отдельный сервис, который хранит секреты, шифрует их в покое, ведёт аудит-лог каждого чтения и умеет выдавать динамические секреты — например, временную пару логин/пароль к базе с TTL. Приложение аутентифицируется в Vault (через токен, AppRole, Kubernetes-аутентификацию) и читает секрет по API.
Официальный клиент — пакет github.com/hashicorp/vault/api. Точное имя метода для чтения секрета из KV v2 зависит от версии клиента; для новых версий это работа через client.KVv2("secret").Get(...), для старых — прямой вызов client.Logical().Read(...). Сверьте сигнатуру с документацией той версии клиента, что у вас в go.mod, — API движка KV между v1 и v2 отличается путями, и правдоподобный вызов из чужого гайда может не совпасть с вашей версией.
Схематично логика чтения выглядит так:
// Схематично — точные имена методов сверьте с документацией
// вашей версии github.com/hashicorp/vault/api
//
// 1. Создать клиент с адресом Vault (VAULT_ADDR).
// 2. Аутентифицироваться — получить токен
// (AppRole в проде, dev-токен только локально).
// 3. Прочитать секрет по пути, например secret/data/myapp.
// 4. Вынуть нужное поле из ответа (data.data для KV v2).
// 5. Обработать отсутствие поля как ошибку, а не пустую строку.
Динамические секреты
Главное преимущество Vault — не хранилище, а генерация. Настроив database-движок, вы получаете от Vault временные креды к PostgreSQL с TTL в несколько минут. Приложение запрашивает их при старте и обновляет по расписанию. Утёкший в лог пароль протухнет сам, а в аудит-логе Vault видно, какой сервис его запрашивал.
Чем платите
Vault — это инфраструктура, которую надо развернуть, настроить, бэкапить и обновлять. Его недоступность превращается в отказ вашего сервиса, если тот читает секреты в рантайме. Появляется задача первичной аутентификации — как приложение докажет Vault, что оно это оно, не имея ещё ни одного секрета. В Kubernetes её решает встроенная аутентификация по service account, вне его — AppRole с разделением role_id и secret_id.
Подход 3. Облачные Secrets Manager
Если сервис уже живёт в AWS, GCP или Azure, у облака есть свой менеджер секретов: AWS Secrets Manager, GCP Secret Manager, Azure Key Vault. Секрет хранится у провайдера, шифруется его KMS, а приложение получает доступ через IAM-роль инстанса — без пароля к самому хранилищу.
Обращение идёт через официальные SDK: github.com/aws/aws-sdk-go-v2 для AWS, cloud.google.com/go/secretmanager для GCP. Конкретные имена методов (например, у AWS это операция GetSecretValue) сверяйте с документацией используемой мажорной версии SDK — v1 и v2 у AWS различаются по структуре пакетов и сигнатурам.
Чем платите
Привязка к провайдеру: код с вызовами AWS SDK не переедет в GCP без переписывания слоя доступа к секретам. Каждое чтение через API тарифицируется и добавляет сетевую задержку, поэтому секреты кешируют в памяти на несколько минут. Ротация есть, но у AWS Secrets Manager она требует лямбда-функции ротации; проверьте актуальный порядок и тарификацию в документации провайдера — цены и лимиты меняются.
Сравнение
| Критерий | Env-переменные | Vault | Облачный SM |
|---|---|---|---|
| Секрета нет в репозитории | да | да | да |
| Ротация без передеплоя | нет | да | да |
| Аудит чтения | нет | да | да (через логи облака) |
| Динамические креды с TTL | нет | да | частично |
| Своя инфраструктура | не нужна | нужна | не нужна |
| Привязка к провайдеру | нет | нет | да |
| Стоимость входа | минимум | высокая | средняя |
Когда что выбирать
- Env-переменные — небольшой сервис, где секреты меняются редко, а секрет доставляет в окружение оркестратор (Kubernetes Secret, systemd EnvironmentFile). Осознавайте, что это не защита, а гигиена.
- Vault — когда нужны аудит, динамические креды к базам и вы готовы содержать инфраструктуру. Оправдан на нескольких сервисах и команде, которая умеет его эксплуатировать.
- Облачный SM — вы уже полностью в одном облаке, не хотите держать свой Vault и готовы к привязке к провайдеру. Дешевле по обслуживанию, дороже по свободе миграции.
Общее правило для любого подхода
Не логируйте секреты. Оберните строку с секретом в тип, у которого метод String() возвращает заглушку вроде [REDACTED] — тогда даже случайный %v в логе не выдаст значение. И не кладите секрет в структуру, которую сериализуете в JSON целиком: пометьте поле тегом json:"-".
Что делать дальше
Прогоните репозиторий через сканер секретов (gitleaks или встроенный в GitHub secret scanning) — сначала убедитесь, что старых утечек в истории нет. Если найдёте, ротируйте секрет: удаление из истории не отменяет того, что его уже могли скопировать. Дальше выберите один подход из таблицы под масштаб вашего проекта и оберните доступ к секретам в один пакет-адаптер — тогда переход с env на Vault не размажется по всему коду. И измеряйте: сколько времени секрет живёт в памяти процесса и попадает ли он хоть в один лог.
Логотип и фирменный стиль: Три концепции разного характера, палитра с обоснованием, проверка на читаемость в малом размере и в один цвет.
Спросить за 60 ₽Источники: HashiCorp Vault — Go client API documentation, AWS Secrets Manager — Retrieve secrets (Developer Guide)
Частые вопросы
У меня другая мажорная версия vault/api или aws-sdk-go. Примеры сработают?
Не сработало: os.Getenv возвращает пустую строку, хотя переменную я задал.
Насколько опасно хранить секреты в переменных окружения в проде?
Vault упал — мой сервис тоже ляжет?
Как гарантированно не залогировать секрет в Go?
Стоит ли шифровать секреты прямо в коде своим ключом?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.