Секреты — самая опасная часть инфраструктурного кода. Пароли БД, API-токены, ключи OAuth и вебхуки часто «живут» в .env и YAML, попадают в Git, а затем гуляют по локальным машинам, CI-логам и артефактам. Связка sops + age решает проблему: секреты хранятся в репозитории только в зашифрованном виде, а расшифровка происходит строго там, где это необходимо — при деплое на сервер или в контролируемом окружении разработчика.
Зачем sops и age в GitOps-процессе
GitOps подразумевает «всё как код», включая конфиги продакшна. Но это безопасно лишь до тех пор, пока чувствительные данные никогда не появляются в открытом виде вне доверенных границ. sops шифрует поля в YAML/JSON/ENV-файлах, управляет ключами и прозрачно встраивается в рабочий процесс: файлы остаются в Git, диффы читаемы (метаданные sops), а расшифровка — по требованию. age — современный, простой и криптографически аккуратный инструмент шифрования, который sops поддерживает «из коробки».
Золотое правило: секрет не должен появляться в незашифрованном виде вне целевой машины и краткого контролируемого интервала редактирования.
Базовая архитектура: где хранятся ключи и кто чем владеет
Минимальная модель:
- Публичные получатели (
age-recipients) хранятся в репозитории внутри.sops.yamlи определяют, кто может расшифровать конкретные файлы. - Приватные ключи
ageхранятся только на целевых серверах деплоя и у конкретных разработчиков (по необходимости). В CI/CD — через защищённые хранилища переменных или подключённые секреты. - Каждому окружению (prod, stage) — свой ключ сервера. Это упрощает отзыв доступа и ротацию.
Результат: в Git у нас только зашифрованные секреты плюс политика шифрования. При деплое на сервер эти данные расшифровываются локально с использованием приватного ключа, который никогда не покидает хост. Если у вас собственная машина, удобнее и безопаснее исполнять процесс на VDS с полной изоляцией окружений.

