Перейти к содержимому
Поиск
Разработка

SOPS и age: зашифрованные настройки — мастерская и 9 практических задач

Настройки в Git, ключи отдельно: настоящий CLI, доступ коллег, ротация и старая копия, Node.js/PHP и восстановление. Мастерская и исходники.

Александр Некрасов
SOPS и age: зашифрованные настройки — мастерская и 9 практических задач — обложка

Зашифрованные JSON, YAML и .env: recipients, доступ, ротация, восстановление и настоящие Node.js/PHP-процессы. Девять задач с исходниками Windows/Linux.

SOPS и age: хранить настройки вместе с проектом, управлять доступом отдельно

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

SOPS позволяет сохранить структурированную конфигурацию с зашифрованными значениями. age используется как один из способов защиты ключа, необходимого для её открытия. В репозитории можно держать зашифрованный документ и публичные правила получателей, а приватные identity хранить отдельно. Это полезно для небольших команд, deployment-конфигураций и собственных серверов, где понятен круг людей и процессов, которым нужны данные.

В статье пройдём полный учебный маршрут: создать ключи, зашифровать несколько форматов, получить ожидаемый отказ, добавить участника, удалить его из нового файла, проверить старую копию и передать настройки настоящим Node.js/PHP-процессам. Мастерская показывает девять задач и десять записей реальных CLI-команд; исходники можно скачать и повторить на Windows или Linux.

Где применять и какой результат получит разработчик

Представьте сервис, который запускается локально, на тестовом сервере и в production. Код одинаковый, а настройки различаются. Зашифрованные файлы можно рассматривать на code review: видно, какой документ изменился, какие поля добавлены и для какого окружения предназначена конфигурация. Само значение закрытого поля при этом не требуется публиковать в обсуждении.

Такой подход удобен для статических секретов конфигурации: параметры подключения, интеграционные токены, закрытые значения приложения. Файл можно переносить вместе с релизом и открывать только в нужной среде. Для каждого окружения полезны собственные получатели, чтобы доступ к тестовой конфигурации не превращался автоматически в доступ к production.

Если нужны динамические короткоживущие credentials, централизованное согласование выдачи и подробный аудит каждого обращения, одного файлового шифрования может быть недостаточно. SOPS умеет работать и с внешними системами ключей, но эта лаборатория сознательно использует age. Мы строим понятный воспроизводимый процесс работы с файлами, не обещая заменить им все возможности специализированного хранилища секретов.

Три разных сущности: identity, ключ данных и пароль приложения

Путаница начинается, когда любое закрытое значение называют одним словом «ключ». В нашей задаче их роли различаются. Приватная age identity позволяет получателю получить доступ к зашифрованному файлу. Публичный recipient можно сообщить автору конфигурации: он нужен для предоставления доступа, но сам по себе не раскрывает значения.

У SOPS есть отдельный ключ данных файла. Его смена меняет криптографическую упаковку содержимого. Пароль базы или API-токен внутри документа — ещё одна сущность: реальный сервис проверяет именно его. Перешифрование документа не обращается к провайдеру и не отзывает ранее выданный пароль.

Это различие прямо проверяется в мастерской. После изменения списка получателей и ротации новый файл недоступен Bob, но старую копию он продолжает открывать. Если в обеих копиях записан один действующий пароль, возможность входа во внешний сервис у Bob сохраняется. Чтобы прекратить её, потребуется сменить сам credential и обновить конфигурацию работающего приложения.

Версии инструментов, установка и происхождение файлов

В проекте закреплены SOPS 3.13.3 и age 1.3.2. Для запуска лаборатории нужны Python 3.11 или новее, Node.js 22 или новее и PHP 8.3 или новее: Python организует опыты, а Node/PHP действительно читают переданные настройки. На публичной странице используется локальный Bootstrap 5.3.9. Проверка версий выполнена 13 ноября 2026 года.

