SSD Nodes Learn 🎉 VPS от $5.50/мес
Руководства Matt ConnorАвтор: Matt Connor

Выбор self-hosted менеджера секретов для одного сервера

Сравнение OpenBao, Infisical, SOPS и системных механизмов для хранения секретов на одном VPS. Узнайте, какой инструмент подходит для автоматизации процессов без лишних затрат.

Чем self-hosted менеджер секретов отличается от менеджера паролей

Self-hosted менеджер секретов передает учетные данные процессам. Менеджер паролей передает их людям. Все остальные различия вытекают из этого факта. Менеджер паролей разблокируется человеком, который присутствует и выполняет действие. Менеджер секретов должен предоставить приложению пароль от базы данных в 03:00, когда все спят.

Режимы отказа также существенно различаются. Заблокированный менеджер паролей — это неудобство: вы просто повторно вводите мастер-пароль. Заблокированный (sealed) менеджер секретов — это авария: каждый сервис, который перезапускается в этот момент, запускается без учетных данных и остается неработоспособным. Запуск Vaultwarden в качестве собственного менеджера паролей хорошо решает проблему доступа для людей. Он не решает задачу автоматизации для машин, так как изначально для этого не предназначался.

Реалистичные варианты для одного сервера делятся на две группы. OpenBao и Infisical — это полноценные сервисы: API, база данных, TLS (transport layer security), этап авторизации и процесс, который теперь необходимо поддерживать в рабочем состоянии. SOPS с использованием age, systemd credentials и Docker secrets — это файловые решения: данные зашифрованы в покое, расшифровываются уже запущенным процессом, и не требуют дополнительного мониторинга.

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

Достаточно ли прав доступа 600 для файла env?

Чаще всего — да. Это защищает от ситуации, когда другой пользователь на сервере пытается прочитать пароль от базы данных. Права доступа Unix справляются с этой задачей, причем еще до того, как поднимется сеть.

sudo install -d -m 750 -o root -g myapp /etc/myapp
sudo install -m 640 -o root -g myapp /dev/null /etc/myapp/env
sudoedit /etc/myapp/env

Проверьте это с обеих сторон:

sudo -u myapp cat /etc/myapp/env
sudo -u nobody cat /etc/myapp/env

Первая команда выводит содержимое файла. Вторая выводит cat: /etc/myapp/env: Permission denied, так как nobody не входит в группу myapp, а у файла отсутствуют права для остальных пользователей. Это и есть вся модель безопасности, и она вполне реальна.

Утечка происходит на следующем этапе. Юнит systemd с параметром EnvironmentFile= копирует эти значения в переменные окружения процесса, а переменные окружения процесса доступны для чтения.

[Service]
User=myapp
EnvironmentFile=/etc/myapp/env
ExecStart=/usr/local/bin/myapp
sudo cat /proc/$(pgrep -n myapp)/environ | tr '\0' '\n'

Эта команда выводит ваши секреты в открытом виде, так как файл /proc/<pid>/environ доступен для чтения пользователю root и пользователю, от имени которого запущен процесс. Инструмент для сбора отчетов об ошибках, который прикрепляет окружение к отчету, увидит то же самое. Как и любая другая утилита, запущенная от той же учетной записи. Именно поэтому защита секретов от AI-агентов начинается с их удаления из переменных окружения. Используйте этот файл в связке с отдельным системным пользователем с низкими привилегиями, чтобы «пользователь, от имени которого запущен процесс» не был root.

SOPS и age: зашифрованные секреты в репозитории git

SOPS (secrets operations) шифрует значения в файлах YAML или JSON, оставляя ключи в открытом виде. age — это компактная утилита для шифрования, которая использует одну пару ключей и не требует сервера ключей. Вместе они позволяют хранить secrets.enc.yaml рядом с кодом, а git diff по-прежнему показывает, какая настройка изменилась, не раскрывая её нового значения.

age входит в состав Ubuntu 24.04. SOPS в репозиториях отсутствует, поэтому скачайте .deb со страницы релизов. Версия 3.13.3 была актуальна на август 2026 года.