Установка sops и age
На Linux пакеты часто доступны из репозиториев дистрибутива, на macOS — через менеджеры пакетов. Примеры:
# Debian/Ubuntu
sudo apt update
sudo apt install -y age sops
# RHEL/CentOS/AlmaLinux/Rocky
sudo dnf install -y age sops
# Arch
sudo pacman -S --noconfirm age sops
# macOS (Homebrew)
brew install age sops
Проверяем версии:
age --version
sops --version
Генерируем ключи age и наводим порядок
Сгенерируйте приватный ключ для сервера каждого окружения и для нужных участников команды:
# На сервере продакшна
sudo mkdir -p /etc/sops/age
sudo age-keygen -o /etc/sops/age/keys.txt
sudo chmod 600 /etc/sops/age/keys.txt
sudo chown root:root /etc/sops/age/keys.txt
# Посмотреть публичного получателя (recipient)
sudo sed -n 's/^# public key: //p' /etc/sops/age/keys.txt
Полученную публичную строку формата age1... добавьте в политику .sops.yaml. Для локальных разработчиков процедуру повторяйте в домашнем каталоге, например ~/.config/sops/age/keys.txt (с теми же правами доступа 600).
Важно: приватные ключи age по умолчанию не имеют пароля; их защита — это права на файл и шифрование диска. Для повышенных требований применяют аппаратные токены или хранилища секретов. В любом случае приватный ключ сервера не должен покидать машину.
Настраиваем .sops.yaml: правила шифрования
Создайте в корне репозитория файл .sops.yaml и опишите правила для разных окружений и типов файлов. Пример:
creation_rules:
- path_regex: secrets/prod/.*\.(ya?ml|json|env)$
age:
- age1exampleRecipientForProdServer...
- age1exampleRecipientForAdmin1...
encrypted_regex: '^(data|stringData|.+_SECRET|PASSWORD|TOKEN|KEY)$'
- path_regex: secrets/stage/.*\.(ya?ml|json|env)$
age:
- age1exampleRecipientForStageServer...
- age1exampleRecipientForDev...
encrypted_regex: '^(data|stringData|.+_SECRET|PASSWORD|TOKEN|KEY)$'
Разбор:
path_regexопределяет, на какие файлы распространяется правило.age— список получателей, у кого есть право расшифровать.encrypted_regexпозволяет шифровать только нужные поля. В .env всё содержимое файла шифруется целиком, а в YAML/JSON — только совпадающие ключи.
Шифруем .env, YAML и JSON
Добавьте файлы секретов в нужные директории и зашифруйте. Примеры:
# .env для продакшна
mkdir -p secrets/prod
printf 'DB_PASSWORD=strongpass\nAPI_TOKEN=abcd1234\n' > secrets/prod/app.env
sops -e -i secrets/prod/app.env
# YAML (например, конфиг приложения)
cat > secrets/prod/config.yaml << 'YAML'
api:
url: api.internal
token: abcd1234
bb:
host: db.internal
user: app
password: strongpass
YAML
sops -e -i secrets/prod/config.yaml
# JSON
printf '{"password":"strongpass","token":"abcd1234"}\n' > secrets/prod/config.json
sops -e -i secrets/prod/config.json
Редактирование шифрованного файла выполняйте через sops — он прозрачно расшифрует во временный буфер и перешифрует при сохранении:
sops secrets/prod/app.env
sops secrets/prod/config.yaml
Получатели берутся из .sops.yaml по правилу path_regex. При добавлении новых получателей используйте команду обновления ключей, чтобы пересчитать заголовки:
sops updatekeys -r secrets/prod
Командный процесс и минимизация рисков
Рабочая модель в команде:
- Каждый разработчик, кому нужна расшифровка, генерирует свой ключ
ageи публикует в MR/PR только публичный recipient. - Мейнтейнер добавляет recipient в
.sops.yaml, обновляет ключи файламиsops updatekeysи мёржит. - Секреты редактируют через
sops. В Git попадают только зашифрованные блоки и метаданные. - На сервере — только приватный ключ окружения. Личные ключи разработчиков на сервер не ставятся.
Такой процесс даёт управляемую модель доступа: доступ легко выдавать и отзывать, а аудит изменений — в истории Git.
Ротация и отзыв доступа без простоев
Сценарий ротации ключа прод-сервера:
- Сгенерировать новый ключ сервера, извлечь публичный recipient.
- Добавить его в
.sops.yamlрядом со старым. - Выполнить
sops updatekeys -rдля всего каталога секретов продакшна и закоммитить изменения. - Доставить новый приватный ключ на прод-сервер и перезапустить воркфлоу деплоя.
- Удалить старый recipient из
.sops.yaml, снова выполнитьsops updatekeys -rи закоммитить.
Благодаря списку получателей можно плавно перейти с одного ключа на другой без простоев и без рисков расшифровки посторонними.
Деплой: расшифровываем только на сервере
Базовый приём — сохранять приватный ключ в /etc/sops/age/keys.txt и использовать sops -d в процессе деплоя. Примеры:
# Расшифровать .env в память и экспортировать переменные для процесса
export $(sops -d --output-type dotenv secrets/prod/app.env)
# Расшифровать в файл с жёсткими правами, затем запуск
umask 177
sops -d secrets/prod/app.env > /run/app.env
systemctl restart myapp.service
Фрагмент юнита systemd, где окружение берётся из заранее расшифрованного файла:
[Unit]
Description=MyApp service
After=network.target
[Service]
Type=simple
EnvironmentFile=/run/app.env
ExecStart=/usr/local/bin/myapp
Restart=on-failure
[Install]
WantedBy=multi-user.target
При деплое расшифровку выполняйте кратковременно перед рестартом сервиса, пишите файл в tmpfs (/run), ограничивайте права и не сохраняйте секреты в артефактах CI. Для сервисов на выделенном сервере или VDS удобно закрепить ключи на уровне хоста и отдавать секреты только локальным юнитам.
Безопасное использование в shell-скриптах
- Не включайте
set -xтам, где работают секреты — это печатает переменные в логи. - Проводите минимальное время жизни расшифрованных файлов; чистите их после использования.
- Отдавайте предпочтение потоковой подаче:
sops -d file | command, когда это возможно.
Docker Compose: .env из sops без утечек
Можно запускать Compose, подавая окружение из расшифрованного потока или временного файла. Пример с временным файлом:
umask 177
sops -d secrets/prod/app.env > /run/app.env
docker compose --env-file /run/app.env up -d
Если используете подстановку процессов Bash, не забудьте экранировать угловые скобки в документации и помнить о совместимости шелла:
docker compose --env-file <(sops -d secrets/prod/app.env) up -d
В CI лучше избегать такой подстановки и работать с временным файлом в /dev/shm или /run (tmpfs), чтобы не оставлять дисковый след.
Ansible и sops: короткий путь
Есть два подхода:
- Использовать коллекцию плагинов для нативной поддержки sops в
vars_files. - Расшифровывать файлы заранее в задаче и подавать их как
extra_varsили через шаблоны.
Минимальный универсальный приём без дополнительных плагинов — перед запуском playbook расшифровать в tmpfs и подставить путь:
umask 177
sops -d inventory/group_vars/prod/secrets.yaml > /run/ansible-prod-secrets.yaml
ansible-playbook -i inventory/hosts site.yaml -e @/run/ansible-prod-secrets.yaml
Не храните расшифрованные файлы в репозитории или артефактах. Чистите их после завершения:
shred -u /run/ansible-prod-secrets.yaml