Бинарные файлы нужно брать из официальных релизов под свою ОС и архитектуру. Для SOPS опубликованы контрольные суммы и сведения для проверки поставщика; не смешивайте простое совпадение SHA-256 с криптографическим подтверждением происхождения. Инструкции находятся на страницах SOPS 3.13.3 и age 1.3.2.

Лаборатория фиксирует версии ради повторения результата, но рабочая инфраструктура требует сопровождения и обновлений. Перед внедрением проверяйте актуальные исправления безопасности. Не загружайте утилиту из случайной статьи только потому, что имя файла похоже: именно этому исполняемому файлу вы доверите работу с открытой конфигурацией.

sops --version
age-keygen --version
python --version
node --version
php -v
# Версии закреплены в README и Dockerfile лаборатории.

Мини-мастерская: реальные команды и честные границы страницы

Мастерская хранит результаты настоящего выполнения SOPS и age. Переключение Windows/Linux выбирает другой запуск. Выбор опыта открывает команду, вывод, exit code и пояснение. Десять записей относятся к девяти задачам, потому что форматная задача отдельно показывает YAML и .env. Неуспешная расшифровка отмечена как ожидаемый отрицательный результат, а не замаскирована под успешную операцию.

Страница не принимает пользовательские секреты, не создаёт реальные ключи посетителя и ничего не отправляет в SOPS. Конструктор команды собирает текст из выбранных операции и формата; он не выполняет этот текст. Для нового запуска предназначен capture.py из архива. Все опубликованные значения учебные, identity созданы специально для опыта, приватные строки в результат не включены.

Начните с JSON, затем посмотрите отказ чужому ключу, добавление участника и отзыв доступа к новому файлу. Обязательно сравните новую и старую копии после ротации. Завершите запуском Node/PHP и восстановлением отдельной identity. Так видно не только шифрование, но и жизненный цикл конфигурации, который нужно организовать вокруг инструмента.

Опыт 1. Создать identity и получить публичный recipient

Лаборатория создаёт три независимые identity: Alice, Bob и Recovery. Сначала доступ выдаётся Alice и резервному получателю. Bob нужен, чтобы проверить ожидаемый отказ и последующее изменение прав. Генерация выполняется настоящим age-keygen, поэтому публичные recipients различаются между запусками и операционными системами.

В своём проекте создавайте приватный файл вне репозитория, рабочей папки сайта и общей директории синхронизации. Команда age-keygen -y извлекает только публичный recipient, который начинается с age1. Именно его добавляют в правила проекта. Приватное содержимое файла не следует копировать в issue, скриншот, README или журнал установки.

На Linux закрывайте директорию и файл правами служебного пользователя. На Windows используйте соответствующие ACL папки и учётной записи: вызов chmod из Python не заменяет Windows ACL. Учебный capture работает только с одноразовыми искусственными данными и удаляет временный каталог при нормальном завершении; это не инструкция хранить реальные ключи в произвольном temp.

# Linux: пример отдельной identity в закрытой директории
install -d -m 0700 "$HOME/.config/sops/age"
age-keygen -o "$HOME/.config/sops/age/keys.txt"
chmod 0600 "$HOME/.config/sops/age/keys.txt"
age-keygen -y "$HOME/.config/sops/age/keys.txt"
# Последняя команда печатает публичный recipient, не приватную строку.

Правило .sops.yaml: кому выдаётся доступ к новому файлу

Публичная конфигурация SOPS связывает имя файла и список получателей. В лаборатории достаточно одного правила, которое подходит JSON, YAML и ENV. В нём перечислены публичные recipients Alice и Recovery. Файл .sops.yaml можно хранить вместе с проектом, но изменение получателей является существенным изменением доступа и заслуживает review.

