SSH client IPs are rewritten by pasta (forgejo port 22 published) #91
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#91
Läser in…
Hänvisa till i nytt ärende
Ingen beskrivning angiven.
Ta bort grenen "%!s()"
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?
Sibling of #81 (fixed for Caddy/HTTP via systemd socket activation): Forgejo's built-in SSH server has its port published through rootless Podman (pasta/rootlessport), so SSH connections reach Forgejo with a rewritten source address — SSH client IPs in Forgejo logs/audit trails are not real.
Impact: SSH-based abuse can't be attributed or rate-limited per client; audit logs for git-over-SSH show the container network address.
Fix candidates, in preference order:
LISTEN_FDS-style activation for its web server via graceful restarts, but the built-in SSH server's fd-inheritance support needs verification first.SSH_SERVER_PROXY_PROTOCOL(verify exact key/semantics in v16).Note the #81 verification steps in the runbook explicitly call this out as unresolved.
Analysis — the suggested fixes don't apply; the clean fix is ADR-level
Architecture: git-over-SSH is served by the forgejo image's bundled OpenSSH (
app.iniSTART_SSH_SERVER=false— Forgejo's built-in Go SSH server is OFF; ADR 0006). It's publishedhost:22 → container:22viaPublishPort(pasta), which SNATs the source to the container network — hence the lost client IP.LISTEN_FDSsupport (only per-connectionsshd -iinetd mode). Doesn't drop in.SSH_SERVER_PROXY_PROTOCOL) is a built-in-server option (disabled here); OpenSSH doesn't parse PROXY protocol. N/A.The one clean fix: switch git-SSH to Forgejo's built-in server + socket-activate :22. The Go server can inherit an fd, and a systemd-bound host fd removes ADR 0006's "git user can't bind :22" objection and preserves the source IP. But this is an ADR-0006 revisit with real risk:
REMOTE HOST IDENTIFICATION HAS CHANGED. Must reuse the current keys.Severity check: medium, attribution-only. fail2ban guards the admin sshd (2222), not git-SSH (:22), so no active per-IP control depends on the git-SSH source IP today. Git-over-HTTPS already logs real client IPs (#81) for anything needing attribution/rate-limiting. So the gap is forensic (can't tie git-SSH ops to real IPs in audit logs), not an enforcement hole.
Recommendation
Accept + document as a known limitation now, and schedule the built-in-server + socket-activation fix (with host-key migration) as a proper ADR + rehearsed change later. The fix's blast radius (all git-SSH + host-key continuity) isn't justified right now by an attribution-only gap that HTTPS already covers. If SSH-based abuse ever needs per-client action, revisit sooner.
Approach decided: ADR 0031 (PR bitborg-docs#40) — option A2. Move git-SSH to the rootless Forgejo image's built-in server + systemd socket activation for :22 (the #81 pattern; Forgejo matches activated fds by address — verified). Feasibility confirmed; A1 rejected (rootful image starts openssh via s6 regardless).
Phased execution (rehearse-first, ADR 0030):
forgejorole + dependent roles (backup, backup-drill, reconciler/web exec-uid,GITEA_*/paths); validate + exercise in the local podman preview.site.yml, prove data intact + git clone/push over SSH + real client IP in Forgejo logs; nail the exact uid/path/userns volume-migration steps there.Now is the window (no external users → the uid/path/host-key migration is cheapest).
Phase 1 (role changes) done — WIP draft PR #118 (
feat/91-git-ssh-rootless), not applied (rehearsal-gated per ADR 0031).Committed & syntax/lint-clean:
:16.0.0-rootlessimage; built-in SSH server (START_SSH_SERVER,SSH_LISTEN_HOST=::) claiming a passed socket fd.forgejo-ssh.socket(host :22,BindIPv6Only=both) → real client IP (#81-style).forgejo.container:DropCapability=ALL, noPublishPort,Requires=forgejo-ssh.socket, rootless volume layout (/var/lib/gitea).WORK_PATH=/var/lib/gitea; tasks socket-install + stop-before-rebind choreography.Blocking remaining work (must run on the ADR 0031 rehearsal clone, not overnight/unattended):
/data/gitea(uid 2000) → rootless/var/lib/gitea(uid 1000 subuid) — the copy +chown.backup-drillrestore path for the new layout.git clone+push over SSH, real client IP in logs, socket fd↔SSH_LISTEN_*address match, caps sufficient.local/+Makefilerootless assumptions.Investigation — the two fix candidates aren't feasible in the current architecture
Option 1 (socket activation, like #81): not applicable. #81 works because Caddy natively consumes a systemd-passed fd (
bind fd/3in the Caddyfile). git-over-SSH here is served by the image's bundled OpenSSH (START_SSH_SERVER=false; the rootful image runs its ownsshd,PublishPort={{ git_ssh_port }}:22). OpenSSHsshdhas no way to consume a systemd-passed socket fd for its main listener — so the fd-passing trick that de-pasta'd Caddy can't de-pasta sshd.Option 2 (PROXY protocol): not applicable.
SSH_SERVER_PROXY_PROTOCOLgoverns Forgejo's built-in SSH server, which is disabled. OpenSSHsshddoesn't speak PROXY protocol.Resolution (option 3 for now) + recommendation
Documented as a known limitation in the runbook (this PR). A real fix requires a re-architecture of git-over-SSH, e.g.:
sshdwithAuthorizedKeysCommandproxying to Forgejo.Both are sizeable changes to a core function (git push/pull) and warrant their own ADR/epic + a maintenance-window rollout — not an incidental fix. Impact is bounded: SSH access is key-authenticated and unaffected; only IP attribution in logs/audit is degraded (abuse-tracing / rate-limiting gap, not an access-control hole).
Keeping #91 open as the tracker for that re-architecture. Runbook now documents the limitation so it isn't rediscovered.
Phase 1 complete — all remaining role work is done and validated in the local preview (commit
7e49b6eonfeat/91-git-ssh-rootless; push + PR #118 refresh pending — SSH agent locked overnight).Load-bearing discovery during validation: Forgejo does not read
FORGEJO__*env natively (verified empirically on 16.0.0 —INSTALL_LOCKvia env was ignored). Env config only works throughenvironment-to-ini, which both images' entrypoints run unconditionally, rewriting$GITEA_APP_INI. Two consequences:forgejo-config/app.ini(mode 0644) has been getting the vaulted secrets persisted into it in plaintext on every container start — the known "permanent render diff" (2026-07-06 note in app.ini.j2) was this. On the rootless image (uid 1000, read-only mount) the same merge is simply fatal — caught in the local preview./etc/gitborg/app.ini, an entrypoint wrapper copies it to a tmpfs (/run/bitborg), andGITEA_APP_INIpoints there — env secrets merge in memory only. Host file pristine (verified), permanent render diff gone, and SECRET_KEY continuity across the cutover is guaranteed (the vaulted env values were and remain authoritative).Local validation (podman,
:16-rootless,--cap-drop=ALL): boots clean; admin created;git cloneand push over the built-in SSH server OK; host app.ini contains no secrets after runs; branding mounts undercustom/OK.Also in the commit: flag-gated volume migration (
-e forgejo_migrate_rootless=true,podman unshare,gitea/*→root + chown 1000, leftovers stashed in.rootful-legacy/), layout-aware backup-drill restore (old + new archives),local/synced (one-timemake cleannote in README), runbook end-state bullet.Remaining (unchanged, user-gated): ADR 0031 phase 2 rehearsal on a restore-drill clone (data intact, SSH clone/push, real client IP, socket fd↔address match) → phase 3 off-peak prod cutover with backup anchor + #107 health gate.
Resolved by the ADR 0031 rootless migration (PR #118), cut over to prod 2026-07-24. git-SSH is now served by Forgejo's built-in SSH server on the :16.0.1-rootless image via forgejo-ssh.socket (systemd socket activation), so pasta no longer rewrites the source and real client IPs reach Forgejo. Verified in prod: SSH clone/push works and 'ss -tnp' shows the gitea process holding the :22 fd with the real public peer (no container-net address). A storage-uid regression during cutover (external [storage] still owned by the rootful uid 2000 vs the rootless image's fixed uid 1000) was fixed forward and at the root in
214b0c7. Note: Forgejo 16 does not log the SSH peer even at trace, so verify via ss / podman port, not podman logs (runbook updated).