Проверки на стороне Git: pre-commit и защита от случайных утечек
Что полезно добавить в проект:
.gitignoreдля всех раскрытых временных файлов:*.dec,*.plain,/run/*.env, прочее.- pre-commit хук, который проверяет, что
secrets/содержит только файлы с заголовком sops и нет «голых» .env. - Линтеры, запрещающие вызов
set -xи эха переменных с шаблонами вродеPASS,TOKEN,KEY.
Диагностика типичных ошибок
- Ошибка расшифровки: нет подходящего ключа. Убедитесь, что recipient сервера присутствует в
.sops.yaml, а файл перезашифрованsops updatekeys -r. На сервере проверьте права и путь кkeys.txt. - Разработчик не может расшифровать локально. Его публичный ключ не добавлен в
.sops.yaml. Добавьте recipient и обновите ключи. - CI «видит» секреты в логах. Отключите эхо команд с переменными, проверьте флаги shell и вывод команд. Никогда не печатайте расшифрованный файл целиком.
- Конфликт при мёрже зашифрованных файлов. Всегда редактируйте через
sops, чтобы он корректно пересчитал метаданные. При конфликте повторно откройте файл черезsopsи сохраните. - .env парсится неверно. Убедитесь в корректных кавычках и экранировании. Используйте
--output-type dotenvпри чтении.
Политики и базовые нормы безопасности
- Ни один секрет не должен храниться в открытом виде в репозитории, артефактах, образах контейнеров и снапшотах.
- Приватные ключи серверов живут только на соответствующих серверах. Резервируйте их в офлайн-хранилищах с контролем доступа.
- Отдельные ключи на окружение. Упрощает отзыв и снижает blast radius.
- Ротация по событию и по расписанию (например, ежеквартально).
- Аудит: периодически проверяйте историю на появление секретов в открытом виде, используйте сканеры по шаблонам.
- CI: секреты подавайте как переменные среды в момент задачи, запрещайте печать значений, ограничивайте видимость джобов.
Полезные приёмы работы с sops
- Селективное шифрование в YAML/JSON через
encrypted_regex, чтобы оставлять несекретные поля видимыми. sops -d --output-type dotenvдля корректного экспорта .env в окружение процесса.sops updatekeys -rпри изменении списка получателей в.sops.yaml.- Собирайте инфрастуктурные переменные без секретов отдельно от секретных — это упрощает ревью и диффы.
После внедрения sops+age стоит подумать и о наблюдении за продом: пригодится алертинг по Nginx и системным метрикам — см. гайд по Prometheus, Node Exporter и алертам Nginx. А для высоконагруженных PHP-проектов на VDS обратите внимание на тюнинг PHP-FPM.
Итог
Пара sops + age даёт предсказуемый и проверяемый способ хранить секреты в Git и деплоить конфигурации без утечек. Вы описываете политику шифрования в .sops.yaml, шифруете секретные поля, храните файлы в репозитории и расшифровываете их только на целевом хосте. Добавьте контроль в CI, внедрите ротацию и дисциплину редактирования через sops — и тема секретов перестанет быть источником случайных инцидентов.


