Как хранить секреты: .env, direnv, git-crypt и pass

Как разделить общую конфигурацию, личные доступы, командные и production-секреты — и проверять их без раскрытия значений.

Запутанный клубок конфигурации проходит через контрольную точку и разделяется на четыре защищённых контура

Я использую эту модель в проектах, где разработчики и AI-агенты работают через один и тот же интерфейс командной строки (CLI): запускают одни и те же текстовые команды в терминале. Она решает две задачи: новый участник понимает, откуда берётся окружение, а секрет не путешествует вместе с инструкцией, логом или сообщением агенту.

В результате у каждого значения появляется ответ на четыре вопроса:

  1. Кто владеет значением?
  2. Кому разрешено его читать?
  3. Как оно попадает в процесс?
  4. Как проверить настройку, не печатая само значение?

Подход: сначала разделите значения по ролям

Файл с переменными окружения — это формат, а не модель безопасности. Если сложить в один .env адрес тестового стенда, личный токен разработчика и пароль production-базы, команда либо начнёт скрывать безобидную конфигурацию, либо однажды закоммитит секрет.

Я разделяю значения так:

СлойПримерыГде хранитьВ Git?
Общая конфигурацияURL сервисов, имена окружений, режимы запуска.env или .env.sharedДа, открытым текстом
Личные локальные значенияПользовательский порт, флаг или override для разработки.env.local; секреты здесь — только простой fallbackНет, файл игнорируется
Личные секретыПерсональный API-токен, пароль, приватный доступpass вне проекта; именованная запись загружается через .envrcНет
Командные секретыОбщий read-only токен, доступ к тестовому стендуЗашифрованный .env.secrets, если команде подходит модель общего файлаДа, только зашифрованным
Production-секретыКлючи подписи, боевые пароли, персональные данныеVault или облачный secret manager1Нет
  1. Это не файл в репозитории, а централизованный сервис хранения секретов. Он выдаёт их авторизованным людям и программам по правилам доступа, ведёт журнал обращений и позволяет отозвать доступ без изменения каждого репозитория. В рамках этого гайда это граница следующего уровня, а не часть базовой настройки.

direnv стоит рядом с этими слоями, но не является хранилищем. Это точка сборки окружения: при входе в рабочую директорию он вычисляет переменные из проверенного .envrc, а при выходе удаляет их из командной оболочки (shell) — программы, которая принимает команды в терминале и запускает другие программы.

Ниже — назначение каждого файла в этой модели.

.env: общая несекретная конфигурация

В версионируемом .env удобно держать значения, которые нужны всем и сами по себе не дают доступ:

.env
LOGS_URL=https://logs.example.test
API_BASE_URL=https://api.example.test

Если в команде принято игнорировать любой файл с именем .env, назовите общий файл .env.shared или храните только пример .env.example. Важен контракт, а не имя: общий файл можно спокойно показать в pull request — запросе на включение изменений в основную ветку.

.env.local: личные значения

.env.local находится в .gitignore. Каждый участник создаёт его на своей машине и хранит там личные значения, если для проекта пока не настроен отдельный менеджер паролей. Например:

.env.local
PERSONAL_API_TOKEN=replace-with-your-personal-token

Это только заглушка формата, не настоящее значение. Сам файл не должен попадать в Git: его имя добавляют в .gitignore. Такой способ прост, но плохо подходит для общей выдачи и отзыва доступов. Чем больше команда, тем дороже ручное первоначальное подключение и проверка актуальности.

.env.secrets: общий зашифрованный файл

.env.secrets можно версионировать только тогда, когда Git сохраняет его в зашифрованном виде. Внутри находятся общие секреты небольшой доверенной команды, например доступы к тестовому стенду:

.env.secrets
STAGING_API_TOKEN=replace-with-shared-token
REGISTRY_PASSWORD=replace-with-shared-password

Это заглушки формата: реальные значения в документацию не копируют. Один из простых способов зашифровать такой файл — git-crypt. Правило шифрования в .gitattributes нужно добавить до первого коммита .env.secrets. После настройки авторизованный разработчик видит обычный текст в рабочей директории, а в Git попадает зашифрованный blob.

У такого решения есть жёсткие границы:

  • git-crypt не скрывает имя файла, размер, историю изменений и другие метаданные;
  • выданный доступ нельзя считать отозванным задним числом: у человека могла остаться копия ключа или прежней истории;
  • один общий файл обычно даёт доступ сразу ко всем значениям внутри;
  • потеря ключей без проверенной резервной копии может сделать расшифрование невозможным;
  • Git-фильтр нужно проверять: наличие строки в .gitattributes само по себе не доказывает, что конкретный commit не содержит открытый текст.

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

