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

Я использую эту модель в проектах, где разработчики и AI-агенты работают через один и тот же интерфейс командной строки (CLI): запускают одни и те же текстовые команды в терминале. Она решает две задачи: новый участник понимает, откуда берётся окружение, а секрет не путешествует вместе с инструкцией, логом или сообщением агенту.
В результате у каждого значения появляется ответ на четыре вопроса:
- Кто владеет значением?
- Кому разрешено его читать?
- Как оно попадает в процесс?
- Как проверить настройку, не печатая само значение?
Подход: сначала разделите значения по ролям
Файл с переменными окружения — это формат, а не модель безопасности. Если сложить в один .env адрес тестового стенда, личный токен разработчика и пароль production-базы, команда либо начнёт скрывать безобидную конфигурацию, либо однажды закоммитит секрет.
Я разделяю значения так:
| Слой | Примеры | Где хранить | В Git? |
|---|---|---|---|
| Общая конфигурация | URL сервисов, имена окружений, режимы запуска | .env или .env.shared | Да, открытым текстом |
| Личные локальные значения | Пользовательский порт, флаг или override для разработки | .env.local; секреты здесь — только простой fallback | Нет, файл игнорируется |
| Личные секреты | Персональный API-токен, пароль, приватный доступ | pass вне проекта; именованная запись загружается через .envrc | Нет |
| Командные секреты | Общий read-only токен, доступ к тестовому стенду | Зашифрованный .env.secrets, если команде подходит модель общего файла | Да, только зашифрованным |
| Production-секреты | Ключи подписи, боевые пароли, персональные данные | Vault или облачный secret manager1 | Нет |
Это не файл в репозитории, а централизованный сервис хранения секретов. Он выдаёт их авторизованным людям и программам по правилам доступа, ведёт журнал обращений и позволяет отозвать доступ без изменения каждого репозитория. В рамках этого гайда это граница следующего уровня, а не часть базовой настройки. ↩
direnv стоит рядом с этими слоями, но не является хранилищем. Это точка сборки окружения: при входе в рабочую директорию он вычисляет переменные из проверенного .envrc, а при выходе удаляет их из командной оболочки (shell) — программы, которая принимает команды в терминале и запускает другие программы.
Ниже — назначение каждого файла в этой модели.
.env: общая несекретная конфигурация
В версионируемом .env удобно держать значения, которые нужны всем и сами по себе не дают доступ:
.envLOGS_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.localPERSONAL_API_TOKEN=replace-with-your-personal-token
Это только заглушка формата, не настоящее значение. Сам файл не должен попадать в Git: его имя добавляют в .gitignore. Такой способ прост, но плохо подходит для общей выдачи и отзыва доступов. Чем больше команда, тем дороже ручное первоначальное подключение и проверка актуальности.
.env.secrets: общий зашифрованный файл
.env.secrets можно версионировать только тогда, когда Git сохраняет его в зашифрованном виде. Внутри находятся общие секреты небольшой доверенной команды, например доступы к тестовому стенду:
.env.secretsSTAGING_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:
.envrcstrict_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
sudo apt updatesudo apt install direnv git-crypt gnupg pass
Для Windows рекомендуется WSL с Ubuntu. Нативный вариант здесь намеренно не предлагается: pass ориентирован на Unix, а поддержка Windows в git-crypt остаётся экспериментальной.
Откройте PowerShell от имени администратора:
wsl --install
После перезагрузки и первоначальной настройки Ubuntu выполните внутри WSL:
sudo apt updatesudo apt install direnv git-crypt gnupg pass
После установки direnv нужно один раз подключить к shell. Выберите свою оболочку и добавьте соответствующую строку:
~/.config/fish/config.fishdirenv hook fish | source
~/.zshrceval "$(direnv hook zsh)"
~/.bashrceval "$(direnv hook bash)"
Перезапустите 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:
- Новый участник устанавливает утилиты, создаёт или импортирует личную пару GPG-ключей и передаёт администратору только публичный ключ.
- Администратор в чистой рабочей директории добавляет публичный ключ получателя и отправляет созданный commit.
- Участник клонирует или обновляет репозиторий и расшифровывает разрешённые ему файлы.
- Участник проверяет
.envrc, затем разрешает его выполнение. - Диагностическая команда проверяет наличие переменных и доступность систем, но не печатает значения.
На машине администратора:
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.developmentDEV_API_TOKEN=replace-with-development-token
.env.secrets.operationsDEPLOY_READ_TOKEN=replace-with-operations-token
.env.secrets.financeBILLING_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.localSECRETS_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-секреты выносятся в хранилище с ролями, аудитом и отзывом. Структуру файлов можно поменять; четыре вопроса из начала статьи должны остаться: кто владеет значением, кто его читает, как оно попадает в процесс и как это проверить без раскрытия.
Ссылки и документация
- Установка direnv и подключение к shell — пакеты, бинарные сборки и hook для Fish, Zsh и других оболочек.
- Стандартная библиотека direnv —
dotenv_if_exists,strict_envи другие функции для.envrc. - git-crypt, инструкция по установке и именованные ключи — настройка
.gitattributes, группы GPG-получателей, команды unlock и ограничения модели. - Загрузка GnuPG — GPG-реализация для ключей
git-cryptиpass. - pass: the standard Unix password manager — локальное GPG-зашифрованное хранилище с иерархией записей.
- Установка WSL — официальный путь Microsoft для запуска Ubuntu и Unix-утилит на Windows.