sudo apt update && sudo apt install -y age
curl -LO https://github.com/getsops/sops/releases/download/v3.13.3/sops_3.13.3_amd64.deb
sudo apt install -y ./sops_3.13.3_amd64.deb
sops --version

Создайте пару ключей. age-keygen запишет закрытый ключ в файл и выведет открытый ключ, поэтому вы увидите строку, начинающуюся с Public key: age1....

mkdir -p ~/.config/sops/age
age-keygen -o ~/.config/sops/age/keys.txt
chmod 600 ~/.config/sops/age/keys.txt
age-keygen -y ~/.config/sops/age/keys.txt

Сохраните открытый ключ в .sops.yaml в корне репозитория, чтобы не указывать получателя в командной строке каждый раз.

creation_rules:
  - age: age1s3cqcks5genc6ru8chl0hkkd04zmxvczsvdxq99ekffe4gmvjpzsedk23c
sops encrypt secrets.yaml > secrets.enc.yaml
sops decrypt secrets.enc.yaml

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

Во время выполнения передавайте значения только одному процессу:

sops exec-env secrets.enc.yaml './myapp'

sops exec-env выполняет расшифровку в оперативной памяти и устанавливает значения в окружении дочернего процесса, поэтому открытый текст не записывается на диск. Ограничение окружения из предыдущего раздела по-прежнему применимо к этому дочернему процессу.

Две проблемы часто вызывают трудности. Ошибка Failed to get the data key required to decrypt the SOPS file при работе под управлением systemd почти всегда означает, что SOPS искал ключ не в той домашней директории, так как юнит не наследует вашу переменную HOME. Явно укажите путь с помощью Environment=SOPS_AGE_KEY_FILE=/etc/sops/age.txt в юните. Кроме того, редактирование .sops.yaml не перешифровывает уже существующие данные: добавление открытого ключа коллеги влияет только на новые файлы, поэтому для каждого существующего файла выполните sops updatekeys secrets.enc.yaml. Если ваша конфигурация уже работает через Ansible, шифрование тех же значений с помощью Ansible Vault позволяет достичь того же результата без использования дополнительного инструмента.

Учетные данные systemd: секреты, не попадающие в переменные окружения

В Ubuntu 24.04 поставляется systemd 255, поэтому дополнительная установка не требуется. systemd-creds шифрует секрет на хосте, а systemd расшифровывает его в приватный каталог, доступный для чтения только конкретной службе.

sudo systemd-creds setup
sudo install -d -m 700 /etc/myapp
echo -n 'hunter2' | sudo systemd-creds encrypt --name=db_password - /etc/myapp/db_password.cred
[Service]
User=myapp
LoadCredentialEncrypted=db_password:/etc/myapp/db_password.cred
ExecStart=/usr/local/bin/myapp

Служба считывает значение из файла с именем db_password, расположенного в каталоге, который задается переменной $CREDENTIALS_DIRECTORY. Значение не попадает в переменные окружения, поэтому команда /proc/<pid>/environ не выводит ничего полезного, а открытый текст никогда не записывается на корневую файловую систему.

Перед тем как указывать путь к файлу в юните, убедитесь, что он успешно расшифровывается:

sudo systemd-creds decrypt /etc/myapp/db_password.cred -

Важно знать, какой ключ использовался для шифрования, так как от этого зависит пригодность вашей резервной копии. По умолчанию --with-key=auto использует чип TPM2 (trusted platform module version 2), если он присутствует и доступен, в противном случае используется ключ хоста. На большинстве VPS-инстансов чип TPM2 отсутствует.

systemd-analyze has-tpm2

no означает, что был использован ключ хоста. Этот ключ находится в /var/lib/systemd/credential.secret и доступен для чтения только пользователю root. Если восстановить db_password.cred на новый VPS без этого файла, расшифровать данные будет невозможно. Скопируйте credential.secret в ту же резервную копию или сохраните открытый текст в доступном вам месте.

Docker secrets: файлы в /run/secrets

Compose считывает файл с хоста и монтирует его в контейнер по пути /run/secrets/<name>.

services:
  app:
    image: myapp:latest
    environment:
      DB_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