Ниже вместо настоящего recipient стоит явно обозначенный placeholder. Его нужно заменить значением своей команды age-keygen -y; строка из примера не является рабочим ключом. Полный capture создаёт правильное правило автоматически, поэтому для первого воспроизведения не приходится вручную переносить случайно сгенерированные значения.

Для разных окружений создавайте отдельные правила и проверяйте, какое имя файла они охватывают. Само добавление recipient в .sops.yaml не переписывает все уже существующие зашифрованные документы. Для них потребуется отдельная операция обновления получателей, которую мы выполняем далее. Это важное различие между политикой создания и состоянием конкретного файла.

creation_rules:
  - path_regex: '\.(json|yaml|env)$'
    age: age1REPLACE_WITH_REAL_PUBLIC_RECIPIENT
# Placeholder заменяется публичным recipient; приватной identity здесь нет.

Опыт 2. JSON: зашифровать значения и проверить полный возврат

В JSON-опыте исходный объект содержит имя учебного пользователя, вымышленный пароль и порт. После encrypt появляется отдельный зашифрованный файл. Программа действительно расшифровывает его и сравнивает весь полученный объект с исходным, а не только наличие одной ожидаемой строки. Дополнительно проверяется, что учебный пароль не остался открытым в ciphertext.

Структура и имена полей в таком документе остаются видимыми. Поэтому название поля само не должно содержать секрет. По структуре могут быть понятны назначение интеграции или устройство приложения; шифрование значений не следует описывать как полное скрытие всех метаданных репозитория.

Для реального файла команда decrypt по умолчанию выводит открытые данные в терминал. В лаборатории это допустимо внутри захваченного процесса, потому что значения искусственные. При работе с настоящей конфигурацией заранее решите, кто увидит stdout, сохраняет ли терминал историю и попадает ли вывод в CI-лог. Для непосредственного запуска приложения чаще удобнее передача окружения дочернему процессу.

sops encrypt --output encrypted-app.json app.json
# Только для вымышленных данных первого опыта:
sops decrypt encrypted-app.json
# capture.py проверяет json.loads(decrypted) == original.

Опыт 3. YAML и .env: формат сохраняет практический смысл

YAML удобен для вложенной конфигурации, а ENV — для плоского набора переменных приложения. В мастерской оба формата проходят настоящее шифрование и расшифровку. В записи показано полученное содержимое и подтверждено присутствие ожидаемого учебного значения после обратного преобразования. Это две отдельные записи одной задачи, а не два выдуманных консольных скриншота.

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

Зашифрованный ENV нельзя автоматически передать обычному dotenv-загрузчику и ожидать исходные значения. Сначала нужен доверенный этап расшифровки. Как приложение читает уже доступные переменные окружения и чем getenv отличается от файла настроек, разобрано в нашей статье о phpdotenv. SOPS добавляет защиту хранения, а не меняет контракт чтения переменных внутри PHP.

sops encrypt --output encrypted-app.yaml app.yaml
sops encrypt --output encrypted-app.env app.env
# Для сохранения открытого файла используйте явный output:
# sops decrypt --output private/app.env encrypted-app.env
# private/ — заранее подготовленная закрытая директория вне Git.

Опыт 4. Чужой ключ: отказ тоже должен быть проверен

До добавления Bob в получатели его identity не открывает JSON. Это проверяется реальной командой SOPS с ненулевым кодом завершения. В мастерской виден отказ и ограниченный диагностический вывод. Смысл теста — подтвердить, что доступ определяется подходящим приватным материалом, а не простым знанием имени файла или публичного recipient.

Для корректной проверки нельзя незаметно использовать другой ключ из пользовательского окружения. Capture изолирует рабочую конфигурацию и задаёт выбранную identity явно. Иначе тест «чужого ключа» мог бы неожиданно пройти благодаря ключу Alice, который утилита нашла в стандартной папке. Это пример того, почему важны условия запуска, а не только последняя строка команды.

