pimenov.ai

База знаний

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

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

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

Связка mise + sops + age решает две практические задачи в проектах команды: воспроизводимый запуск окружения и безопасное хранение project-level секретов. В репозиторий коммитится зашифрованный конфиг, приватный ключ для расшифровки живёт отдельно — у разработчика, в Keychain, 1Password или CI secrets.

Что это такое

mise — менеджер версий и команд проекта. В одном файле mise.toml (старый вариант имени .mise.toml тоже поддерживается) вы фиксируете, какие Node, Python, pnpm нужны проекту, и описываете команды вроде mise run dev или mise run smoke. Заменяет nvm, pyenv, asdf и зоопарк самописных скриптов. Новый человек клонирует репозиторий, делает mise install — и у него ровно те же версии и те же команды, что у вас.

sops — инструмент шифрования конфигов: изначально проект Mozilla, сейчас его поддерживает сообщество getsops под эгидой CNCF. Берёт ваш YAML или JSON и шифрует только значения, оставляя ключи открытыми. Diff в Git остаётся читаемым: видно, какое поле поменялось, а само значение зашифровано. Файл secrets/dev.sops.yaml спокойно лежит в репозитории — без приватного ключа из него ничего не достать.

age — современный инструмент шифрования файлов. Он закрывает тот же сценарий, что и GPG при шифровании, но сознательно не делает подписи, keyring и веб-доверие. Простой формат ключей и понятный CLI. Публичный ключ представляет собой одну короткую строку, её можно коммитить рядом с проектом. Приватный ключ живёт у каждого человека отдельно: в Keychain, 1Password или в файле с правильными правами. Именно age решает, кто имеет право расшифровать sops-файл.

Как они работают вместе. mise запускает проект, в нужный момент дёргает sops, тот через age расшифровывает секреты и подсовывает их в окружение. Снаружи это одна команда mise run dev. Внутри — три инструмента, каждый отвечает за свой слой: окружение, формат конфига, криптография.

Установка

Все три инструмента ставятся одной строкой через Homebrew (на Linux — через пакетные менеджеры или бинарники с GitHub):

brew install mise sops age

После установки создайте пару ключей age и положите её туда, где sops ищет ключ по умолчанию:

age-keygen -o ~/.config/sops/age/keys.txt
# в файле будет и публичный ключ (age1...), и приватный (AGE-SECRET-KEY-1...)

Публичный ключ (строка age1...) идёт в .sops.yaml как recipient. Приватный ключ остаётся только у владельца: в ~/.config/sops/age/keys.txt, в Keychain или 1Password. sops находит его автоматически по этому пути или по переменной SOPS_AGE_KEY_FILE.

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

В этой связке три инструмента закрывают три разных слоя проекта.

  • mise отвечает за окружение и команды: версии Node, Python, pnpm, ENV-переменные, задачи dev, test, build, smoke.
  • sops отвечает за зашифрованные файлы конфигурации: YAML, JSON, .env-подобные структуры, которые безопасно лежат в репозитории.
  • age отвечает за саму криптографию: публичный ключ коммитится в проект, приватный ключ хранится вне репозитория.

Идея простая: репозиторий перестаёт быть местом, куда «случайно попал .env». Там лежит инструкция запуска и зашифрованный сейф, а право открыть этот сейф у конкретных людей и сервисов.

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

Пройдите по списку перед тем, как внедрять связку в команде.

В проекте есть mise.toml с фиксированными версиями инструментов.
mise tasks показывает понятный набор команд (dev, test, sync:dry, build, smoke).
В репозитории нет ни одного незашифрованного файла с реальными токенами.
Секреты лежат в secrets/<contour>.sops.yaml с разделением по контурам.
У каждого контура свой список recipients в .sops.yaml.
Публичные age-ключи команды собраны в одном файле и закоммичены.
Приватные age-ключи разработчиков хранятся в Keychain или 1Password, а не в репозитории.
CI получает отдельный age-ключ с правами только на свой контур.
Описан процесс добавления нового человека: какой ключ выдать, какие файлы перешифровать.
Описан процесс отзыва доступа: ротация ключа и перешифровка секретов.
Production-секреты не лежат в репозитории — даже зашифрованными.
В README зафиксировано, какие команды безопасные, а какие требуют отдельного approval.

Роли инструментов

mise — воспроизводимое окружение

mise фиксирует версии runtime и описывает команды проекта в одном файле. Новый человек открывает репозиторий и запускает mise install плюс одну понятную команду вместо ручной установки Node, pnpm и Python нужных версий.

sops — зашифрованные конфиги

sops шифрует значения в YAML или JSON, оставляя ключи в открытом виде. Diff в Git остаётся читаемым: видно, какое поле изменилось, а само значение зашифровано. Файлы вида secrets/dev.sops.yaml спокойно коммитятся в репозиторий.

age — криптография под капотом

