К содержимому
gtcnsl
EN RU

Секреты

Секреты сессий, JWT и LFS у Gitea лежат в отдельном файле. gtcnsl генерирует их, проверяет, что они реальные, и ротирует по одному с бэкапом и рестартом.

Актуально для v1.3.0

Четыре управляемых секрета

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
!
--force разлогинивает всех
Перезапись 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-проверка не прошла.

i
Не передавайте секретные ключи в --var
Значения секретов подставляются за вас. Ваши --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.

!
Пустой SECRET_KEY — это публичный дефолтный ключ Gitea
Если [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.241.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 вместе с ним не нужен.

i
--reencrypt и --dry-run — только для SECRET_KEY
При ротации 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 сделает что-то неожиданное.