🔒 fix(identity): ключ шифрования секретов в БД перестал следовать за SECRET_KEY

Ключ выводился из SECRET_KEY, а ошибка расшифровки глоталась молча. Значит
смена ключа подписи — обычное действие после утечки — делала нечитаемыми
секреты TOTP сотрудников, токены интеграций, пароль SMTP и ключи S3, и в логе
об этом не было ни строки: секреты просто становились пустыми.

Теперь ключ живёт своим файлом в томе секретов: его кладёт туда первый старт
стека, выводя из secret_key ровно тем же способом, каким это делал сам продукт.
Значение от этого не меняется, поэтому работающая установка ничего не теряет —
но ключ больше не привязан к SECRET_KEY, и подпись можно ротировать.

Человек ключ по-прежнему не вводит: файла с переменными у продукта нет.

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

Проверено на живом томе: shell-вывод ключа совпадает с питоновским байт в байт
(сверено в образе pgvector/pgvector:pg16); после запуска скрипта сгенерированный
ключ равен действующему выводимому; приложение читает его из файла и
расшифровывает все пять сохранённых секретов интеграций. Четыре теста.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
AndreyandClaude Opus 5 committed 2026-09-08 17:22:43 +03:00
1 parent a2e6672424
commit 486492c1d3
4 files changed
+116 -7

No files matched your search

@@ -49,6 +49,36 @@ ensure_secret postgres_migration_password
# иначе пришлось бы вписывать одно и то же значение в двух местах.
ensure_secret turn_secret
# Ключ шифрования секретов в БД (Fernet): им зашифрованы секреты TOTP
# сотрудников, токены интеграций, пароль SMTP и ключи S3.
#
# Значение выводится из ключа подписи ровно тем же способом, каким его выводил
# сам продукт, пока отдельного файла не было. Поэтому работающая установка
# ничего не теряет: ключ тот же, просто теперь он живёт своим файлом и больше
# не следует за secret_key. До этого смена ключа подписи молча делала всё
# зашифрованное нечитаемым — Fernet без ключа не расшифровать.
ensure_field_encryption_key() {
file="$DIR/field_encryption_key"
if [ -s "$file" ]; then
return 0
fi
if ! command -v openssl >/dev/null 2>&1; then
# Вывести ключ нечем. Файла не будет, продукт выведет его сам — значение то
# же самое, просто связка с secret_key сохранится до появления openssl.
echo "openssl not found — field_encryption_key left derived from secret_key"
return 0
fi
# base64url(sha256(secret_key)) — совпадает с chatballs.identity.crypto.
printf %s "$(cat "$DIR/secret_key")" \
| openssl dgst -sha256 -binary \
| openssl base64 -A \
| tr '+/' '-_' > "$file"
chmod 444 "$file"
echo "generated field_encryption_key"
}
ensure_field_encryption_key
# Coturn читает секрет не из аргумента, а из конфига: значение не светится
# в списке процессов и не дублируется в compose.
if [ ! -s "$DIR/turnserver-secret.conf" ]; then