При собственном отказе сначала проверьте путь SOPS_AGE_KEY_FILE, пользователя процесса, права чтения и соответствие recipient в файле. Не начинайте с отключения проверок целостности или копирования production-ключа в новую машину. Ошибка доступа должна помогать найти причину, сохраняя исходную границу доверия.

# Linux: identity выбирается явно для данного процесса
SOPS_AGE_KEY_FILE=/private/bob.txt sops decrypt encrypted-app.json
code=$?
printf 'exit=%s\n' "$code"
# До добавления Bob ожидается ненулевой exit code.

Опыт 5. Добавить участника и проверить доступ

В следующем опыте правило расширяется: к Alice и Recovery добавляется публичный recipient Bob. Затем выполняется updatekeys для конкретного зашифрованного JSON. После этого Bob действительно расшифровывает документ, а программа сравнивает полученный объект с исходным. Такой тест проверяет результат изменения, а не только успешное завершение команды обновления.

При добавлении коллеги передавать ему приватную identity другого человека не требуется. У каждого может оставаться свой ключ, а документ становится доступен нужному набору получателей. Это упрощает последующий разбор, кто и каким способом имел доступ к определённой версии конфигурации, хотя само по себе не создаёт централизованный журнал каждого чтения.

В интерактивной работе полезно просматривать предложенное изменение списка перед подтверждением. Флаг -y в лаборатории нужен для воспроизводимого автоматического запуска. В production применяйте его только там, где изменение правил уже проверено и одобрено процессом команды. Доступ к новым данным — существенное действие, даже если технически меняется небольшой участок метаданных.

# После добавления публичного recipient Bob в .sops.yaml:
sops updatekeys encrypted-app.json
# В контролируемом автоматическом сценарии:
sops updatekeys -y encrypted-app.json
# Затем отдельная проверка расшифровки identity Bob.

Опыт 6. Удалить получателя, сменить ключ данных и открыть старую копию

Лаборатория сохраняет старую копию в момент, когда Bob ещё имеет доступ. Затем его recipient удаляется из правила, обновляются получатели существующего файла и выполняется rotate. Новый JSON не открывается identity Bob. Старая сохранённая копия по-прежнему открывается и содержит прежнее значение пароля. Оба результата проверены настоящими командами.

Это практически важнее абстрактного утверждения «мы отозвали доступ». Отзыв относится к новой версии упаковки; он не стирает уже прочитанные значения, старые commits и резервные копии. Если секрет был доступен ушедшему участнику или скомпрометированному ключу, нужно отдельно оценить необходимость смены действующего пароля или токена у сервиса.

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

# 1. Удалить recipient из .sops.yaml.
# 2. Обновить существующий encrypted-файл.
sops updatekeys -y encrypted-app.json
# 3. Сменить ключ данных и перешифровать значения.
sops rotate --in-place encrypted-app.json
# 4. Проверить оставшийся и удалённый доступ.
# 5. При необходимости сменить credential у провайдера.

Ротация секрета приложения: отдельная операция и отдельная проверка

Пароль базы живёт на стороне базы, а API-токен — на стороне выдавшего его сервиса. SOPS не знает, как этим сервисам отменить прежнее значение. Поэтому криптографическая ротация файла и ротация credential — разные задачи, каждая со своим критерием успеха. Изменившийся ciphertext не доказывает, что старый токен перестал работать.

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

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

Опыт 7. Подмена ciphertext: ожидаемый отказ целостности

В отдельном опыте программа переносит зашифрованное значение одного поля на место другого. Это реальная модификация созданного JSON, после которой SOPS пытается открыть документ с подходящим ключом Alice. Расшифровка отклоняется. В мастерской видны ненулевой exit code и диагностическое сообщение, полученные от инструмента.

Такой результат показывает, что наличие правильной identity не означает принятие произвольно изменённого содержимого. Однако он не является полным тестом криптографической реализации или доказательством безопасности всего процесса deployment. Здесь проверяется один конкретный отрицательный сценарий, полезный для понимания целостности зашифрованной конфигурации.