secrets:
  db_password:
    file: ./db_password.txt

cat /run/secrets/db_password

docker compose exec app cat /run/secrets/db_password
docker compose exec app env | grep -i password

env | grep DB_PASSWORD_FILE=/run/secrets/db_password

Первая команда выводит секрет. Вторая выводит только DB_PASSWORD_FILE=/run/secrets/db_password, в этом и заключается смысл: значение переменной никогда не попадает в окружение контейнера, поэтому оно не отображается в выводе docker inspect. Многие официальные образы уже поддерживают такой формат, и образ Postgres считывает POSTGRES_PASSWORD_FILE именно таким способом.

Важно понимать суть этого механизма. Вне режима Swarm шифрование на каком-либо уровне отсутствует: ./db_password.txt — это обычный текстовый файл на хосте, и его единственная защита заключается в правах доступа и владельце. Устанавливайте их самостоятельно, так как Compose без предупреждений смонтирует файл, доступный для чтения всем пользователям. Подробное сравнение этого метода с использованием обычных файлов env_file приведено в руководстве по env-файлам и секретам в Compose.

Реальная стоимость эксплуатации OpenBao и Vault

OpenBao — это форк HashiCorp Vault от Linux Foundation, созданный после того, как HashiCorp перевела Vault на лицензию Business Source License в 2023 году. OpenBao распространяется под лицензией MPL 2.0 (Mozilla Public License). Версия 2.6.2 была актуальной по состоянию на август 2026 года. Почти всё нижесказанное применимо и к Vault, так как форк сохранил тот же набор команд.

docker pull docker.io/openbao/openbao

Пакеты для Debian и Ubuntu доступны на странице загрузки OpenBao, если вы предпочитаете управлять обновлениями через apt. Серверу требуется конфигурационный файл, содержащий настройки listener и storage backend:

listener "tcp" {
  address       = "127.0.0.1:8200"
  tls_cert_file = "/path/to/full-chain.pem"
  tls_key_file  = "/path/to/private-key.pem"
}

storage "raft" {
  path = "/path/to/raft/data"
  node_id = "raft_node_1"
}

Затем запустите его один раз:

bao operator init

По умолчанию система разделяет root key на 5 частей и требует 3 из них для снятия блокировки (unseal), что определяется флагами -key-shares и -key-threshold. Система выводит эти части и начальный root token только один раз, после чего они не отображаются.

Теперь о том, что пропускают большинство сравнений. Перезагруженный сервер — это заблокированный сервер. OpenBao хранит root key только в оперативной памяти, поэтому после перезапуска он не может расшифровать собственное хранилище, пока кто-либо не предоставит пороговое количество частей ключа. Таким образом, обновление ядра или завершение процесса по OOM (out of memory) приводит к блокировке сервера и невозможности входа для приложений.

На VPS, обслуживаемом одним человеком, разделение по методу Шамира ничего не защищает, так как все пять частей оказываются в одном менеджере паролей, принадлежащем одному и тому же лицу. Функция auto unseal переносит ключ на доверенное устройство или сервис. В крупном облаке это означает использование управляемого сервиса ключей, а на вашем VPS — обычно файл ключа, лежащий на том же диске, что и защищаемые им данные. Это реальное снижение уровня безопасности в обмен на то, что сервер автоматически восстанавливает работу после перезагрузки. Идите на этот компромисс осознанно и зафиксируйте, какой вариант вы выбрали.

Infisical: веб-интерфейс, база данных и мастер-ключ, который остается у вас

Infisical — это платформа для управления секретами с веб-интерфейсом, поддержкой проектов, сред и разграничением доступа пользователей. Развертывание с помощью Compose выполняется быстро:

curl -o docker-compose.prod.yml https://raw.githubusercontent.com/Infisical/infisical/main/docker-compose.prod.yml
curl -o .env https://raw.githubusercontent.com/Infisical/infisical/main/.env.example
docker compose -f docker-compose.prod.yml up -d

Перед выполнением последней команды отредактируйте .env. Два значения должны быть уникальными, а одно из них нельзя изменять после запуска:

openssl rand -hex 16
openssl rand -base64 32

