Секреты в проде: Vault, переменные окружения и частые ошибки
Где протекают пароли и ключи, почему переменные окружения — компромисс, как поднять Vault на Linux, выдавать секреты сервисам и CI и что делать при утечке.

Коротко: секрет в продакшене не должен лежать в git, в образе контейнера, в логах и в переменных окружения, которые видит каждый процесс. Рабочая схема для Linux-сервера: секреты хранятся в менеджере вроде HashiCorp Vault или OpenBao, сервис получает их по короткоживущему токену и читает из файла в tmpfs или через LoadCredential= в systemd, у каждого сервиса своя политика доступа, всё ротируется по расписанию, а утечку закрывают сменой ключа, а не удалением коммита.
Почти каждый разбор инцидента начинается одинаково: пароль от базы нашёлся в публичном репозитории, токен облака — в логе CI, ключ платёжного шлюза — в docker inspect на тестовом стенде, куда доступ был у половины компании. Злоумышленнику не нужно ломать ядро или искать 0-day, если ключ лежит открытым текстом там, где его никто не ждёт.
Острее всего проблема проявляется на стыке команд. Когда вы отдаёте продукт команде, которая ведёт разработку под ключ, или принимаете проект обратно, хуже всего обычно передают именно секреты: архивом .env в мессенджере, паролем в тикете, одним общим root-ключом на всех. Поэтому вопрос «где и как вы храните секреты, кто их видит и как их отозвать» стоит задать до первой строчки кода. Ниже — как устроить хранение секретов на Linux-сервере правильно, от простого к сложному, с командами и типичными ошибками.
Что считается секретом и где он протекает?
Секрет — любая строка, знание которой даёт доступ или права. Это не только пароли:
- пароли и строки подключения к базам данных, брокерам, Redis;
- API-ключи внешних сервисов: платёжные шлюзы, SMS, почта, облака, карты;
- приватные ключи TLS, SSH и подписи пакетов, ключ подписи JWT;
- токены CI/CD, deploy-ключи репозиториев, kubeconfig;
- ключи шифрования данных и соль для хешей;
- OAuth client secret, webhook-секреты, коды восстановления 2FA.
Утекают секреты не через «взлом хранилища», а через побочные каналы. Самые частые:
- Git. Закоммиченный
.env,config.ymlили тестовый скрипт с ключом. Удаление файла в следующем коммите ничего не меняет — ключ остаётся в истории и во всех форках. - Образы контейнеров.
ENV DB_PASSWORD=...илиCOPY .envв Dockerfile — и секрет оказывается в слое образа в registry. - Логи. Приложение при старте печатает конфиг целиком, отладочный вывод HTTP-клиента пишет заголовок
Authorization, CI выводит команду с паролем в аргументах. - Аргументы командной строки.
mysql -pSecretвиден любому пользователю черезps auxи попадает в~/.bash_history. - Переменные окружения — об этом подробно ниже.
- Тикеты, чаты, вики. «Скинь пароль от прода» в рабочем мессенджере живёт годами и находится поиском.
- Бэкапы и дампы. Копия
/etcили домашнего каталога в незашифрованном бакете.
Почему переменные окружения — не хранилище секретов?
Методология Twelve-Factor App советует держать конфигурацию в окружении, и многие поняли это как «секреты — в переменных окружения». Для конфигурации это удобно, для секретов — компромисс с заметными дырами:
- Окружение наследуется. Каждый дочерний процесс — shell-скрипт,
curl, ImageMagick, сторонний бинарник — получает полную копию всех переменных, в том числе тех, что ему не нужны. - Его видно в /proc. Файл
/proc/<PID>/environчитается владельцем процесса и root. Любой код, выполненный от того же пользователя (уязвимость в зависимости, RCE через загрузку файла), прочитает все секреты одной командой. - Его выводят инструменты.
docker inspect,systemctl show,kubectl describe pod, страницы ошибок фреймворков в debug-режиме, системы сбора ошибок вроде Sentry, если не настроить фильтры. - Оно попадает в дампы. Core dump и отчёты о падениях содержат окружение процесса.
- Его нельзя сменить на лету. Чтобы обновить значение, процесс нужно перезапустить.
Это не значит, что переменные окружения запрещены. Для некритичных параметров (уровень логов, адрес сервиса, флаги) они идеальны. Для секретов лучше файл с правами 0400 на tmpfs, который читает только нужный процесс, или прямой запрос к менеджеру секретов. Сравнение способов:
| Способ | Кто видит | Ротация | Аудит | Когда уместен |
|---|---|---|---|---|
| Хардкод в коде | git, образ, все разработчики | только релизом | нет | никогда |
Файл .env в проекте | все, у кого есть доступ к серверу или архиву | вручную | нет | локальная разработка |
| Переменные окружения | /proc, дочерние процессы, inspect | рестартом | нет | некритичная конфигурация, CI с masked-переменными |
| Файл на tmpfs, права 0400 | владелец и root | перечитыванием файла | частично | один сервер, systemd-сервисы |
| SOPS + age в git | держатели ключа расшифровки | коммитом | история git | GitOps, небольшие команды |
| Vault / OpenBao | только по политике | автоматически, динамические секреты | полный журнал | несколько сервисов и сред, команда от 3–5 человек |
Какие уровни зрелости бывают у управления секретами?
Не нужно сразу разворачивать кластер Vault для одного VPS с блогом. Разумно двигаться по ступеням и останавливаться там, где затраты соответствуют риску.
Уровень 0: секреты в коде и в git
Самое опасное состояние, и встречается оно чаще, чем кажется. Первое действие — найти и вычистить. Инструменты gitleaks и trufflehog сканируют всю историю репозитория:
gitleaks detect --source . --log-opts="--all" --report-path gitleaks.json
trufflehog git file://. --only-verified
Всё найденное считается скомпрометированным и ротируется. Переписывать историю (git filter-repo) имеет смысл только после ротации и только ради гигиены.
Уровень 1: .env вне репозитория и права на файлы
Секреты вынесены в файл, файл — в .gitignore, на сервере он принадлежит пользователю сервиса с правами 0600. Плюс pre-commit хук с gitleaks, чтобы новый ключ не уехал в репозиторий. Это минимум для любого проекта.
Уровень 2: шифрованные секреты в git
SOPS с ключами age шифрует значения в YAML или JSON, оставляя имена ключей читаемыми. Файл можно коммитить, смотреть diff на ревью и разворачивать вместе с кодом. Расшифровка — на сервере или в CI ключом, который хранится отдельно. Хорошо подходит для Ansible и GitOps, но не даёт аудита чтения и динамических секретов.
Уровень 3: централизованный менеджер секретов
Vault, OpenBao, Infisical или облачный Secret Manager. Секреты в одном месте, доступ по политикам, журнал каждого чтения, автоматическая ротация, короткоживущие учётные данные к базам и облакам. На этот уровень стоит выходить, когда сервисов больше двух-трёх, есть несколько сред и доступ к проду есть больше чем у одного человека.
Как устроен HashiCorp Vault?
Vault — сервер с HTTP API, который хранит секреты в зашифрованном виде и выдаёт их только аутентифицированным клиентам в рамках политик. Ключевые понятия:
- Storage backend — где физически лежат зашифрованные данные. Для новых установок рекомендуется встроенный Integrated Storage (Raft): не нужен отдельный Consul, кластер собирается из 3 или 5 узлов.
- Seal и unseal. После старта Vault «запечатан»: ключ шифрования данных сам зашифрован мастер-ключом, который разбит по схеме Шамира на несколько частей. Чтобы распечатать хранилище, нужно ввести пороговое число частей, например 3 из 5. В продакшене часто используют auto-unseal через облачный KMS или HSM.
- Secret engines — модули под разные типы секретов:
kvдля статических значений,databaseдля временных пользователей БД,pkiдля выпуска сертификатов,transitдля шифрования «как сервис»,sshдля подписанных SSH-сертификатов. - Auth methods — как клиент доказывает, кто он: AppRole для сервисов, Kubernetes для подов, JWT/OIDC для CI и людей, LDAP, userpass.
- Policies — правила на языке HCL: какие пути и с какими действиями (
read,create,update,list,delete) доступны. - Tokens — результат аутентификации. У токена есть TTL, после которого он умирает, и набор политик.
- Audit devices — журнал каждого запроса. Значения секретов в нём хешируются HMAC, поэтому лог можно отправлять в общую систему логирования.
Важная оговорка про лицензию: с августа 2023 года Vault распространяется под Business Source License, а не под MPL. Для внутреннего использования это ничего не меняет, но если важна свободная лицензия, есть форк OpenBao под эгидой Linux Foundation — он совместим с Vault по API и командам (бинарник называется bao). Всё, что написано ниже, применимо к обоим.
Как поднять Vault на Linux-сервере?
Пример для Debian или Ubuntu с одним узлом и Integrated Storage. Для боевого кластера узлов должно быть три, но порядок действий тот же. Подробности — в официальной документации Vault.
Шаг 1. Установка из репозитория HashiCorp
wget -O- https://apt.releases.hashicorp.com/gpg | \
sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \
https://apt.releases.hashicorp.com $(lsb_release -cs) main" | \
sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install vault
Пакет создаёт пользователя vault, юнит vault.service и каталог /etc/vault.d/.
Шаг 2. Конфигурация
# /etc/vault.d/vault.hcl
ui = true
cluster_addr = "https://10.0.0.10:8201"
api_addr = "https://vault.example.internal:8200"
storage "raft" {
path = "/opt/vault/data"
node_id = "vault-1"
}
listener "tcp" {
address = "0.0.0.0:8200"
tls_cert_file = "/opt/vault/tls/vault.crt"
tls_key_file = "/opt/vault/tls/vault.key"
}
Никогда не включайте tls_disable = 1 за пределами локальной машины: токены и секреты пойдут по сети открытым текстом. Порт 8200 закройте файрволом и оставьте доступ только из внутренней сети — базовые правила разобраны в статье о защите Linux-сервера.
Шаг 3. Инициализация и распечатывание
sudo systemctl enable --now vault
export VAULT_ADDR=https://vault.example.internal:8200
vault operator init -key-shares=5 -key-threshold=3
vault operator unseal # повторить трижды с разными ключами
vault status
Команда init выведет пять ключей распечатывания и root-токен. Это самый ценный вывод во всей инфраструктуре: раздайте ключи разным людям, по одному на человека, а не сохраняйте все пять в одном файле рядом с сервером. Root-токен используйте для первичной настройки и затем отзовите командой vault token revoke; при необходимости новый генерируется через vault operator generate-root тем же кворумом ключей.
Шаг 4. Хранилище секретов, политика и аудит
vault audit enable file file_path=/var/log/vault/audit.log
vault secrets enable -path=secret kv-v2
vault kv put -mount=secret myapp/prod db_password=@db_password.txt
cat > myapp-prod.hcl <<'HCL'
path "secret/data/myapp/prod" {
capabilities = ["read"]
}
HCL
vault policy write myapp-prod myapp-prod.hcl
Обратите внимание на путь в политике: у KV версии 2 данные лежат под secret/data/..., а метаданные — под secret/metadata/.... Политика на secret/myapp/* без data/ не сработает, и это самая частая причина «permission denied» у новичков.
Значение, набранное прямо в команде, останется в истории shell. Поэтому в примере оно читается из файла через @; так же можно передать его через stdin (db_password=-) или выставить HISTCONTROL=ignorespace и начинать команду с пробела. Временный файл после этого удалите.
Шаг 5. Аутентификация сервиса через AppRole
vault auth enable approle
vault write auth/approle/role/myapp-prod \
token_policies=myapp-prod token_ttl=1h token_max_ttl=4h \
secret_id_ttl=24h secret_id_num_uses=1
vault read auth/approle/role/myapp-prod/role-id
vault write -f auth/approle/role/myapp-prod/secret-id
Role ID — это «логин», он не секретный и может лежать в конфигурации. Secret ID — «пароль», его доставляют на сервер отдельным каналом (из CI в момент деплоя или через response wrapping) и делают одноразовым и короткоживущим.
Как приложение получает секрет на сервере?
Есть несколько подходов, от простого к гибкому.
Vault Agent и шаблоны
Агент сам проходит аутентификацию, продлевает токен и рендерит файл с секретами. Приложение ничего не знает про Vault и просто читает файл:
# /etc/vault.d/agent.hcl
auto_auth {
method "approle" {
config = {
role_id_file_path = "/etc/myapp/role-id"
secret_id_file_path = "/run/myapp/secret-id"
remove_secret_id_file_after_reading = true
}
}
}
template {
source = "/etc/vault.d/myapp.ctmpl"
destination = "/run/myapp/secrets.env"
perms = "0400"
exec {
command = ["systemctl", "reload", "myapp"]
}
}
Сам шаблон myapp.ctmpl — обычный текст с конструкцией with secret "secret/data/myapp/prod" в двойных фигурных скобках, которая подставляет поля .Data.data.db_password и другие. Каталог /run — это tmpfs: файл не попадёт на диск и исчезнет при перезагрузке. Когда секрет в Vault меняется, агент перерисовывает файл и выполняет команду перезагрузки сервиса.
Credentials в systemd
С версии 247 systemd умеет передавать сервису секреты без переменных окружения. Директива LoadCredential= копирует файл в приватный каталог сервиса, доступный только ему:
# /etc/systemd/system/myapp.service.d/secrets.conf
[Service]
LoadCredential=db_password:/run/myapp/db_password
# в приложении путь к файлу: $CREDENTIALS_DIRECTORY/db_password
Ещё удобнее systemd-creds encrypt (systemd 250+): секрет шифруется ключом хоста или TPM2, зашифрованный файл можно хранить хоть в репозитории конфигурации, а расшифровывается он только на этой машине при старте сервиса через LoadCredentialEncrypted=. Для одного-двух серверов без Vault это лучший вариант. Как устроены юниты и drop-in файлы, мы разбирали в материале про systemd без боли.
Прямой запрос из кода
Приложение само ходит в Vault через SDK (hvac для Python, официальный клиент для Go и т. д.). Это даёт максимум гибкости — динамические секреты, шифрование через transit, — но привязывает код к Vault. Подходит, когда сервис и так глубоко интегрирован с инфраструктурой.
Контейнеры и Kubernetes
В Docker Compose используйте блок secrets: — файл монтируется в /run/secrets/<имя> и не виден в docker inspect, в отличие от environment:. Основы работы с контейнерами — в статье про Docker простыми словами. В Kubernetes объект Secret по умолчанию хранится в etcd лишь в base64, поэтому нужно включить шифрование etcd at rest и ограничить RBAC, а лучше подключить Vault Agent Injector, Vault Secrets Operator или External Secrets Operator. Если вы только решаете, нужен ли кластер, загляните в разбор Kubernetes для небольших команд.
Что такое динамические секреты и зачем ротация?
Статический пароль живёт, пока его не сменят вручную, — то есть годами. Динамический секрет создаётся по запросу и умирает сам. Например, движок database создаёт в PostgreSQL временного пользователя под каждый экземпляр приложения:
vault secrets enable database
vault write database/config/appdb \
plugin_name=postgresql-database-plugin \
connection_url="postgresql://{{username}}:{{password}}@db.internal:5432/app" \
username="vault_admin" password=@vault_admin.txt allowed_roles="myapp"
vault write database/roles/myapp db_name=appdb \
creation_statements=@create_role.sql \
default_ttl=1h max_ttl=24h
vault read database/creds/myapp
В create_role.sql лежит шаблон вида CREATE ROLE "{{name}}" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}' плюс нужные GRANT. Что это даёт на практике:
- утёкший пароль перестаёт работать через час без вашего участия;
- у каждого экземпляра свой пользователь — в
pg_stat_activityи логах видно, какой именно сервис выполнил запрос; - отозвать доступ одного скомпрометированного сервиса можно, не трогая остальные (
vault lease revoke); - пароль администратора, которым Vault создаёт пользователей, можно сменить командой
vault write -f database/rotate-root/appdb, после чего его не знает ни один человек.
Для статических ключей, которые нельзя сделать динамическими (API внешних сервисов), заведите расписание ротации: критичные — раз в 90 дней, остальные — раз в год, и обязательно при уходе сотрудника с доступом. Без расписания ротация не происходит никогда. Как настроить саму базу под нагрузку, мы разбирали в статье про тюнинг PostgreSQL.
Как безопасно передавать секреты в CI/CD?
Пайплайн — одно из самых уязвимых мест: у него есть доступ к проду, а запускается он на коде из каждой ветки. Правила:
- Masked и protected переменные. В GitLab CI секрет помечается как masked (не выводится в лог) и protected (доступен только в защищённых ветках). Тогда пайплайн из feature-ветки не получит ключ от прода.
- Аутентификация по OIDC вместо постоянного токена. GitLab выдаёт каждой джобе подписанный JWT (
id_tokens), Vault проверяет его через методjwtи выдаёт токен на минуты с политикой, привязанной к проекту и ветке. Постоянного секрета для Vault в CI нет вообще. - Никаких
set -xиechoс секретами. Маскировка работает по точному совпадению строки: base64 от секрета или его часть в лог попадут. - Отдельные учётки для деплоя с минимальными правами, а не личный ключ администратора.
- Сканирование в пайплайне: gitleaks на каждый merge request не пускает новый ключ в основную ветку.
Базовую настройку пайплайна с нуля мы разбирали в статье про GitLab CI, а работу с ключами доступа к серверам — в материале про SSH-ключи вместо пароля.
Какие ошибки с секретами встречаются чаще всего?
- Удалить коммит вместо ротации. Ботам, которые сканируют публичные репозитории, хватает нескольких минут после пуша. Утёкший ключ меняется всегда.
- Один секрет на все среды. Пароль от продовой базы совпадает со стейджинговым, а доступ к стейджингу шире. Разделяйте пути и политики:
secret/myapp/dev,secret/myapp/prod. - Политика «на всё».
path "secret/*"с правом чтения у каждого сервиса превращает компрометацию одного в компрометацию всех. - Root-токен в работе. Его используют для повседневных задач, кладут в CI и забывают отозвать.
- Все ключи распечатывания в одном месте. Пять ключей Шамира в одном текстовом файле на том же сервере сводят защиту к нулю.
- Нет бэкапа Vault. Пропал узел без снапшотов Raft (
vault operator raft snapshot save) — пропали все секреты и доступ ко всему. Бэкап должен быть зашифрован и проверен восстановлением; подход описан в статье о стратегии 3-2-1. - Бесконечные TTL. Токены и динамические секреты с TTL в год — это те же статические пароли, только сложнее устроенные.
- Секрет в аргументах и URL.
curl https://api.example.com/...?token=...оседает в логах nginx, прокси и истории браузера. - Логирование конфигурации целиком. «Для отладки» приложение печатает все настройки при старте. Секреты в коде стоит оборачивать в тип, который не сериализуется в лог.
- Нет инвентаря. Никто не знает, какие ключи существуют, где используются и кто их выпустил, поэтому при увольнении администратора непонятно, что ротировать.
Что делать, если секрет утёк?
Порядок действий важен: сначала закрыть доступ, потом разбираться.
- Отозвать и выпустить новый секрет. Сменить пароль, отозвать токен, перевыпустить ключ. Если приложение читает секрет из Vault или файла, обновление занимает минуты.
- Проверить, использовался ли он. Журналы доступа провайдера, аудит Vault, логи базы данных и облака за период с момента утечки.
- Оценить радиус поражения. Что ещё было доступно с этими правами: соседние базы, бакеты, другие ключи, сохранённые рядом.
- Убрать следы. Переписать историю git, удалить артефакты CI и старые образы, почистить сообщения в чатах.
- Устранить причину. Добавить сканер, сузить права, перевести секрет в менеджер — чтобы та же ошибка не повторилась через месяц.
Чем заменить Vault, если он избыточен?
- OpenBao — свободный форк Vault, если важна лицензия MPL; команды и API совместимы.
- systemd-creds — для одного-двух серверов: секреты шифруются ключом хоста или TPM2, отдельный сервис не нужен.
- SOPS + age — для GitOps и Ansible: шифрованные секреты в репозитории рядом с конфигурацией (про сам Ansible — в статье о первых playbook).
- Infisical — менеджер секретов с веб-интерфейсом и удобной интеграцией для разработчиков, можно развернуть у себя.
- Облачные менеджеры секретов — если инфраструктура и так живёт у одного облачного провайдера.
Главное — не инструмент, а принципы: секрет хранится в одном месте, выдаётся по минимальным правам, живёт недолго, каждое чтение записывается, а замена занимает минуты, а не выходные.
Чек-лист: секреты в проде
- В git нет ни одного секрета — проверено gitleaks по всей истории, стоит pre-commit хук.
- Образы контейнеров собираются без секретов, в Compose используются
secrets:, а неenvironment:. - Секреты на сервере лежат на tmpfs или в
$CREDENTIALS_DIRECTORY, права0400, владелец — пользователь сервиса. - У каждого сервиса и среды своя политика доступа, без масок «на всё».
- Root-токен отозван, ключи распечатывания разнесены по людям, есть проверенный бэкап хранилища.
- Включён аудит обращений к секретам.
- Пароли к базам динамические или ротируются по расписанию.
- CI получает доступ по OIDC/JWT с коротким TTL, секреты masked и protected.
- Есть инвентарь секретов и записанный порядок действий при утечке.
Частые вопросы
Можно ли хранить секреты в переменных окружения?
Можно, если это осознанный компромисс: окружение видно в /proc/PID/environ, наследуется дочерними процессами и попадает в docker inspect и дампы. Для критичных секретов надёжнее файл на tmpfs с правами 0400, systemd credentials или прямой запрос к менеджеру секретов.
Нужен ли Vault для одного сервера?
Обычно нет. Для одного-двух серверов хватит systemd-creds или SOPS с age и строгих прав на файлы. Vault окупается, когда сервисов и сред несколько, доступ к проду есть у нескольких людей и нужны аудит и динамические секреты.
Чем Vault отличается от OpenBao?
OpenBao — форк Vault, созданный после перехода Vault на Business Source License в 2023 году. Он развивается под эгидой Linux Foundation, распространяется под MPL 2.0 и совместим с Vault по API и большинству команд. Для базовых сценариев они взаимозаменяемы.
Что делать, если пароль попал в git?
Сразу сменить пароль или отозвать ключ — это главное. Затем проверить журналы на использование и только после этого переписывать историю через git filter-repo и удалять кэши CI. Удаление коммита без ротации не защищает: ключ уже могли скопировать.
Как часто нужно ротировать секреты?
Динамические секреты баз данных — с TTL от часа до суток. Статические ключи внешних API — раз в 90 дней для критичных и раз в год для остальных. Внеплановая ротация обязательна при утечке, увольнении сотрудника с доступом и смене подрядчика.
Безопасны ли Kubernetes Secrets?
По умолчанию нет: значения хранятся в etcd в base64, а это кодирование, а не шифрование. Нужно включить шифрование etcd at rest, ограничить доступ к Secret через RBAC и по возможности подключить внешний менеджер через Vault Secrets Operator или External Secrets Operator.
Как передать секреты новой команде или подрядчику?
Не архивом и не в мессенджере. Заведите подрядчику отдельные учётные записи с минимальными правами в менеджере секретов, а после завершения работ отзовите их и ротируйте всё, к чему у него был доступ. Общий root-ключ «на всех» не выдавайте никогда.