pass: личное хранилище вне проекта

pass хранит каждую запись в отдельном GPG-зашифрованном файле вне рабочего репозитория. Имена можно организовать иерархически:

project/
├── analytics/read-token
├── cloud/personal-token
└── registry/password

В инструкции достаточно назвать путь записи и требуемую переменную — значение там появляться не должно. Например: «для REGISTRY_PASSWORD нужна запись project/registry/password».

Чтобы добавить личный токен, выполните интерактивную команду — значение не нужно помещать в аргументы или историю shell:

pass insert project/personal-api-tokenEnter password for project/personal-api-token:Retype password for project/personal-api-token:

Прочитать запись в терминале или временно скопировать её первую строку в буфер обмена можно так:

pass show project/personal-api-token[значение секрета скрыто]pass -c project/personal-api-tokenCopied project/personal-api-token to clipboard. Will clear in 45 seconds.

Первая команда печатает секрет на экран, поэтому её не запускают в логах, демонстрациях и сессиях агента. pass -c не печатает значение и автоматически очищает буфер обмена через короткое время.

Я не загружаю весь pass в окружение автоматически. Безопаснее, когда конкретная операция читает только нужную запись в момент запуска и передаёт её потребителю через стандартный ввод, защищённый временный файл или окружение дочернего процесса.

.envrc: агрегатор переменных окружения

.envrc — исполняемый сценарий сборки, а не ещё одно хранилище. direnv запускает его в отдельном Bash-процессе, собирает изменения окружения и передаёт их в текущий shell. Приложения, запущенные из этого shell, наследуют собранные переменные.

Базовый .envrc объединяет общую конфигурацию, личный файл, командные секреты и при необходимости одну конкретную запись из pass:

.envrc
strict_env

dotenv_if_exists .env
dotenv_if_exists .env.shared
dotenv_if_exists .env.local
dotenv_if_exists .env.secrets

watch_file "${PASSWORD_STORE_DIR:-$HOME/.password-store}/project/personal-api-token.gpg"
export PERSONAL_API_TOKEN="$(pass show project/personal-api-token)"

Функция dotenv_if_exists загружает файл только при его наличии. Порядок строк задаёт порядок сборки, поэтому одну переменную лучше не определять сразу в нескольких слоях. watch_file добавляет зашифрованную запись pass в список наблюдения: после её изменения direnv пересоберёт окружение при следующем появлении prompt. Последняя строка уместна, только если токен нужен большинству команд проекта и запись pass содержит одно значение без дополнительного текста.

Секрет из pass, экспортированный через .envrc, будет доступен всем дочерним процессам этого shell. Для единичной команды безопаснее не добавлять его в агрегатор, а читать запись в отдельной обёртке непосредственно перед запуском потребителя.

Настройка: установка и подключение

1. Установите инструменты

Для минимальной схемы нужны четыре утилиты:

  • direnv — загружает окружение из проверенного .envrc при входе в директорию;
  • git-crypt — шифрует выбранные файлы при записи в Git;
  • GnuPG (gpg) — управляет личными ключами для git-crypt и pass;
  • pass — хранит личные секреты вне проекта в GPG-зашифрованных файлах.
brew install direnv git-crypt gnupg pass

После установки direnv нужно один раз подключить к shell. Выберите свою оболочку и добавьте соответствующую строку:

~/.config/fish/config.fish
direnv hook fish | source

Перезапустите shell и проверьте команды без вывода секретов:

direnv version2.37.1git-crypt versiongit-crypt 0.8.0gpg --versiongpg (GnuPG) 2.5.22pass --versionpass: the standard unix password manager, v1.7.4

Номера версий в выводе могут отличаться. Важно, чтобы каждая команда завершилась без ошибки.

pass требует личный GPG-ключ. Если ключ уже создан или импортирован, найдите его идентификатор и инициализируйте хранилище:

gpg --list-secret-keys --keyid-format=longsec   ed25519/0123456789ABCDEF 2026-09-07 [SC]uid   [ultimate] Developer <developer@example.test>pass init <GPG_KEY_ID>Password store initialized for <GPG_KEY_ID>

В примере показаны вымышленные идентификатор и адрес. В команду pass init подставляют идентификатор своего секретного ключа из предыдущего вывода.

Если секретного ключа ещё нет, сначала создайте его:

gpg --full-generate-keyPlease select what kind of key you want:

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

2. Сделайте .env.secrets зашифрованным

Начинайте в чистой рабочей директории Git. Инициализируйте git-crypt и добавьте свой публичный GPG-ключ как первого получателя:

