v1.2.0
2026-07-12
● stable
What changed
Added
- **`secrets rotate SECRET_KEY --reencrypt` — the safe rotation.** Stops Gitea, backs up the database (sqlite: a file copy next to the original, `.reencrypt-bak`; mysql/postgres: the single re-encryption transaction is the safety net), writes the new `secrets.ini`/`app.ini`, re-encrypts every affected row — verifying `decrypt(new) == plaintext` for each one *before* writing it — then starts Gitea and health-checks. Any failure rolls everything back automatically: database, `secrets.ini`, `app.ini`, restart. Gitea never observes a new-key/old-ciphertext mismatch. Both Gitea encryption schemes are implemented byte-compatibly (verified against the pinned v1.26.4 source), covering all five encrypted sites. Supported engines: `sqlite3`, `mysql`, `postgres` (`mssql` is refused with a clear error). Re-running is always safe: each run migrates whatever decrypts under the current key and never touches rows it cannot read.
- **`secrets rotate SECRET_KEY --dry-run`** — classifies every encrypted row (would-be-re-encrypted / already-migrated / unreadable) and reports per-site counts and row IDs without changing anything. Doesn't require `--yes`.
- **`--orphan-encrypted-data-i-understand`** — the explicit, informed opt-out for operators who intend to re-issue secrets and re-enroll 2FA by hand.
- **`doctor` warns when `SECRET_KEY` is empty or Gitea's public default.** An empty `[security] SECRET_KEY` means Gitea encrypts with a value baked into its own source code — the data is effectively unprotected. The probe names the safe fix (`secrets rotate SECRET_KEY --reencrypt`) and never prints the operator's actual key.
Changed
- **Behaviour change (safety): `secrets rotate SECRET_KEY` refuses to orphan data.** Previously the rotation applied cleanly and reported success while silently orphaning every encrypted row — the production incident this release grew from. It now hard-stops *before touching any file* when data encrypted under the current key exists, printing per-category counts of what would be lost, and requires either `--reencrypt` (migrate it) or `--orphan-encrypted-data-i-understand` (accept the loss). Rotating `INTERNAL_TOKEN` / `JWT_SECRET` / `LFS_JWT_SECRET` is unchanged. Strictly a changed outcome for an existing invocation — shipped in a minor release deliberately, because the old outcome was data loss.
- **Binary size grew by ~7 MB** (≈19.6 → ≈27 MB): gtcnsl now bundles the same CGO-free database drivers Gitea itself uses (`modernc.org/sqlite`, `go-sql-driver/mysql`, `lib/pq`) to read and re-encrypt the database in-process — plaintext never leaves the gtcnsl process, no client tools needed on the host.
Fixed
- The `secrets` command summary no longer says rotation is "(in v0.4)" — rotation has been shipping since v0.4 actually landed.
Download
Pick your architecture. Direct links point at the release assets.
$ curl -fsSL https://dl.gtcnsl.com/v1.2.0/gtcnsl-v1.2.0-linux-amd64 -o /usr/local/bin/gtcnsl && chmod +x /usr/local/bin/gtcnsl
i
Verify
Compare against the signed checksum manifest before you run anything.
checksums.txt