Не исправляйте неожиданный отказ выключением контроля MAC только ради запуска приложения. Сначала сравните файл с проверенной версией, выясните источник изменения и восстановите корректный документ. В рабочем pipeline важно отличать отказ доступа от повреждения или подмены: причины различны, хотя обе операции завершаются без выдачи ожидаемой конфигурации.

Опыт 8. Exec-env: передать настройки процессу без печати пароля

Для приложения, которое читает переменные окружения, удобно вызвать exec-env. SOPS расшифровывает выбранный документ и запускает дочернюю команду с доступными значениями. В лаборатории последовательно запускаются Python-проверка, Node.js и PHP. Все три действительно читают настройку; пароль при этом не выводится в опубликованный результат и не сохраняется в отдельный plaintext-файл.

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

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

sops exec-env encrypted-app.json 'node src/app.mjs'
sops exec-env encrypted-app.json 'php src/app.php'
# Результат учебных приложений:
# {"configured":true,"user":"demo_reader"}

PHP getenv и Node process.env: небольшие исполняемые примеры

PHP-пример получает DB_PASSWORD через getenv и явно отклоняет отсутствие значения или пустую строку. Node.js делает аналогичную проверку process.env. Это минимальная граница между успешно расшифрованным файлом и готовностью приложения использовать настройку. Факт успешного старта SOPS сам по себе не проверяет все обязательные поля приложения.

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

Оба маленьких приложения лежат в src и действительно запускаются capture.py через exec-env. Они одноразовые, поэтому не следует выдавать их за готовый веб-сервер или PHP-FPM-интеграцию. Их назначение — показать передачу данных через реальную границу процессов и безопасный по содержанию диагностический ответ.

<?php
declare(strict_types=1);
$password = getenv('DB_PASSWORD');
if ($password === false || $password === '') {
    throw new RuntimeException('DB_PASSWORD is required');
}
echo json_encode([
    'configured' => true,
    'user' => getenv('DB_USER') ?: '(missing)',
], JSON_THROW_ON_ERROR), PHP_EOL;

Если программе нужен файл: понимать exec-file и временный plaintext

Некоторые программы читают только путь к конфигурации. Для них у SOPS есть exec-file. Это другой механизм передачи: программе подставляется путь вместо выдачи всех полей через окружение. На поддерживаемых системах может использоваться FIFO, но он подходит не любому способу чтения и не всем платформам.

Флаг –no-fifo выбирает обычный временный файл. Такой вариант полезен, в частности, там, где FIFO недоступен, но его нельзя описывать как «секрет никогда не был на диске». Срок жизни временного файла, права, аварийное завершение, резервное копирование и поведение потребляющей программы нужно учитывать отдельно. Удаление после процесса не означает гарантированного стирания всех следов носителя.

Наша исполняемая лаборатория проверяет exec-env, а не все режимы exec-file. Для интеграции с файловым потребителем изучите официальное описание передачи данных процессам и проверьте конкретную программу. Не заменяйте осмысленный bootstrap привычной командой decrypt с перенаправлением в общий каталог приложения.

Опыт 9. Восстановление отдельной резервной identity

Резервный recipient присутствует в документе с начала опыта. В конце capture использует отдельную identity Recovery, а не ключ Alice, и проверяет полное совпадение восстановленного JSON. Это настоящий тест восстановления доступа. Сам факт наличия encrypted-файла в Git без подходящего приватного материала такой возможности не даёт.

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

Проверяйте восстановление на копии в закрытом окружении. Успех означает, что правильные данные действительно прочитаны нужным независимым способом; файл с названием backup ещё ничего не доказывает. После теста убедитесь, что открытые значения и временные материалы не попали в журнал или общий каталог. В лаборатории опубликованы только результаты сравнения и публичные сведения.