Первое — ENCRYPTION_KEY, 16-байтовая шестнадцатеричная строка. Это ключ, которым шифруются секреты внутри PostgreSQL. Его потеря превратит резервную копию базы данных в бесполезный набор зашифрованных данных, а изменение на работающем экземпляре сделает невозможной расшифровку существующих секретов. Второе — AUTH_SECRET, 32-байтовая строка в формате base64, используемая для сессий. SITE_URL должен быть абсолютным URL, по которому вы будете обращаться к сервису (включая протокол), иначе редирект при входе в систему работать не будет.

Infisical подходит лучше, чем OpenBao, если вам в первую очередь нужны люди: веб-интерфейс для небольшой команды и разделение сред, а не автоматическая ротация учетных данных базы данных. Взамен вы берете на себя обслуживание PostgreSQL, Redis и TLS-сертификата, которые теперь нужно обновлять и резервировать самостоятельно.

Что происходит, когда сервис секретов недоступен, а ваше приложение перезапускается

Этот вопрос определяет, стоит ли размещать сервис секретов на одном узле с приложением. Файлы доступны для чтения еще до запуска сети, а сервис — нет.

При перезагрузке сервера ваше приложение и OpenBao запускаются одновременно. Приложение запрашивает пароль от базы данных, но OpenBao еще заблокирован (sealed), запрос завершается ошибкой, и systemd начинает перезапускать приложение в цикле, пока администратор вручную не введет ключи для разблокировки. Ничего не сломано, но и ничего не работает.

Существует два честных способа решения этой проблемы. Первый: настроить порядок запуска юнитов и добавить логику повторных попыток в приложение: After= сервис секретов, а также Restart=on-failure и RestartSec= с интервалом, достаточным для того, чтобы не перегружать API запросами. Второй: получать секреты во время развертывания, а не при загрузке системы. В этом случае секрет записывается в файл с правами 600 или передается через systemd credentials, чтобы работающая система зависела от файла, а не от API.

Истечение срока действия токена — это та же проблема, но растянутая во времени. Токены и лизы OpenBao имеют время жизни (TTL), поэтому долго работающий процесс, который не обновляет токен, теряет доступ к данным в момент, не связанный с развертыванием. Такой сбой сбивает с толку именно потому, что в этот день в конфигурации ничего не менялось.

Резервное копирование самого хранилища

У каждого варианта здесь есть ключ, и резервная копия без этого ключа бесполезна. Запишите, где хранится ваш ключ.

Для файла env сам файл является секретом, поэтому резервная копия должна быть зашифрована. В случае с SOPS зашифрованный файл можно разместить в любом публичном месте, а закрытый ключ age в ~/.config/sops/age/keys.txt — это то, что нельзя терять. Для учетных данных systemd создавайте резервную копию /var/lib/systemd/credential.secret вместе с файлами .cred. Для Infisical сделайте дамп PostgreSQL и храните ENCRYPTION_KEY отдельно от него.

OpenBao с хранилищем raft создает собственные снимки состояния:

bao operator raft snapshot save backup.snap
bao operator raft snapshot restore backup.snap

Снимок содержит ваше зашифрованное хранилище, поэтому для восстановления на новом сервере всё равно потребуются доли ключа для снятия защиты (unseal shares) из bao operator init. Ежедневное задание, которое копирует снимки в объектное хранилище, в то время как доли ключа нигде не хранятся — это резервная копия, которая ни от чего не защищает. Протестируйте восстановление на временном VPS, прежде чем полагаться на него.

Аудит: кто прочитал секрет

Файлы не обеспечивают контрольный журнал. Права доступа и владелец показывают лишь то, кто мог прочитать секрет. Они не сообщают, кто это сделал на самом деле. auditd с отслеживанием пути — ближайшая альтернатива, но этот инструмент сообщает лишь о факте открытия файла, а не о том, какое значение было использовано.

OpenBao регистрирует каждый запрос в устройстве аудита, которое вы активируете явно:

bao audit enable file file_path=/var/log/openbao_audit.log

