git-SSH via rootless image + built-in SSH server (ADR 0031, #91) — rehearsed + prod cutover done #118
Inga granskare
Etiketter
Inga etiketter
area/backups
area/ci
area/control-panel
area/identity
area/infra
area/observability
area/payments
area/security
area/storage
area/web
blocked
needs-info
needs-triage
ready-for-implementation
type
bug
type
chore
type
docs
type
epic
type
feature
type
task
wontfix
Ingen milstolpe
Inget projekt
Inga tilldelade
1 deltagare
Notiser
Förfallodatum
Inget förfallodatum satt.
Beroenden
Inga beroenden satta
Referens
bitborg/bitborg-infra!118
Läser in…
Hänvisa till i nytt ärende
Ingen beskrivning angiven.
Ta bort grenen "feat/91-git-ssh-rootless"
Borttagning av en gren är permanent. Även om den borttagna grenen kan fortsätta existera en kort tid innan den faktiskt tas bort, kan det INTE ångras i de flesta fall. Vill du fortsätta?
✅ LANDED — prod cutover complete 2026-07-24. Forgejo is live on
16.0.1-rootlesswith git-SSH via socket activation. The ADR 0031 phase-2 rehearsal passed and the phase-3 prod cutover is done; the health gate is green andmainnow matches deployed prod. Safe to merge.Phase 1 of #91 / ADR 0031. Grounded in the verified rootless image config (
User=1000,GITEA_WORK_DIR=/var/lib/gitea,GITEA_CUSTOM=/var/lib/gitea/custom).In the branch (phase 1):
:16.0.1-rootless; Forgejo's built-in SSH server (START_SSH_SERVER=true,SSH_LISTEN_HOST=::/SSH_LISTEN_PORTmatched to the socket by address).forgejo-ssh.socket(user unit) binds host :22 and passes the fd into the container → real client IP in logs (the #81 caddy pattern).forgejo.containerdrops OpenSSH (DropCapability=ALL,NoNewPrivileges=true), noPublishPort,Requires=forgejo-ssh.socket.GITEA_APP_INI=/run/gitborg/app.ini) so the entrypoint'senvironment-to-inimerge never writes secrets to the host file.migrate-rootless.yml(gated on-e forgejo_migrate_rootless=true):podman unshare,gitea/*→ volume root, chown uid 1000, rootful leftovers stashed in.rootful-legacy/.local/Makefile+ local vars synced.Two cutover-blocking fixes (commit
9cd473b) — neither exercised by the local preview (it publishes-p 2222:2222and never uses socket activation):forgejo.container: removed a duplicatecustom/{templates,public}Volume=mount (rebase artifact from the #188 merge) → podman "duplicate mount destination" → container won't start.forgejo-ssh.socket: addedService=forgejo.service→ without it the socket fd targets a non-existentforgejo-ssh.service, so git-over-SSH would be dead (noPublishPortfallback).Phase-2 rehearsal tooling: so the rootful→rootless cutover could be rehearsed on a throwaway restore-drill clone before prod (ADR 0031 / 0027 / 0030).
backup-drill: a manualDRILL_HOLD=1mode that boots + data-stages a clone as the weekly drill does, but skips restore/doctor/teardown and holds the VM (with ashutdownself-destruct backstop) for a rehearsal. The weekly timer never setsDRILL_HOLD— automated drill behaviour is unchanged.ansible/rehearsal-rootless.yml: reaches the clone (FIP-direct, or ProxyJump via the services host), bootstraps the host preconditions theforgejorole needs (bitborg user, subuid/subgid, linger,:22sysctl, quadlet dirs, aardvark-dns + thegitborgnetwork, and an admin-sshd →:2222cutover so Forgejo's git-SSH gets the real:22), restores prod data in rootful layout, then runs the real, unmodifiedforgejorole withforgejo_migrate_rootless=trueand asserts every ADR 0031 §2 criterion plus a manual clone/push + real-client-IP checklist.opentofu/backup-drill.tofu: drill SG allows:2222ingress (admin SSH after the clone's cutover).docs/runbook.md: the full rehearsal procedure + mandatory teardown (the clone materialises real prod signing secrets → destroy it after) + go/no-go gate.Rehearsal (phase 2) — PASSED (2026-07-23, re-validated 2026-07-24 on a clone of live prod data): all ADR 0031 §2 asserts green (socket + service active, volume migrated rootful→rootless, caps dropped,
forgejo doctor, secret-free host app.ini); git-SSH clone/push works; protected-maincorrectly rejected; real client IP confirmed viass(thegiteaprocess holds the:22fd with the real public peer — no pasta rewrite). Note: Forgejo 16 does not console-log the SSH peer even attrace, so verification is viass/podman port, notpodman logs— runbook corrected.Cutover (phase 3) — DONE (2026-07-24; fresh backup anchor →
site.yml --tags forgejo -e forgejo_migrate_rootless=true). The migration succeeded, but Forgejo then crash-looped briefly on a storage-uid bug: the external[storage]volume (/srv/gitborg-lfs, mounted/srv/storage) was still owned by the rootful git uid (2000) while the rootless image's git user is a fixed 1000 →mkdir /srv/storage/attachments: permission denied. Fixed forward (recursivepodman unshare chown 1000+ restart), and fixed at the root in commit214b0c7: the steady-state storage-chown task now targets uid 1000, andmigrate-rootless.ymlgained a one-time recursive storage chown (the storage analogue of the data-volume chown). The rehearsal missed this because it skipped the external[storage]volume — now covered. The follow-up apply is green; the health gate (#107) confirms[storage]serves a DB-known object andgit.gitborg.seis up.Also in this branch (corrections captured during the cutover): a dpkg-lock wait in the rehearsal bootstrap (first-boot
unattended-upgradesrace), and runbook fixes — the manualDRILL_HOLDinvocation environment, the--tags backup-drillprerequisite, andss-based client-IP verification for both the rehearsal and the prod cutover.pnpm ansible:check, the rehearsal playbook--syntax-check,ansible-lint(production profile), andtofu validateall clean.Closes #207.
Refs #91.
ac6bc3d16ab85bd6560bPhase 1 complete + rebased on main (still rehearsal-gated)
The role changes are done and validated in the local podman preview, not just drafted. Branch head
7e49b6e.Added since the original WIP push:
/etc/gitborg/app.ini; an entrypoint wrapper copies it to a tmpfs andGITEA_APP_INIpoints there, so the image'senvironment-to-inimerges the env secrets in memory instead of rewriting the host file. Discovery: Forgejo does NOT readFORGEJO__*natively — that merge is the only mechanism, and on the rootful image it has been persisting vaulted secrets in plaintext into the host app.ini (the 2026-07-06 "permanent render diff"). Now fixed + gone; SECRET_KEY continuity across cutover is safe.-e forgejo_migrate_rootless=true), layout-aware backup-drill restore,local/synced, runbook end-state.Local validation (
:16-rootless,--cap-drop=ALL): boots clean, admin created,git cloneand push over the built-in SSH server OK, host app.ini secret-free.Remaining (user-gated): ADR 0031 phase 2 rehearsal on a restore-drill clone (real-client-IP proof) → phase 3 off-peak prod cutover. Do not apply until then.
7e49b6e54f7839ceeece7839ceeecece7342c120ce7342c1200abd5df64e0abd5df64e10c0555364WIP: git-SSH via rootless image + built-in server (#91) — DO NOT APPLY (rehearsal-gated)till git-SSH via rootless image + built-in server + phase-2 rehearsal tooling (#91) — DRAFT, rehearsal-gatedgit-SSH via rootless image + built-in server + phase-2 rehearsal tooling (#91) — DRAFT, rehearsal-gatedtill WIP: git-SSH via rootless image + built-in server + phase-2 rehearsal tooling (#91) — rehearsal-gated58258b6f829b1bab96039b1bab9603aaaad747e7aaaad747e702831d5e5f59262df850675b32ee3eWIP: git-SSH via rootless image + built-in server + phase-2 rehearsal tooling (#91) — rehearsal-gatedtill git-SSH via rootless image + built-in SSH server (ADR 0031, #91) — rehearsed + prod cutover done