Секреты
Секреты сессий, JWT и LFS у Gitea лежат в отдельном файле. gtcnsl генерирует их, проверяет, что они реальные, и ротирует по одному с бэкапом и рестартом.
Четыре управляемых секрета
gtcnsl держит их в /etc/gitea/secrets.ini (режим 0640, владелец root:git), отдельно от шаблона конфигурации — чтобы config apply их не трогал:
| Секрет | Роль |
|---|---|
SECRET_KEY |
глобальный секрет сессий / OAuth / 2FA |
INTERNAL_TOKEN |
JWT для внутренних API Gitea |
JWT_SECRET |
секрет OAuth2 JWT |
LFS_JWT_SECRET |
секрет JWT протокола Git LFS |
Сгенерировать
gtcnsl secrets generate --yes
Генерирует свежие значения через gitea generate secret <TYPE> и пишет файл. Если файл уже есть, нужно явное согласие:
gtcnsl secrets generate --force --yes
SECRET_KEY инвалидирует все активные сессии, привязки 2FA и OAuth-гранты. Используйте --force только на свежей машине или при намеренном сбросе.Проверить
gtcnsl secrets check
Только чтение. Возвращает 0, когда все четыре содержат реальные значения, и 1, если хоть один отсутствует или остался заглушкой (changeme, REPLACE_ME, …). Падает явно, если secrets.ini нет — удобно как гейт в CI или doctor.
Ротировать один секрет
Ротация перегенерирует один секрет и заново применяет ваш шаблон конфигурации с подставленными секретами:
gtcnsl secrets rotate JWT_SECRET \
--template app.ini.tmpl \
--var DOMAIN=git.example.com \
--yes
<TYPE> чувствителен к регистру и один из SECRET_KEY, INTERNAL_TOKEN, JWT_SECRET, LFS_JWT_SECRET. Процесс бэкапит secrets.ini.bak + app.ini.bak, пишет оба, рестартит и откатывает оба вместе, если health-проверка не прошла.
--var несут только операторские переменные (DOMAIN, ROOT_URL, …); секретный ключ в --var будет отклонён.SECRET_KEY — особый секрет
Gitea шифрует под [security] SECRET_KEY целый класс строк в БД: секреты Actions, TOTP-сиды 2FA, заголовки Authorization вебхуков, LDAP bind-пароли и креды задач импорта. У Gitea нет штатной ротации ключа (go-gitea/gitea#16832, открыт с 2021 года) — наивная смена ключа молча осиротит все эти данные. Это не гипотетика: именно так и произошло на проде самого gtcnsl — после обычной ротации CI перестал получать секреты Actions.
[security] SECRET_KEY пуст, Gitea использует значение, зашитое прямо в исходники — его знает любой, кто читал код. Данные, «зашифрованные» под ним, фактически не защищены. gtcnsl doctor предупреждает, если видит такое.Guard (защита)
gtcnsl secrets rotate SECRET_KEY --template app.ini.tmpl --var DOMAIN=git.example.com --yes
жёстко останавливается до того, как тронуть хоть один файл, если находит данные, зашифрованные под текущим ключом, и печатает по категориям, сколько строк осиротеет (секреты Actions, 2FA-сиды, заголовки авторизации вебхуков, LDAP bind-пароли, креды задач импорта). Ротация INTERNAL_TOKEN, JWT_SECRET или LFS_JWT_SECRET не затронута — guard срабатывает только для SECRET_KEY.
Безопасный путь: --reencrypt
gtcnsl secrets rotate SECRET_KEY --reencrypt \
--template app.ini.tmpl \
--var DOMAIN=git.example.com \
--yes
Порядок действий: остановить Gitea → сделать бэкап БД (sqlite: копия файла рядом с оригиналом, суффикс .reencrypt-bak; mysql/postgres: файлового бэкапа нет — страховкой служит сама транзакция перешифровки, но собственный бэкап всё равно стоит сделать) → записать новые secrets.ini/app.ini → перешифровать каждую затронутую строку, проверяя decrypt(new) == plaintext до записи → запустить Gitea → health-check. Сбой на любой стадии после бэкапа БД запускает полный автоматический откат — БД, secrets.ini, app.ini, рестарт, — так что вручную разгребать наполовину мигрированный инстанс не придётся.
Повторный запуск --reencrypt всегда безопасен: каждый запуск генерирует свежий ключ и мигрирует всё, что расшифровывается под текущим, проверяя каждую строку до записи; строки, которые прочитать нельзя, никогда не трогаются. Перезапуск после отката просто мигрирует те же строки ещё раз, под следующий ключ.
Конверт сверенных версий
Криптография перешифровки — не публичный контракт Gitea: апстрим может изменить её в любом миноре, не упомянув в changelog, — поэтому --reencrypt запускается только на том миноре Gitea, для которого gtcnsl реально сверил эту криптографию (сейчас 1.24–1.27). Вне конверта команда отказывает до любых изменений, называя установленную версию и сверенный диапазон. Явный, отдельный обход:
gtcnsl secrets rotate SECRET_KEY --reencrypt \
--reencrypt-unverified-version-i-understand \
--template app.ini.tmpl --var DOMAIN=git.example.com --yes
Он не заменяет --orphan-encrypted-data-i-understand — это защита от разных рисков (несверенная криптосхема vs. осознанное принятие потери данных), и оба флага применимы независимо друг от друга. Read-only классификация guard'а (--dry-run) по-прежнему работает на любой версии; вне конверта её счётчики по местам помечаются как потенциально неполные.
--dry-run
gtcnsl secrets rotate SECRET_KEY --reencrypt --dry-run \
--template app.ini.tmpl --var DOMAIN=git.example.com
Классифицирует данные и показывает, что затронул бы настоящий --reencrypt (счётчики по каждому месту, затронутые таблица/строка/поле, но никогда — значение) — без остановки, записи или рестарта. --dry-run не требует --yes.
--orphan-encrypted-data-i-understand
gtcnsl secrets rotate SECRET_KEY --orphan-encrypted-data-i-understand \
--template app.ini.tmpl --var DOMAIN=git.example.com --yes
Пропускает миграцию и осознанно принимает потерю данных — для случая, когда вы и так собирались перевыпустить секреты Actions и попросить пользователей перезавести 2FA вручную. Этого флага достаточно, чтобы пройти guard; --reencrypt вместе с ним не нужен.
INTERNAL_TOKEN, JWT_SECRET или LFS_JWT_SECRET любой из этих флагов будет отклонён — они имеют смысл только для SECRET_KEY.Инстансы с SECRET_KEY_URI
С версии 1.19 Gitea умеет брать [security] SECRET_KEY из файла через SECRET_KEY_URI (URI вида file:) вместо буквального значения. gtcnsl резолвит его точно так же, как это делает сама Gitea при старте, а secrets rotate SECRET_KEY держит этот файл синхронизированным: новый ключ записывается и в него тоже — со своим бэкапом с меткой времени и откатом при любом сбое, — а не только фиксируется в secrets.ini. Одновременная установка и SECRET_KEY, и SECRET_KEY_URI отклоняется — как и у самой Gitea при старте. На инстанс с буквальным SECRET_KEY всё это никак не влияет.
Нюансы
- Строки, которые не расшифровываются ни старым, ни новым ключом, никогда не трогаются — они только перечисляются (таблица и ID строки, без значений) для отдельного ручного разбора.
- 2FA переживает перешифровку: существующие TOTP-сиды продолжают работать, перезаводить их не нужно. Если пользователь уже перезавёл 2FA под новым ключом до запуска
--reencrypt, эта строка уже считается смигрированной и пропускается — тоже нормально. mssqlпока не поддержан: gtcnsl вообще не умеет заглядывать в mssql-базу, поэтому guard не может посчитать, что осиротеет, — он сразу отказывает в ротации SECRET_KEY (и--reencryptтоже откажет), если не задан--orphan-encrypted-data-i-understand. Поддержанные движки:sqlite3,mysql,postgres.
Дальше
Встройте gtcnsl secrets check в свой прогон gtcnsl doctor — тогда отсутствующий или заглушечный секрет всплывёт раньше, чем Gitea сделает что-то неожиданное.