age отвечает за криптографию: тот же сценарий, что у GPG при шифровании файлов, но без подписей, keyring и веб-доверия. Простой формат ключей и понятный CLI. Публичный ключ лежит в проекте рядом с .sops.yaml, приватный хранится у владельца — в Keychain, 1Password или в файле ~/.config/sops/age/keys.txt.

Как mise подключает секреты

У связки два рабочих способа отдать расшифрованные значения в окружение.

Способ 1. Обёртка sops exec-env. mise-задача вызывает sops, тот расшифровывает файл и запускает команду с секретами в окружении. Подходит, когда нужен полный контроль над форматом файла и моментом расшифровки:

[tasks.dev]
run = "sops exec-env secrets/dev.sops.yaml 'pnpm dev'"

Способ 2. Нативная интеграция mise. mise умеет сам читать sops-зашифрованный файл и отдавать значения как переменные окружения через _.file, без ручного вызова sops:

[env]
_.file = ".env.json"        # mise сам расшифрует, если файл зашифрован sops

[settings]
sops.age_recipients = "age1devkey..."
⚖️
Нативная интеграция mise с sops помечена как experimental и поддерживает только форматы .env.json, .env.yaml, .env.toml и бэкенд age (без облачных KMS на части платформ). Для нестандартных форматов и KMS оставайтесь на sops exec-env.

Варианты применения в команде

1. Быстрый запуск проекта новым человеком

В проект кладётся mise.toml с версиями инструментов и понятными задачами. Новый разработчик клонирует репозиторий, получает age-ключ, и через десять минут уже запускает mise run dev или mise run sync:dry.

2. Зашифрованные dev и staging секреты

В репозитории живут secrets/dev.sops.yaml и secrets/staging.sops.yaml. Расшифровать их может только человек или CI с подходящим приватным ключом. Если ключ скомпрометирован, он отзывается, файлы перешифровываются, история в Git остаётся чистой.

3. Безопасные dry-run и sandbox сценарии

Для интеграций с Notion, Telegram, Directus, OpenAI и CRM удобно держать отдельные sandbox-секреты. Это даёт возможность проверять скрипты без доступа к production-данным и без риска случайных live writes.

4. Управляемый CI/CD

CI получает свой отдельный ключ расшифровки и запускает только разрешённые задачи: тесты, сборку, sync:dry, smoke. Секреты не лежат в репозитории как простой текст и не передаются вручную между людьми.

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

Один файл — один контур: dev, staging, production, client-demo, sandbox. У каждого контура свой набор recipients. Разработчик читает dev-секреты, но не production. CI читает только то, что ему нужно для своей задачи.

6. Операционный стандарт команды

Связка помогает зафиксировать рабочую дисциплину: где лежат секреты, кто имеет доступ, как добавить нового человека, как отозвать доступ, как ротировать ключи и какие команды считаются безопасными для запуска без approval.

Что НЕ класть в project secrets

В project-level сейфе не должно быть глобальных админских токенов, root SSH, личных ключей разработчиков, account-wide доступов Cloudflare, приватных age identities и production-секретов без отдельного процесса ротации. Для таких вещей подходят:

  • Keychain или 1Password — для личных и админских ключей.
  • CI secrets — для токенов, которыми пользуется только pipeline.
  • Server secret store — для production runtime.
  • Отдельный технический пользователь с минимальными правами — вместо того чтобы выдавать сервису ключи владельца аккаунта.

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

Минимальная рабочая структура для команды.

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

Пример mise.toml с базовыми задачами:

[tools]
node = "20"
pnpm = "9"
python = "3.12"

[env]
PROJECT_ENV = "dev"

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

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

[tasks.smoke]
description = "Smoke-тесты после деплоя"
run = "pnpm smoke"

Пример .sops.yaml, разделяющий доступ по контурам:

creation_rules:
  - path_regex: secrets/dev\.sops\.yaml$
    age: >-
      age1devkey...,
      age1cikey...
  - path_regex: secrets/staging\.sops\.yaml$
    age: >-
      age1leadkey...,
      age1cikey...

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

Начинайте не с production-критичных систем. Хорошие первые проекты для пилота:

  • Контентные пайплайны и Notion-to-site синхронизация.
  • Sandbox-боты и staging-интеграции.
  • Внутренние devtools и скрипты команды.
  • Тестовые контуры новых интеграций до выхода в production.

Production runtime и клиентские live-секреты стоит переводить только после отдельного preflight и плана ротации.

📌
Практическое правило. В репозитории живёт инструкция и зашифрованный сейф. Ключ от сейфа живёт вне репозитория. mise запускает проект в правильном порядке, sops открывает зашифрованный конфиг там, где есть право доступа, age определяет, у кого это право есть.

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

По теме

Если в команде уже накопились проекты с .env-файлами по разным машинам или ручной передачей токенов в личке, имеет смысл проверить на одном пилоте, насколько mise + sops + age упрощает жизнь.

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