git-crypt initGenerating key...git-crypt add-gpg-user <GPG_KEY_ID>[main …] Add 1 git-crypt collaborator

Команда git-crypt add-gpg-user создаёт отдельный commit с файлом ключа, зашифрованным для получателя. Затем запишите правило в .gitattributes — это содержимое файла, а не shell-команда:

.gitattributes
.env.secrets filter=git-crypt diff=git-crypt

Сначала отдельно зафиксируйте правило:

git add .gitattributesgit commit -m "Configure git-crypt for shared secrets"[main …] Configure git-crypt for shared secrets

Только после этого создайте .env.secrets в редакторе и внесите реальные значения. На общей машине дополнительно ограничьте права файла, затем добавьте его в Git:

chmod 600 .env.secretsgit add .env.secrets

Перед коммитом проверьте настройку, не печатая содержимое файла:

git check-attr filter -- .env.secrets.env.secrets: filter: git-cryptgit-crypt status -eencrypted: .env.secretsgit diff --cached --check

Первая команда должна показать фильтр git-crypt, а вторая — .env.secrets среди зашифрованных файлов. Не используйте cat, env или git show как проверку: при ошибке настройки они могут вывести секрет открытым текстом.

Если проверки прошли, зафиксируйте зашифрованный файл:

git commit -m "Add encrypted shared secrets"[main …] Add encrypted shared secrets

3. Настройте .envrc как точку сборки

Создайте .envrc по базовому примеру из раздела о подходе и удалите строки для слоёв, которых в проекте нет. Сам .envrc храните в Git: команда должна видеть и проверять логику сборки окружения.

Разработчик читает .envrc и только после этого разрешает его выполнение:

direnv allowdirenv: loading .envrc

Это важная граница: .envrc — исполняемый shell-код. Команда direnv allow означает не «загрузить настройки», а «я проверил этот код и разрешаю ему выполняться при входе в директорию».

Я стараюсь не помещать в .envrc сетевые операции, изменение password store и чтение десятков секретов. Иначе обычный cd становится медленным, недетерминированным и получает лишние полномочия.

Для постоянной защиты нужен pre-commit или pre-push hook, который проверяет зашифрованность blob в Git index, а также сканер случайных секретов. Hook нельзя обходить через --no-verify только потому, что commit «срочный».

4. Подключите нового участника

Хорошая схема доступа описывает не только хранение, но и onboarding:

  1. Новый участник устанавливает утилиты, создаёт или импортирует личную пару GPG-ключей и передаёт администратору только публичный ключ.
  2. Администратор в чистой рабочей директории добавляет публичный ключ получателя и отправляет созданный commit.
  3. Участник клонирует или обновляет репозиторий и расшифровывает разрешённые ему файлы.
  4. Участник проверяет .envrc, затем разрешает его выполнение.
  5. Диагностическая команда проверяет наличие переменных и доступность систем, но не печатает значения.

На машине администратора:

git-crypt add-gpg-user <GPG_KEY_ID>[main …] Add 1 git-crypt collaborator

На машине нового участника:

git-crypt unlockdirenv allowdirenv: loading .envrc

Приватный GPG-ключ остаётся у владельца. Его не отправляют администратору, не кладут в Git и не передают через чат.

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

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

Каждый независимый репозиторий должен владеть своей схемой окружения: в нём версионируются собственные .envrc, .env или .env.shared, .gitattributes и зашифрованный .env.secrets. Не делайте общий .envrc, который при входе в одну директорию загружает окружение соседних репозиториев: разработчик должен явно войти в нужный проект и разрешить именно его сценарий. Единая корневая точка сборки уместна для монорепозитория, но не для нескольких самостоятельных проектов.

Ключ git-crypt по умолчанию тоже отдельный для каждого репозитория. Повторное использование одного ключа возможно для одной стабильной группы получателей, но расширяет последствия его утечки: скомпрометированный ключ откроет секреты сразу нескольких проектов. Записи pass удобно именовать по организации и репозиторию, например rocketwash/backoffice/personal-api-token; в документации указывают этот путь и имя переменной, но не значение.

Как выбрать хранилище

СитуацияПрактичный выбор
Личные токены для постоянной работыpass
Временный локальный прототип, пока pass не настроенИгнорируемый .env.local; после стабилизации перенести секреты в pass
Небольшая доверенная команда, общие read-only доступыgit-crypt и отдельный .env.secrets
Нужны две-три стабильные группы доступа внутри командыНесколько файлов и именованные ключи git-crypt

Пример: несколько файлов и разные получатели

