pimenov.ai

База знаний

Секреты в репозитории без риска: связка mise + sops + age для команд

Связка mise + sops + age для команд: воспроизводимый запуск окружения, безопасное хранение project-level секретов и контроль доступа по контурам.

Опубликовано Обновлено

Связка mise + SOPS + age помогает команде воспроизводимо запускать проект и хранить секреты локального и предбоевого контуров (dev и staging) в репозитории в зашифрованном виде. mise описывает инструменты и команды, SOPS шифрует конфигурацию, а age задаёт публичные получатели шифрования (recipients), для которых соответствующие приватные identities могут расшифровать файл.

Материал сверён с официальной документацией 7 сентября 2026 года. Примеры основаны на документации и требуют проверки в вашем проекте перед рабочим использованием.

Целевая архитектура

В репозитории находятся:

  • mise.toml с инструментами, переменными окружения и задачами;
  • .sops.yaml с правилами выбора recipients;
  • зашифрованные конфиги отдельных контуров;
  • публичные age-recipients, если команда хранит их отдельным списком.

Вне репозитория остаются:

  • приватные age-identities разработчиков, то есть секретные ключи;
  • identity CI, переданная через защищённое хранилище CI-пайплайна;
  • production-секреты, если политика команды требует отдельного хранилища секретов на этапе выполнения;
  • глобальные административные токены и личные ключи.

Роли разделены следующим образом:

  • mise описывает инструменты, окружение и команды проекта;
  • SOPS шифрует конечные значения в YAML, JSON, ENV и INI и хранит MAC, который используется для проверки целостности значений и структуры;
  • age задаёт публичные recipients для шифрования и приватные identities для расшифровки.

Одна команда mise run dev может запускать SOPS. SOPS передаёт расшифрованные значения только дочернему процессу, которому они нужны.

Чеклист быстрой проверки

В проекте есть mise.toml с выбранными версиями инструментов и задачами.
mise tasks показывает понятные команды dev, test, build и проверочный запуск.
В репозитории нет незашифрованных токенов и паролей.
Секреты разделены по контурам: dev, staging, sandbox и другим необходимым средам.
Для каждого контура задан свой набор age-recipients.
Приватные age-identities не коммитятся в Git.
CI использует отдельную identity с минимально необходимым доступом.
Команда знает, как добавить нового recipient и обновить зашифрованные файлы.
Процесс отзыва доступа включает удаление recipient и ротацию самих секретов при компрометации identity.
Production-секреты обрабатываются по отдельной политике и не попадают в локальные dev-задачи.
Команды с записью во внешние системы отделены от проверочных запусков.
После настройки есть наблюдаемая проверка: дочерний процесс получает нужные переменные, а открытая копия файла не создаётся.

Что делает каждый инструмент

mise фиксирует окружение и команды

Проектный mise.toml может содержать инструменты, переменные окружения и задачи. Конфигурация проекта и локальные файлы переопределяют глобальные и родительские настройки, при этом правила объединения зависят от конкретного раздела конфигурации.

Основной файл называется mise.toml. Вариант .mise.toml также распознаётся, однако актуальная документация рекомендует начинать с mise.toml в корне проекта.

Минимальный пример:

[tools]
node = "22"      # выберите версию, на которой тестируется проект
pnpm = "9"       # пример запроса версии менеджера пакетов
python = "3.12"  # пример версии Python

[env]
PROJECT_ENV = "dev"

[tasks.hello]
run = "node --eval 'console.log(process.env.PROJECT_ENV)'"

Проверка:

mise install
mise run hello

mise install установит объявленные инструменты при необходимости, а команда выведет dev.

SOPS шифрует значения конфигурации

SOPS обрабатывает YAML, JSON, ENV и INI как деревья данных и шифрует конечные значения. Имена ключей остаются открытыми, поэтому структура и изменения файла читаются в Git. MAC в метаданных SOPS помогает обнаружить изменение значений или структуры.

Расширение файла влияет на выбор формата. После шифрования лучше сохранять исходное расширение. Если расширение изменено, при расшифровке укажите --input-type. При работе через stdin при необходимости задайте --input-type и --output-type явно.

Не используйте верхнеуровневые массивы в YAML и JSON: SOPS должен добавить ключ sops с метаданными на том же уровне. YAML anchors также не поддерживаются.

age задаёт получателей и identities

Команда age-keygen создаёт identity-файл:

age-keygen -o key.txt

Команда печатает публичный recipient вида age1..., а identity-файл содержит секретный ключ вида AGE-SECRET-KEY-1.... Публичный recipient можно использовать в конфигурации проекта. Приватную identity храните вне репозитория и передавайте SOPS через защищённый механизм, поддерживаемый вашей средой.

