Шифрование .env файлов
• #sops #age #env #infosec #иб #безопасность #macos
Схема шифрования локальных .env-файлов с помощью sops и age для безопасного хранения чувствительных данных
На днях озадачился вещью прозаической - как хранить старые и не слишком часто используемые проекты на рабочей машине? А у меня их накопилось изрядно. Проектов много, живут они долго, поднимаются нерегулярно, а .env так и лежит себе в открытом виде
Идея: храним секреты в зашифрованном .env.enc, а обычный .env держим только тогда, когда он действительно нужен. Для шифрования используем sops и age, а для удобства добавляем в оболочку команды ce для шифрования и de для дешифровки
Установка sops и age
Ставим сами инструменты, которые будут делать магию
brew install sops age
Генерация ключа age
Далее создаем age ключ. Приватный ключ появится в файле ~/.config/sops/age/keys.txt
mkdir -p ~/.config/sops/age
age-keygen -o ~/.config/sops/age/keys.txt
Это, собственно, приватный ключ который нельзя светить
Одной командой создаем .sops.yaml и правим .gitignore
Чтобы не заниматься однообразными действиями вручную в каждом проекте, удобно добавить в оболочку функцию, которая сразу создает .sops.yaml и аккуратно дописывает нужные правила в .gitignore
init_sops_env() {
emulate -L zsh
setopt pipefail localoptions
local sops_file=".sops.yaml"
local gitignore_file=".gitignore"
local age_key_file="${SOPS_AGE_KEY_FILE:-$HOME/.config/sops/age/keys.txt}"
local recipient="${SOPS_AGE_RECIPIENTS:-}"
if [[ -z "$recipient" ]]; then
if [[ ! -f "$age_key_file" ]]; then
echo "Ошибка: не найден age private key: $age_key_file"
echo "Создать ключ: age-keygen -o ~/.config/sops/age/keys.txt"
echo "Экспорт публичного ключа: export SOPS_AGE_RECIPIENTS='age1...'"
return 1
fi
recipient="$(grep -E '^# public key: age1' "$age_key_file" | head -n1 | sed 's/^# public key: //')"
fi
if [[ -z "$recipient" ]]; then
echo "Ошибка: не удалось определить public age key"
return 1
fi
if [[ -f "$sops_file" ]]; then
echo "Файл $sops_file уже существует"
else
cat > "$sops_file" <<EOF
creation_rules:
- path_regex: \\.env(\\..*)?$
age: $recipient
EOF
echo "Создан $sops_file"
fi
touch "$gitignore_file"
if [[ -s "$gitignore_file" ]] && [[ "$(tail -c 1 "$gitignore_file" 2>/dev/null)" != $'\n' ]]; then
printf '\n' >> "$gitignore_file"
fi
local line
for line in \
".env" \
".env.*" \
"!.env.example"
do
if ! grep -Fxq "$line" "$gitignore_file"; then
printf '%s\n' "$line" >> "$gitignore_file"
echo "Добавлено в $gitignore_file: $line"
fi
done
echo "Готово"
}
После этого достаточно перечитать конфиг оболочки командой source ~/.zshrc, а затем в корне проекта выполнить init_sops_env. Команда создаст .sops.yaml и добавит в .gitignore такие строки:
.env
.env.*
!.env.example
Если же вы хотите хранить .env.enc в репозитории, в .gitignore нужно добавить исключения
!*.env.enc
!*.env.*.enc
Что-то, разумеется, можно подправить под свои привычки и нейминги, тут уж каждый сам себе хозяин
Добавляем команды ce и de
Для опять же удоства добавляем в оболочку команды:
ceшифрует .env в .env.encce -rпосле шифрования спрашивает, удалить ли открытый файлce -rfудаляет его без лишних церемонийdeвыполняет обратную операцию: дешифрует.env.encобратно в.env
ce() {
emulate -L zsh
setopt pipefail localoptions
local remove_mode=""
local src_file=".env"
local enc_file=""
local arg=""
while [[ $# -gt 0 ]]; do
arg="$1"
case "$arg" in
-r)
remove_mode="confirm"
;;
-rf)
remove_mode="force"
;;
-*)
echo "Ошибка: неизвестный флаг '$arg'"
echo "Использование: ce [-r|-rf] [файл]"
return 1
;;
*)
src_file="$arg"
;;
esac
shift
done
if ! command -v sops >/dev/null 2>&1; then
echo "Ошибка: sops не найден, установите brew install sops age"
return 1
fi
if [[ ! -f "$src_file" ]]; then
echo "Ошибка: файл '$src_file' не найден"
return 1
fi
if [[ -z "${SOPS_AGE_KEY_FILE:-}" ]]; then
export SOPS_AGE_KEY_FILE="$HOME/.config/sops/age/keys.txt"
fi
if [[ ! -f "$SOPS_AGE_KEY_FILE" ]]; then
echo "Ошибка: age-ключ не найден: $SOPS_AGE_KEY_FILE"
return 1
fi
enc_file="${src_file}.enc"
sops --encrypt \
--input-type dotenv \
--output-type dotenv \
"$src_file" > "$enc_file" || {
echo "Ошибка: не удалось зашифровать '$src_file'"
rm -f "$enc_file"
return 1
}
chmod 600 "$enc_file" 2>/dev/null
echo "Зашифровано: $src_file -> $enc_file"
case "$remove_mode" in
confirm)
local reply
read -q "reply?Удалить открытый файл '$src_file'? [y/N] "
echo
if [[ "$reply" =~ ^[Yy]$ ]]; then
rm -f "$src_file"
echo "Удален: $src_file"
else
echo "Открытый файл не удален"
fi
;;
force)
rm -f "$src_file"
echo "Удален: $src_file"
;;
esac
}
de() {
emulate -L zsh
setopt pipefail localoptions
local enc_file=".env.enc"
local out_file=""
local arg=""
while [[ $# -gt 0 ]]; do
arg="$1"
case "$arg" in
-*)
echo "Ошибка: de не принимает флаги"
echo "Использование: de [файл.enc]"
return 1
;;
*)
enc_file="$arg"
;;
esac
shift
done
if ! command -v sops >/dev/null 2>&1; then
echo "Ошибка: sops не найден: brew install sops age"
return 1
fi
if [[ ! -f "$enc_file" ]]; then
echo "Ошибка: файл '$enc_file' не найден"
return 1
fi
if [[ -z "${SOPS_AGE_KEY_FILE:-}" ]]; then
export SOPS_AGE_KEY_FILE="$HOME/.config/sops/age/keys.txt"
fi
if [[ ! -f "$SOPS_AGE_KEY_FILE" ]]; then
echo "Ошибка: age-ключ не найден: $SOPS_AGE_KEY_FILE"
return 1
fi
if [[ "$enc_file" == *.enc ]]; then
out_file="${enc_file%.enc}"
else
out_file="${enc_file}.dec"
fi
sops --decrypt \
--input-type dotenv \
--output-type dotenv \
"$enc_file" > "$out_file" || {
echo "Ошибка: не удалось расшифровать '$enc_file'"
rm -f "$out_file"
return 1
}
chmod 600 "$out_file" 2>/dev/null
echo "Расшифровано: $enc_file -> $out_file"
}
После этого снова перечитайте ~/.zshrc командой source ~/.zshrc
Как с этим жить дальше
Схема работы такая: в проекте у вас живет зашифрованный .env.enc. Перед началом работы выполняете de. Появляется обычный .env. Поработали, закончили, и жмете ce -rf. Открытый .env шифруется обратно и удаляется. Ничего особенного, просто не держим голые секреты на диске дольше, чем в них есть нужда
Если вы используете не только .env, но и, скажем, .env.local / .env.production, команды работают и с ними:
ce .env.local
ce -rf .env.local
de .env.local.enc
Что коммитить, а что нет
Коммитить обычно можно .sops.yaml, .env.example (пример конфига без открытых кредсов) и, при желании, сам .env.enc, если вы решили хранить зашифрованный env в репозитории
Естественно нельзя тащить в репу открытый .env, приватный ключ ~/.config/sops/age/keys.txt, любые иные приватные ключи и вообще все, что содержит секреты в открытом виде
Нужно ли коммитить .sops.yaml? Да, обычно нужно. Это не секрет, а конфиг. Публичный ключ там хранить можно, а приватный - нельзя. Все как обычно
Нужно ли удалять .env.enc? Не обязательно, ибо таким образом есть возможность хранить в репе каноничные env переменные для команды
Иными словами, рабочая модель выглядит так: в проекте лежит .env.enc, при необходимости вы делаете de, работаете, затем выполняете ce -rf. Уот и все
И наконец важная правда жизни
Шифрование .env файлов в первую очередь защищает кредсы от необязательного длительного хранения их в plaintext на диске. Это просто разумная, взрослая защита, а не панацея
Если меж тем кто-нибудь найдет вменяемый способ автоматизировать все это хозяйство и избавиться от ручного ввода ce и de без лишнего колдовства, напишите мне на почту, буду признателен. Я удобного для себя варианта пока не сыскал. Все либо чересчур хитро, либо откровенная чертовщина