Само разделение одного .env.secrets на несколько файлов ещё не разделяет доступ. Если все файлы используют один ключ git-crypt, каждый получатель этого ключа сможет расшифровать их все. Для двух-трёх устойчивых групп git-crypt поддерживает именованные ключи: один ключ соответствует одной группе получателей и своему набору файлов.

Например, секреты можно разделить по операционным ролям.

.env.secrets.development
DEV_API_TOKEN=replace-with-development-token
.env.secrets.operations
DEPLOY_READ_TOKEN=replace-with-operations-token
.env.secrets.finance
BILLING_READ_TOKEN=replace-with-finance-token

В простом варианте с одним ключом фильтр всегда называется git-crypt. Здесь доступ нужно разделить между тремя группами, поэтому ниже создаются именованные ключи через git-crypt init -k <ИМЯ>. Для такого ключа официальный синтаксис добавляет имя к фильтру: git-crypt-<ИМЯ>. Поэтому ключу development соответствует filter=git-crypt-development, а не обычный filter=git-crypt.

В .gitattributes каждому файлу назначается фильтр его ключа:

.gitattributes
.env.secrets.development filter=git-crypt-development diff=git-crypt-development
.env.secrets.operations filter=git-crypt-operations diff=git-crypt-operations
.env.secrets.finance filter=git-crypt-finance diff=git-crypt-finance

Ключи и первые получатели настраиваются отдельно:

git-crypt init -k developmentGenerating key...git-crypt init -k operationsGenerating key...git-crypt init -k financeGenerating key...git-crypt add-gpg-user -k development <DEVELOPER_GPG_FINGERPRINT>[main …] Add 1 git-crypt collaboratorgit-crypt add-gpg-user -k operations <OPERATOR_GPG_FINGERPRINT>[main …] Add 1 git-crypt collaboratorgit-crypt add-gpg-user -k finance <FINANCE_GPG_FINGERPRINT>[main …] Add 1 git-crypt collaborator

Чтобы добавить в группу ещё одного человека, повторите git-crypt add-gpg-user с тем же -k, но с отпечатком его публичного GPG-ключа. В .envrc не нужно безусловно загружать все три файла: участник выбирает доступный ему профиль через игнорируемый .env.local.

.env.local
SECRETS_PROFILE=development
.envrc
if [[ -n "${SECRETS_PROFILE:-}" ]]; then
  dotenv_if_exists ".env.secrets.${SECRETS_PROFILE}"
fi

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

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

Частые ошибки

«Добавим .env в .gitignore — и всё»

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

«Если файл зашифрован, его можно отправлять куда угодно»

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

Когда уместен pass show в .envrc

Это нормальная практика, если секрет нужен большинству команд проекта. direnv читает запись pass при загрузке или перезагрузке .envrc, экспортирует результат в текущий shell и использует это окружение, пока директория или наблюдаемый файл не изменится. Поэтому в базовом примере запись pass добавлена через watch_file: после обновления токена окружение пересоберётся при следующем prompt. Если наблюдение не настроено, после ротации явно выполните direnv reload. Следует лишь помнить, что переменную унаследуют все дочерние процессы. Для секрета, нужного одной редкой операции, точечная команда-обёртка сужает область доступа.

«Попросим агента проверить, какое там значение»

Проверять нужно доступность и результат операции, а не раскрывать секрет в чате. Команда диагностики должна отвечать «запись читается» или «доступ отклонён», не выводя токен, пароль или приватный ключ.

Проверка готовности схемы

Перед тем как считать работу законченной, проверьте:

  • общая конфигурация отделена от значений, дающих доступ;
  • личные секреты не попадают в общий репозиторий;
  • для каждого командного секрета известны владелец, получатели и способ отзыва;
  • чистый clone без ключа не раскрывает .env.secrets;
  • CI не получает ключ расшифрования без доказанной необходимости;
  • диагностика проверяет наличие и права без печати значений;
  • hooks блокируют plaintext в защищённых путях;
  • резервная копия ключей существует и её восстановление проверено;
  • production-секреты имеют отдельный операционный контур;
  • инструкция описывает onboarding, ротацию и отзыв доступа.

Что взять как стартовый контракт

Для небольшого проекта я начинаю с такой схемы:

.envrc          # собирает окружение; секреты не хранит
.env            # общая несекретная конфигурация
.env.local      # личные значения; ignored
.env.secrets    # командные значения; encrypted and tracked
.gitattributes  # правила git-crypt
.gitignore      # локальные и временные файлы

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

Ссылки и документация

Нужно выстроить безопасный рабочий контур для команды и AI-агентов?

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

Пройти диагностику или посмотреть форматы работы