Два факта об этих логах влияют на эксплуатацию сервера. Большинство строк в запросах и ответах хешируются с помощью HMAC-SHA256 и соли, поэтому вы можете сопоставить известное вам значение с записью в логе, не раскрывая сам секрет в открытом виде. Целые числа и логические значения записываются в открытом виде, поэтому числовые секреты не защищены таким хешированием.

Важный эксплуатационный нюанс: OpenBao перестает отвечать на запросы, если ни одно из включенных устройств аудита не может их записать. Если устройство переходит в блокирующий режим при сбое, запросы зависают до устранения проблемы. Переполнение диска в /var/log по замыслу разработчиков приводит к остановке API секретов. Выделите для логов аудита отдельное место и настройте правило logrotate в первый же день, а не после первого сбоя.

Какой менеджер секретов для self-hosted решений выбрать?

Оцените количество машин и пользователей, затем сделайте выбор.

  1. Одна машина, один пользователь: используйте файл с правами 600, принадлежащий root и доступный для чтения пользователю сервиса. Добавьте systemd credentials, если необходимо исключить значение из переменных окружения процесса.
  2. Одна машина, от двух до пяти пользователей, конфигурация уже в git: используйте SOPS с age. У каждого пользователя должна быть своя пара ключей, а в .sops.yaml перечисляются все публичные ключи, которым разрешена расшифровка.
  3. Несколько машин, один репозиторий конфигурации, нет необходимости в учетных данных с ограниченным сроком действия: по-прежнему SOPS с age, с одним ключом получателя на каждый хост. В этом случае скомпрометированный ключ хоста позволит расшифровать только файлы этого конкретного хоста.
  4. Несколько машин и несколько команд, которым действительно нужны учетные данные базы данных с ограниченным временем жизни, а также журнал аудита, который кто-то просматривает: используйте OpenBao и заложите в бюджет один час работы оператора в месяц на процедуры разблокировки (unsealing) и тренировки по восстановлению.

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

FAQ

Стоит ли использовать self-hosted менеджер секретов для одного VPS?

Обычно нет, если речь идет о таких сервисах, как OpenBao или Infisical. На одном сервере с одним или двумя пользователями файл с правами 600 или зашифрованные учетные данные systemd обеспечивают ту же защиту от других локальных пользователей, не требуя процедуры unseal и обслуживания дополнительного сервиса. Сервис управления секретами становится оправданным, когда у вас появляется несколько машин, несколько пользователей или реальная необходимость в учетных данных, которые истекают без ручного обновления.

В чем разница между менеджером паролей и менеджером секретов?

Менеджер паролей хранит учетные данные, которые вводит человек, и разблокируется пользователем в его присутствии. Менеджер секретов предоставляет учетные данные процессам, поэтому он должен работать в 03:00 без участия человека. Из этого вытекает главное различие: заблокированный менеджер паролей требует ввода мастер-пароля, а заблокированный (sealed) менеджер секретов останавливает все сервисы, которые пытаются перезапуститься в этот момент.

Что произойдет с моими приложениями, если OpenBao заблокируется после перезагрузки?

Они не смогут получить свои секреты и не запустятся, а systemd будет перезапускать их в цикле, пока кто-то не предоставит порог для разблокировки (по умолчанию это 3 из 5 ключей). OpenBao хранит корневой ключ только в оперативной памяти, поэтому каждая перезагрузка снова блокирует сервис. Либо включите auto unseal, смирившись с тем, что на одном VPS ключ разблокировки окажется на том же диске, что и данные, либо записывайте секреты в файл во время развертывания, чтобы загрузка системы не зависела от API.

Можно ли коммитить зашифрованные через SOPS файлы в публичный репозиторий?

Значения зашифрованы, поэтому они в безопасности от любого, у кого нет приватного ключа age. Сами ключи не зашифрованы: читатель может увидеть, что у вас есть STRIPE_SECRET_KEY и SMTP_PASSWORD, а также как часто каждый из них меняется. Эти метаданные приемлемы для большинства проектов, но недопустимы для некоторых. Храните приватный ключ age вне репозитория и запускайте sops updatekeys для каждого существующего файла при добавлении или удалении получателя.