# Linux: резервная identity применяется только к этой команде
SOPS_AGE_KEY_FILE=/private/recovery.txt sops decrypt encrypted-app.json
# В лаборатории stdout перехватывается и сравнивается с synthetic JSON.
# Для настоящих секретов не выводите результат в общий терминал/CI-лог.

Windows: повторение лаборатории и особенности PowerShell

Для Windows скачайте официальные сборки SOPS и age под свою архитектуру. Их не обязательно сразу добавлять в системный PATH: capture принимает абсолютные пути через SOPS_BIN и AGE_KEYGEN_BIN. Это помогает не запустить другой бинарник, оставшийся от старой установки. Python, Node и PHP должны быть доступны для дочерних команд.

После запуска capture.py публичная запись обновится реальными результатами вашей машины. Для мастерской используйте HTTP на loopback, а не открытие HTML через file://: интерфейс загружает JSON и исходники. Такой просмотрщик не создаёт внешнего сервиса управления секретами и не требует входа в production.

Для сохранения открытого или зашифрованного файла предпочитайте явный –output. У разных версий PowerShell перенаправление вывода может иметь особенности кодировки; не стоит превращать их в неочевидную часть криптографического опыта. Приватную рабочую identity храните в закрытой папке профиля с проверенными ACL, а не рядом с ZIP для публикации.

$env:SOPS_BIN = 'C:\tools\sops-v3.13.3.amd64.exe'
$env:AGE_KEYGEN_BIN = 'C:\tools\age\age-keygen.exe'
python capture.py
python -m http.server 8543 --bind 127.0.0.1 --directory public
# Остановить локальный просмотр: Ctrl+C
# SOPS_AGE_KEY_FILE для своего приложения задаётся отдельно.

Linux и Docker: воспроизведение без подключения к рабочим ключам

На Linux можно установить официальные утилиты и запустить тот же Python-скрипт, указав пути через переменные окружения. Для первого опыта используйте отдельный каталог и учебные данные. Capture создаёт собственные identity, изолирует стандартные места поиска конфигурации и не должен опираться на ключи, которыми вы открываете настоящие приложения.

В архиве также есть Dockerfile и Compose. Сборка загружает закреплённые версии инструментов; сам запуск сценариев выполняется без сети. Публичный каталог подключается для сохранения JSON-результата. Приватный временный каталог не включается в опубликованные материалы. Dockerfile лаборатории рассчитан на amd64; для ARM64 нужны соответствующие бинарные файлы.

Контейнер помогает повторить Linux-процесс на Windows и проверить границы передачи окружения. Он не доказывает корректность вашей production-конфигурации, прав каталогов или CI-политики. Реальная приёмка сервера должна выполняться от его служебного пользователя с теми путями и файлами, которые будет использовать приложение.

SOPS_BIN=/usr/local/bin/sops AGE_KEYGEN_BIN=/usr/local/bin/age-keygen python3 capture.py

# Альтернатива через проектный Compose:
docker compose build linux
docker compose run --rm linux
docker compose down

VPS: отдельный получатель сервера и права файлов

Для сервера создайте самостоятельную identity и добавьте её recipient только в нужное production-правило. Не переносите туда универсальный ключ всех окружений. Так компрометация одной среды меньше расширяет доступ к другим документам. Зашифрованный файл можно разместить рядом с конфигурацией сервиса; приватный ключ остаётся в закрытой папке вне web-root и образа приложения.

Пример ниже предполагает существующего пользователя myapp и проверенные пути. Его нужно адаптировать к своему серверу. Каталог ключа доступен служебному пользователю, а права файла ограничены. После размещения проверьте запуск под тем же пользователем: успешная расшифровка администратором не доказывает доступность ключа рабочему процессу.

