
Где хранить пароли, токены и SSH-ключи на сервере
Отделяем секреты от кода, создаём защищённый env-файл для systemd, разделяем ключи людей и служб и готовим безопасную замену.
Содержание
Пароль базы данных, токен внешнего API и закрытая часть SSH-ключа дают программе или человеку дополнительные права. Такие значения называют секретами. Их нельзя хранить рядом с исходным кодом по одной простой причине: код приходится копировать, просматривать, собирать и архивировать гораздо чаще, чем выдавать доступ к рабочей системе.
Если положить .env в репозиторий, его получит каждый клон проекта, система непрерывной сборки и резервная копия Git. Если поместить файл в публичный каталог сайта, веб-сервер при ошибочной настройке может отдать его посетителю. Если передать токен параметром команды, он может остаться в истории терминала или списке процессов.
Для небольшого приложения на одном VPS не обязательно начинать с отдельного сервиса управления секретами. Достаточно отделить значения от кода, создать закрытый системный файл, разрешить чтение только нужной службе и заранее знать, как заменить каждое значение.
Определите, что именно нужно защищать
Не вся конфигурация секретна. Адрес сайта, номер внутреннего порта и имя базы обычно можно хранить в коде или обычном конфигурационном файле. Пароль к базе, приватный ключ и токен с правом отправлять запросы от имени проекта — нельзя.
Составьте перечень без записи самих значений:
| Что используется | Кому нужно | Где хранится | Где отозвать или заменить |
|---|---|---|---|
| пароль пользователя базы | приложению | закрытый файл на VPS | в самой базе данных |
| токен платёжного или почтового API | приложению | закрытый файл на VPS | в панели сервиса |
| личный SSH-ключ | одному администратору | на его компьютере | в authorized_keys сервера |
| ключ автоматической публикации | одной задаче деплоя | в защищённом хранилище CI | на сервере или в настройках репозитория |
У каждого секрета должно быть одно понятное назначение. Общий токен для ноутбука разработчика, CI и рабочего сервера неудобен: при утечке нельзя отключить одно место, не остановив остальные.
Не отправляйте рабочие значения в Git
В репозитории полезно хранить пример конфигурации с именами переменных, но без значений. Например, файл .env.example может выглядеть так:
DATABASE_URL=
MAIL_API_TOKEN=
Так разработчик видит, какие параметры нужны приложению, но не получает доступ к рабочей базе и почтовому сервису.
Реальные локальные файлы добавьте в .gitignore:
.env
.env.*
!.env.example
secrets/
*.key
Первые две строки исключают файлы окружения, третья возвращает безопасный пример, а последние две закрывают распространённые каталоги и расширения ключей. Это защита только от будущего случайного добавления. .gitignore не действует на файл, который Git уже отслеживает, и не удаляет старые коммиты.
Проверьте имена отслеживаемых файлов, не печатая их содержимое:
git ls-files | grep -E '(^|/)(\.env|secrets/)|\.(key|pem)$'
Пустой вывод означает, что по этим распространённым именам совпадений нет. Совпадение ещё не доказывает утечку — .pem может содержать открытый сертификат, — но каждый найденный файл нужно проверить локально и не вставлять его содержимое в публичный тикет.
Если секрет уже попал в репозиторий
Удаление файла новым коммитом не делает старое значение безопасным. Оно остаётся в истории Git и уже могло попасть в чужой клон или журнал сборки. Действуйте в таком порядке:
- Отзовите токен, смените пароль или удалите доверие к ключу.
- Создайте новое значение с минимально необходимыми правами.
- Передайте его приложению отдельным защищённым способом.
- Проверьте журналы использования старого значения.
- Только после этого решайте, нужно ли переписывать историю Git и очищать кеши CI.
Переписывание истории уменьшает вероятность дальнейшего копирования, но не отменяет уже сделанные копии. Поэтому ротация — обязательное действие, а очистка истории — последующее.
Создайте закрытый файл для приложения
Предположим, приложение работает от отдельного пользователя myapp. Проверьте это имя в unit-файле systemd; не подставляйте его в команды вслепую. Создайте каталог и пустой файл так, чтобы владелец root мог изменять содержимое, а группа приложения — только читать:
sudo install -d -o root -g myapp -m 750 /etc/myapp
sudo install -o root -g myapp -m 640 /dev/null /etc/myapp/app.env
Первая команда создаёт /etc/myapp с правами 750: владелец может читать, изменять и открывать каталог, группа — читать и открывать, остальные не получают доступа. Вторая создаёт пустой app.env с правами 640: root читает и изменяет его, группа myapp только читает.
Откройте файл безопасным редактором от имени администратора:
sudoedit /etc/myapp/app.env
Добавьте пары ИМЯ=значение, которые ожидает программа. Не ставьте пробелы вокруг знака =, если документация приложения не говорит обратного. Формат EnvironmentFile похож на shell, но это не полноценный shell-скрипт; сложные значения с кавычками и обратными слешами нужно сверять с документацией systemd.
После сохранения проверьте только путь, владельца и права:
sudo namei -l /etc/myapp/app.env
sudo -u myapp test -r /etc/myapp/app.env && echo 'служба может прочитать файл'
namei -l показывает права каждого каталога на пути, не раскрывая значение. Вторая команда выполняет проверку от имени службы. Если подтверждение не появилось, исправьте владельца или группу; не выдавайте чтение всем пользователям через chmod 644.
Подключите файл к службе systemd
Откройте unit-файл приложения или его override и добавьте путь в секцию [Service]:
[Service]
User=myapp
Group=myapp
EnvironmentFile=/etc/myapp/app.env
ExecStart=/opt/myapp/current/bin/server
User и Group задают учётную запись процесса. EnvironmentFile загружает пары из закрытого файла при запуске. ExecStart приведён для формы примера: в действующем unit нужно оставить реальный путь запуска приложения.
Попросите systemd перечитать unit-файлы, перезапустите службу и покажите её состояние:
sudo systemctl daemon-reload
sudo systemctl restart myapp
sudo systemctl status myapp --no-pager
Первая команда нужна после изменения unit, вторая создаёт новый процесс с новыми переменными, третья показывает результат запуска. Ожидается active (running). При ошибке смотрите журнал службы, но не добавляйте в диагностический вывод env, printenv или содержимое app.env.
Переменная окружения удобна, но не является непроницаемым хранилищем. Пользователь root и процессы с достаточными правами могут получить доступ к памяти и окружению службы. Для одного VPS это приемлемая базовая схема; для большого проекта, короткоживущих токенов или строгого разграничения стоит перейти к systemd credentials либо отдельному менеджеру секретов.
Не передавайте секрет в командной строке
Команда вида program --token REAL_VALUE может попасть в историю shell, журнал автоматизации и список процессов. Безопаснее, когда программа читает значение из закрытого файла, стандартного ввода или поддерживаемого ею механизма credentials.
Во время диагностики проверяйте факт наличия и длину, не само значение. Лучше встроить такую проверку в приложение: сообщение MAIL_API_TOKEN is missing полезно, а сообщение с полным токеном создаёт новую утечку.
Не включайте set -x в сценарии, который работает с секретами: этот режим печатает выполняемые команды после подстановки переменных. Если трассировка нужна для соседнего участка, выключите её до чтения токена.
Разделите личные и автоматические SSH-ключи
У каждого администратора должна быть собственная пара SSH-ключей. Тогда доступ конкретного человека удаляется одной строкой из authorized_keys, а действия не зависят от общего файла, разосланного всей команде.
Автоматическая публикация получает другую пару. Такой ключ используют только для одной задачи и по возможности ограничивают отдельным пользователем, одним репозиторием, разрешённой командой или доступом только на чтение. Закрытая часть ключа публикации не должна лежать на личных ноутбуках, если она нужна только CI.
Парольная фраза защищает личный закрытый ключ при краже файла. Для безлюдной автоматизации парольную фразу часто негде вводить, поэтому особенно важны узкие права ключа, защищённое хранилище CI и возможность немедленного отзыва.
Учитывайте копии и журналы
Файл вне Git всё равно может попасть в snapshot VPS или резервный архив. Определите, должна ли копия содержать секреты. Если да, архив шифруют и защищают отдельно. Если нет, после восстановления создают новые значения и вносят их вручную.
Приватный ключ на компьютере администратора защищают шифрованием диска, парольной фразой и блокировкой сеанса. Не копируйте один и тот же ключ на несколько устройств: отдельные ключи проще отозвать после потери ноутбука или телефона.
В журналах оставляйте имя интеграции, время, код ответа и идентификатор запроса. Полный заголовок Authorization, строка подключения к базе и тело приватного ключа для расследования не нужны. Маскирование полезно как страховка, но лучше вообще не передавать значение логгеру.
Заранее опишите замену каждого секрета
Рабочий токен не должен быть вечным только потому, что его трудно заменить. Для каждой строки из перечня запишите последовательность:
- Где создать новое значение.
- Как выдать ему только нужные права.
- Как передать его на VPS, не используя Git и общий чат.
- Как перезапустить потребителя и проверить его функцию.
- Где отозвать старое значение.
- Как подтвердить, что старое значение больше не действует.
Если сервис допускает два одновременно действующих ключа, сначала включите новый, проверьте приложение и только затем удалите старый. Такой порядок позволяет заменить значение без простоя. Если возможен только один ключ, назначьте короткое окно изменения и подготовьте возврат.
Хорошая схема хранения отвечает на четыре вопроса без просмотра самих значений: кто может прочитать секрет, какой процесс его использует, где он отзывается и когда последний раз проверялась замена. После этого можно переходить к аудиту входов на сервер и плану действий при инциденте.
Первоисточники
Самопроверка
Проверьте, что материал усвоен
Ответьте на все вопросы. Результат сохранится только в этом браузере и будет учтён в статистике прочитанных материалов.
Разбираем коротко
Частые вопросы
Можно ли хранить секреты в приватном Git-репозитории?
Не следует. Репозиторий клонируется на компьютеры, попадает в CI и резервные копии, а старые значения остаются в истории. Код и рабочие секреты лучше передавать и хранить отдельно.
Удаление токена из последнего коммита устраняет утечку?
Нет. Уже опубликованное значение нужно сначала отозвать или заменить. Оно могло остаться в истории Git, клонах, журналах сборки и кеше сервисов.
Достаточно ли прав 600 для env-файла?
Права файла важны, но также нужны правильный владелец, отдельная служебная учётная запись, отсутствие значений в логах и командах, а также понятный порядок отзыва и замены.