Несколько recipients позволяют владельцам соответствующих identities расшифровать один и тот же файл. Скомпрометированная identity требует отдельной процедуры отзыва доступа.

⚠️
Внимание: если identity была скомпрометирована, удаление recipient и повторное шифрование только текущей версии файла не меняет старые ciphertext в истории Git, которые эта identity могла открыть. Сначала ротируйте значения секретов, затем обновите recipients и перешифруйте зашифрованные файлы.

Установка инструментов

На macOS и Linux с Homebrew age устанавливается командой:

brew install age

Для mise и SOPS используйте способы установки из их официальной документации либо доступные пакеты вашей платформы. После установки проверьте доступность CLI:

mise --version
sops --version
age --version

Не фиксируйте в командном шаблоне путь к приватной identity, пока не выбрали единый способ хранения для локальной разработки и CI.

Передача секретов дочернему процессу

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

[tasks.dev]
description = "Локальный запуск с dev-секретами"
run = "sops exec-env secrets/dev.sops.yaml 'pnpm dev'"

[tasks."sync:dry"]
description = "Проверочный запуск синхронизации без записи"
run = "sops exec-env secrets/dev.sops.yaml 'pnpm sync --dry-run'"

Для конфигурации, которую передаёте через exec-env, используйте верхнеуровневые ключи с допустимыми именами переменных окружения, например:

API_URL: https://sandbox.example.invalid
CHECK_VALUE: replace-before-encryption

replace-before-encryption здесь является безопасной заглушкой. Реальное значение добавляют перед шифрованием и не сохраняют в открытом виде.

Если приложению нужен файл, SOPS предоставляет exec-file. На Unix-подобных системах по умолчанию используется FIFO, то есть именованный канал: открытые данные не записываются на диск, а дочерний процесс читает их через этот канал. Параметр --no-fifo создаёт временный файл и удаляет его после завершения процесса; этот режим требует отдельной оценки риска.

⚠️
Внимание: --no-fifo означает появление открытого временного файла. Убедитесь, что права доступа, каталог временных файлов и очистка после завершения соответствуют политике проекта.

Разделение доступа по контурам

Файл должен называться .sops.yaml, а не .sops.yml. Если не задан параметр --config, SOPS ищет .sops.yaml в текущем каталоге и родительских каталогах и использует первый найденный файл.

Пример правил для двух контуров:

creation_rules:
  - path_regex: secrets/dev\.sops\.yaml$
    age:
      - age1developer-recipient-placeholder # замените на полный публичный recipient
      - age1ci-recipient-placeholder        # замените на полный публичный recipient

  - path_regex: secrets/staging\.sops\.yaml$
    age:
      - age1lead-recipient-placeholder      # замените на полный публичный recipient
      - age1ci-recipient-placeholder        # замените на полный публичный recipient

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

Разделение по контурам уменьшает область доступа:

  • разработчик получает доступ к dev или sandbox;
  • CI получает только контуры, необходимые его задачам;
  • production использует отдельный процесс выдачи и ротации доступа.

Эталонный шаблон проекта

project/
├── mise.toml
├── .sops.yaml
├── secrets/
│   ├── dev.sops.yaml
│   └── staging.sops.yaml
└── age-keys/
    ├── team.pub          # только публичные recipients
    └── ci.pub            # только публичный recipient CI

Собранный mise.toml:

min_version = "2026.1.0" # жёсткий минимум версии mise; выберите подходящую версию

[tools]
node = "22"      # выберите версию, на которой тестируется проект
pnpm = "9"       # пример запроса версии менеджера пакетов
python = "3.12"  # пример версии Python

[env]
PROJECT_ENV = "dev"

[tasks.dev]
description = "Локальный запуск с зашифрованными dev-секретами"
run = "sops exec-env secrets/dev.sops.yaml 'pnpm dev'"

[tasks."sync:dry"]
description = "Проверочный запуск без записи"
run = "sops exec-env secrets/dev.sops.yaml 'pnpm sync --dry-run'" # если проект поддерживает этот флаг

[tasks.smoke]
description = "Smoke-тесты после развёртывания"
run = "pnpm smoke" # команда должна существовать в проекте

Строковая форма min_version задаёт жёсткий минимум версии mise. Значения инструментов, имена задач и флаг sync --dry-run в этом блоке являются шаблоном. Выберите и протестируйте варианты, которые поддерживает ваш код.

Пример .sops.yaml с разделением доступа:

creation_rules:
  - path_regex: secrets/dev\.sops\.yaml$
    age:
      - age1developer-recipient-placeholder # полный публичный recipient разработчика
      - age1ci-recipient-placeholder        # полный публичный recipient CI

  - path_regex: secrets/staging\.sops\.yaml$
    age:
      - age1lead-recipient-placeholder      # полный публичный recipient ответственного
      - age1ci-recipient-placeholder        # полный публичный recipient CI

Проверка результата

Создайте отдельный тестовый секрет без реальных токенов, например с ключом CHECK_VALUE, зашифруйте его по подходящему creation_rule, затем запустите дочерний процесс через SOPS:

sops exec-env secrets/dev.sops.yaml 'printenv CHECK_VALUE'

Ожидаемый результат:

  1. Подходящая identity позволяет запустить команду.
  2. Дочерний процесс получает значение CHECK_VALUE.
  3. После завершения рядом с исходным зашифрованным файлом не появляется открытая копия.

mise run hello отдельно проверяет переменную PROJECT_ENV из [env] в mise.toml. Прямой вызов sops exec-env не загружает настройки mise.toml, поэтому для проверки SOPS используйте ключ, который действительно находится в зашифрованном файле.

Дополнительно повторите проверку без доступной identity. Расшифровка должна завершиться ошибкой.

Полезные сценарии

Локальная разработка и подключение нового участника

Задача: дать новому участнику воспроизводимый запуск проекта без передачи токенов в открытом виде. Условие: в репозитории есть mise.toml, зашифрованный dev-конфиг и выдана dev-identity. Действие: участник устанавливает инструменты и запускает стандартную задачу через mise. Результат: проект получает выбранные версии инструментов, а секреты доступны только дочернему процессу. Ограничение: используйте dev или sandbox, а не production-доступ.

Dev, staging и sandbox

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

CI/CD

Задача: запускать тесты, сборку и проверочные задачи без ручной передачи секретов. Условие: CI получает отдельную identity через защищённое хранилище pipeline. Действие: передавайте секреты через exec-env и запускайте только разрешённые задачи. Результат: значения доступны процессу задачи и не попадают в репозиторий как открытый текст. Ограничение: не выводите секреты в логи и не используйте команды, печатающие всё окружение.

Безопасные проверочные запуски

Задача: проверить интеграцию с внешней системой без записи в рабочие данные. Условие: есть sandbox-секреты и команда с режимом --dry-run или другим явно проверочным режимом. Действие: запускайте эту команду через mise и SOPS. Результат: можно проверить конфигурацию и права доступа по наблюдаемому выводу. Ограничение: название и поведение проверочного флага зависят от вашего проекта, их нужно подтвердить отдельно.

Разделение доступов по контурам

Задача: не выдавать разработчику или сервису лишние права. Условие: каждый контур имеет собственный набор recipients. Действие: добавляйте identity только в правила нужных файлов. Результат: доступ можно отозвать на уровне конкретного контура. Ограничение: удаление recipient не заменяет ротацию самих секретов после компрометации.

Что не хранить в project-level сейфе

Даже зашифрованный репозиторий не заменяет управление особо чувствительными доступами. Отдельно храните:

  • приватные age-identities;
  • корневые SSH-ключи;
  • личные ключи разработчиков;
  • account-wide административные токены;
  • production runtime-секреты, если для них принят server secret store или KMS.

Для сервисов создавайте отдельные технические учётные записи с минимальными правами. Процессы добавления, отзыва доступа и ротации документируйте до подключения production.

С чего начать пилот

Выберите некритичный проект: внутренний devtool, sandbox-интеграцию или staging-пайплайн. Добавьте один зашифрованный файл, отдельную identity для CI и две задачи mise: безопасный проверочный запуск и обычный локальный запуск.

Переход к production имеет смысл только после проверки ротации, журналирования, ограничений CI и сценария компрометации ключа.

📌
В репозитории находятся инструкция запуска, публичные recipients и зашифрованный конфиг. Приватные identities хранятся отдельно, а доступ отзывается вместе с ротацией затронутых секретов.

Официальные ресурсы

Если в команде накопились .env-файлы на разных машинах или токены передаются вручную, начните с одного sandbox-контура и проверьте весь цикл: выдачу доступа, запуск, отзыв и ротацию.

Следующий шаг

Для следующего шага в CI полезно сверить границы доступа и проверочный запуск с руководством по DevSecOps и CI/CD.

Связанные материалы

Если вы внедряете связку в команде, полезно отдельно обсудить границы CI-доступа и ротацию секретов.

Если захотите обсудить, как это применить у себя или в команде — пишите в Telegram @pimenov