Не путайте размещение identity с её защитой от всех локальных угроз. Привилегированный пользователь машины и скомпрометированное приложение остаются важными границами. SOPS защищает сохранённую конфигурацию, но в момент работы процесс должен получить данные. Внешнее VPS-развёртывание не выдаётся здесь за выполненную проверку; команды служат практическим шаблоном.

sudo install -d -m 0700 -o myapp -g myapp /etc/myapp
sudo -u myapp age-keygen -o /etc/myapp/age.identity.txt
sudo chmod 0600 /etc/myapp/age.identity.txt
sudo -u myapp age-keygen -y /etc/myapp/age.identity.txt
# Добавить публичный recipient в production-правило и updatekeys.
sudo install -m 0640 -o root -g myapp encrypted-app.json /etc/myapp/encrypted-app.json

Запуск сервиса, перечитывание настроек и PHP-FPM

Для долгоживущего Node.js-приложения SOPS можно включить в команду запуска systemd. В архивном LINUX-VPS.md приведён unit с отдельным пользователем, рабочей директорией и путём к identity. Он ссылается на сервер пользователя; учебный app.mjs является одноразовой проверкой и не должен автоматически перезапускаться как веб-сервис.

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

У PHP-FPM собственные правила окружения и настройки pool, включая clear_env. Запуск php src/app.php через exec-env проверяет отдельный CLI-процесс и не передаёт переменные автоматически всем веб-запросам FPM. Для веб-приложения нужен отдельно продуманный закрытый bootstrap или механизм deployment. Не используйте публичный phpinfo как способ подтвердить передачу секретов.

sudo -u myapp env SOPS_AGE_KEY_FILE=/etc/myapp/age.identity.txt \
  /usr/local/bin/sops exec-env /etc/myapp/encrypted-app.json \
  '/usr/bin/node /opt/myapp/src/app.mjs'
# Здесь app.mjs — одноразовая проверка.
# Для systemd нужен ваш долгоживущий сервер; шаблон в LINUX-VPS.md.

Git, CI и журнал: не разрушить защиту соседним шагом

Зашифрованный документ удобно хранить в Git, но приватная identity должна попадать только в доверенную среду расшифровки. Не выдавайте её произвольному pull request, в котором автор может изменить исполняемый код. Если job получил ключ и запустил чужой скрипт, шифрование файла уже не мешает этому скрипту прочитать значение.

Отключите привычку печатать env, весь объект конфигурации и аргументы с секретными значениями. Даже при аккуратном использовании exec-env следующий шаг может выгрузить окружение в лог или диагностический архив. В рабочем журнале достаточно ID релиза, версии конфигурации, результата проверки и класса ошибки. Маскирование логов полезно, но не заменяет исключение ненужного вывода.

Если plaintext раньше попадал в историю, шифрование текущей версии не удаляет старые копии. Рассмотрите отзыв реального credential и дальнейшую работу с историей отдельно. Контроль получателей, доступ к CI, права на сервере и проверенное восстановление должны быть понятной частью процесса команды, а не случайным набором команд, выполненных одним разработчиком.

Исходники, проверенные границы и дальнейшее применение

Архив содержит capture.py, реальные примеры Node.js и PHP, Dockerfile, Compose, инструкции Windows/Linux/VPS и статическую мастерскую. Windows и Linux прошли по 31 проверке. Опыт создаёт свежие ключи, использует только вымышленные данные, проверяет положительные и отрицательные результаты и сохраняет запись после завершения набора. Поэтому новый ciphertext и recipients будут отличаться от опубликованных — это ожидаемое поведение, а не ошибка воспроизведения.

Материал не обещает безопасность всей инфраструктуры, отсутствие следов plaintext в любых условиях или отзыв информации, которую получатель уже прочитал. Фактические результаты ограничены показанными командами: roundtrip, отказ чужому ключу, изменение получателей, старая копия, подмена данных, дочерние процессы и резервное восстановление. Готовность своего deployment проверяйте отдельно на своих доверенных тестовых настройках.

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