Перемикання між варіантами .env (local, staging, production) та синхронізація між машинами через age-шифрування.
curl -fsSL https://raw.githubusercontent.com/dmythro/envs/main/envs -o ~/.local/bin/envs
chmod +x ~/.local/bin/envs
envs setup Один і той самий проєкт у мене лежить і на ноутбуці, і на десктопі, і щоразу після переходу з машини на машину я втрачав хвилин десять на .env. На якому варіанті зараз цей? Чи URL стейджингової бази взагалі переїхав, чи так і лишився на іншій машині? Єдиний файл, потрібний усім інструментам, — це той самий файл, який не можна закомітити, тож він існує рівно в одному місці, і ніколи в тому, за яким ти зараз сидиш.
envs — це bash-скрипт, який це припинив. Він шифрує варіанти .env вашим наявним SSH-ключем і кладе їх у теку, яка вже синхронізується.
Приклади
envs push staging # зашифрувати поточний .env і зберегти як "staging"
envs use production # розшифрувати "production" поверх .env
envs diff staging # порівняти локальний .env зі збереженим варіантом
envs list # усі варіанти для цього репозиторію
envs projects # усі репозиторії, де є варіанти
Це весь інструмент. Ні демона, ні хука в оболонці, ні обгортки над командою запуску — .env лишається звичайним файлом, який усі читають як завжди.
Навіщо це
Усі альтернативи вимагали того, чого я не хотів віддавати. Комітити зашифровані секрети в репозиторій означає, що шифротекст назавжди лишається в історії, а будь-яка ротація — це ще один коміт. Сервіс секретів означає підписку, логін і мережевий запит перед стартом дев-сервера. Для команди обидва варіанти правильні; для однієї людини, яка переходить між двома своїми машинами, обидва абсурдні.
Мені була потрібна картотека: іменовані варіанти, по проєктах, зашифровані, у теці, яку мої машини й так синхронізують. Це сто рядків bash, а не платформа.
Чим відрізняється
| Інструмент | Модель |
|---|---|
envs | іменовані варіанти в синхронізованому сховищі, зашифровані вашим SSH-ключем, поза репозиторієм |
| dotenvx | шифрує .env на місці, шифротекст комітиться разом з кодом |
| SOPS | шифрує структуровані файли через KMS/age/PGP, зазвичай теж у репозиторії |
| direnv | автозавантаження змінних по каталогах — це про активацію, не про зберігання |
| 1Password CLI | підставляє секрети зі сховища в момент запуску |
Якщо ви в команді — беріть інше. Це чесне обмеження: один SSH-ключ означає, що немає ні відкликання доступу, ні процесу ротації, ні журналу аудиту. Якщо людина йде з команди, вимикати нічого. Для команди правильний інструмент — SOPS з окремими age-отримувачами або справжній vault, і я сам так і робив би. envs розрахований на секрети, які й так тільки ваші.
direnv взагалі не конкурент — він відповідає на питання «коли це завантажується», а не «де це лежить». Вони чудово поєднуються.
Що всередині
Проєкт визначається за git remote
Варіанти зберігаються в каталозі, названому за нормалізованим git remote URL, тож той самий репозиторій, клонований в інший шлях на іншій машині, потрапляє в ту саму теку без жодних налаштувань. Якщо remote змінився — envs mv <old-id> перенаправить.
Захист від перезапису
envs use перезаписує локальний .env, тому спершу рахує хеш файлу й порівнює з тим, що було відправлено востаннє. Локальні правки, які ви не встигли запушити, дають попередження замість тихої втрати даних:
envs use staging
# .env has unpushed changes — push them first, or re-run with --force
Дві налаштовувані змінні
ENVS_STORE_DIR # типово: ~/Library/Mobile Documents/com~apple~CloudDocs/envs
ENVS_SSH_KEY # типово: ~/.ssh/id_ed25519
Спрямуйте ENVS_STORE_DIR на Syncthing, Dropbox, змонтований мережевий диск чи приватний git-репозиторій — скрипту байдуже, що саме забезпечує синхронізацію.
Нотатки про рішення
age замість GPG. GPG це вміє, і я пробував. Його модель з pinentry та агентом ненадійна поза інтерактивним режимом — запити пароля виникають там, де їх не чекаєш, а кешування в агенті робить помилки невідтворюваними між запусками. age не має демона, читає файл ключа напряму й падає з помилкою, яку можна прочитати. Для скрипта, що працює всередині інших скриптів, нудне краще за потужне.
Ваш SSH-ключ, а не новий. age приймає SSH-ключі як отримувачів, тож немає ні кроку генерації, ні нового секрету для бекапу, ні проблеми «а де я подів ключ від envs» на третій машині. Налаштування — одна команда, бо налаштовувати справді нічого.
Не macOS Keychain. На Mac це очевидне місце для такого, але програло воно на практичності, а не на можливості: елементи живуть за security-API, а не як файли, синхронізація прив’язана саме до iCloud Keychain, і нічого з цього немає на Linux. Синхронізована тека з .age-блобами — звичайна файлова система: її можна подивитися через ls, спрямувати на будь-який сервіс синхронізації, і поводиться вона однаково на обох системах.
Шифрування навіть попри те, що сервіс синхронізації шифрує. iCloud з розширеним захистом даних уже наскрізно зашифрований, тож на папері цей шар зайвий. Мені він потрібен усе одно: у постачальника синхронізації ніколи немає секретів у читабельному вигляді, а будь-хто, хто дістанеться до цієї теки — сесія підтримки, спільний Mac, я сам через рік з неуважно виставленими правами — побачить непрозорі .age-блоби. Це купує конфіденційність, а пошкоджений блоб просто не розшифрується замість розшифруватися неправильно. Але автора age не автентифікує: той, хто контролює теку, все ще може видалити файл чи підмінити його іншим коректно зашифрованим, тож за те, що в теці лежать саме потрібні файли, далі відповідає сервіс синхронізації. Ешелонований захист дешевий, коли коштує один виклик age.
Статус
Живий і робить свою справу. Це свідомо завершене програмне забезпечення: один bash-скрипт, переносний між macOS і Linux, ідемпотентний, без дорожньої карти, за яку я б тримався. Найімовірніша майбутня зміна — більше бекендів сховища, і лише якщо поточний почне дратувати.