Languages: Français · English
Architecture
How gitfed's code is organized, and why. For the identity/federation model
itself, see HOW_IT_WORKS.en.md; for deployment
steps, see deploy/k8s/README.md.
1. Overview: four binaries, one shared core
cmd/
gitfed-server/ daemon: SSH server + /.well-known endpoint
gitfed-web/ web UI (public browsing + self-service + admin)
gitfed-tui/ terminal admin tool
gitfed-renew-cert/ client-side certificate renewal tool
gitfed-server is the only process that touches the database and the git
repos directly. gitfed-web and gitfed-tui don't reimplement anything:
they drive the same business core through the admin.Ops interface
(§3) — either by talking to an already-running gitfed-server over a
socket, or, if it isn't running, by opening the database directly.
gitfed-renew-cert is entirely separate: a small standalone client, meant
to run on a user's own machine (cron/systemd timer), which only ever
speaks SSH to a remote instance.
2. The internal/ packages
| Package | Role |
|---|---|
ca |
Generates the instance's certificate authority key and signs user certificates. |
ssh |
The SSH server itself: authentication (bare key or certificate), revocation checking, running git-upload-pack/git-receive-pack. |
federation |
.well-known discovery, anti-SSRF guards (wellknown.go), the trust store and its policy (resolver.go). |
acl |
Per-repo authorization rules (public/private, read/write/admin roles). |
gitexec |
Everything that invokes git as a subprocess: init, reading a tree/file at HEAD, default-branch resolution, commit history/diffs, and the branch/merge plumbing behind merge requests (§8). |
store |
bbolt persistence: users, repos, ACLs, sessions, trust store, audit log, revocations, pinned repos, federated notifications, merge requests and their comments. |
admin |
The Ops interface: every admin or self-service operation, in one place, implemented once. |
adminrpc |
The JSON-over-Unix-socket protocol that exposes admin.Ops to a remote client (see §3). |
opsconnect |
Automatically picks live-socket vs. direct-store mode depending on whether a gitfed-server is already running. |
i18n |
Translation dictionaries (French/English) for the web UI and the language-resolution helper. |
config |
Loads the instance's configuration file (gitfed.json). |
version |
Version number, injected at build time. |
3. Why there's an admin RPC socket
bbolt (the embedded database store uses) enforces an exclusive,
single-writer lock on its file: only one process can have it open for
writing at a time. If gitfed-web opened the database directly while
gitfed-server was already running, one of the two would fail to start —
or worse, they'd fight over the lock.
The fix: gitfed-server exposes its admin.Ops over a local Unix socket
(data/admin.sock), through a small homegrown JSON protocol
(internal/adminrpc). gitfed-web and gitfed-tui connect to it as
clients instead of reopening the database — internal/opsconnect decides,
at startup, whether to go through this socket ("live" mode) or open the
database directly ("offline" mode, used by gitfed-tui when no server is
running, e.g. to create the very first account).
One gotcha fixed along the way: errors crossing this socket lose their
identity if you're not careful — errors.New("not found") on the server
side comes back as a plain string on the client side, which can no longer
be compared with == store.ErrNotFound. adminrpc.Client therefore
explicitly reconstructs known sentinel errors by matching the message,
rather than letting them pass through as-is.
4. The web UI (cmd/gitfed-web)
Every page is an HTTP handler that: checks access rights via admin.Ops,
runs a small Go template to produce the page body, then calls
server.render(), which wraps that body in the common "shell" (nav bar,
footer, stylesheet, SVG icon sprite) defined in render.go.
Notable points:
- No JS framework. All rendering is done server-side with
html/template; the little JavaScript there is (profile menu, tab switching, clipboard copy) is a single inline block, whose SHA-256 hash is pinned in the CSP policy (see §6). - i18n via a template function. Every page passes its current language
(resolved from a cookie, then
Accept-Language, seelang.go) into the template data; strings go through{{t .Lang "key"}}, which looks up the translation ininternal/i18n. - Web authentication is independent of git. The web password (bcrypt
hashed, cost 12) only opens an opaque web-side session — it's never
involved in SSH/git authentication, which relies solely on keys and
certificates (see
HOW_IT_WORKS.en.md).
5. Deployment topology
┌─────────────────────────── Pod (1 replica) ────────────────────────────┐
│ │
│ "server" container "web" container │
│ gitfed-server gitfed-web │
│ ├─ SSH :2222 (hostPort) ├─ HTTP :8088 │
│ ├─ well-known :8443 └─ socket data/admin.sock (client) │
│ └─ socket data/admin.sock (server) │
│ │
│ "data" volume (PVC) mounted in both containers: │
│ bbolt database, bare repos, CA key, SSH host key, admin socket │
└──────────────────────────────────────────────────────────────────────┘
- Always a single replica. bbolt (single-writer lock), the admin
socket, and the SSH port exposed via
hostPortare all, by nature, single-instance resources —deployment.yamlsetsreplicas: 1andstrategy: Recreateinstead of a rolling update. - Two containers, one pod. They share the same data volume; the
webcontainer waits, via a shell loop, fordata/admin.sockto exist before starting. - SSH on
hostPort: 2222, not 22 — the node's port 22 is already taken by the VPS's own sshd. - Only
gitfed-webis exposed via an Ingress (Traefik + cert-manager, already in place on the cluster for another project); it's safe to expose it publicly because web authentication is real (password + session), not an unprotected admin-only access point.
The step-by-step detail (DNS, first deployment, bootstrapping the admin
account) is in deploy/k8s/README.md.
6. What protects the instance in production
See docs/security/AUDIT.md (French) for the full
detail, but in short, what's structurally in place:
- Writes only ever happen over SSH —
git-receive-packis never exposed any other way, authenticated by key or federated certificate only (see §7 for the one HTTP exception, strictly read-only). - Anti-SSRF at connection time, not just at domain-name validation
(
internal/federation/wellknown.go) — also protects against DNS-rebinding. - Strict CSP with the inline script pinned by hash, standard hardening
headers, origin checking on mutating requests (CSRF defense-in-depth, on
top of
SameSite=Lax). - Immediate certificate revocation, checked on every SSH authentication
(
internal/ssh/server.go). - Rate limiting on the web login, per account and per IP.
7. Anonymous HTTPS clone for public repos
As of 0.8.0, cmd/gitfed-web/handlers_git_http.go implements a small
slice of git's "smart HTTP" protocol (git-upload-pack only) so that
git clone https://<domain>/<owner>/<repo>.git works with no account and
no SSH key — see HOW_IT_WORKS.en.md §8 for the
user-facing explanation. Three structural guarantees:
- Read-only, full stop. There simply is no
git-receive-packroute over HTTP — nothing to bypass, the code to write doesn't exist on this path. repo.Publicis re-checked on every request, never cached — a repo that goes back to private instantly stops being cloneable over HTTP, and a private or nonexistent repo returns the exact same 404, the same as everywhere else in the app (canView).- Routed at the site root (
/{owner}/{repo}.git/...) rather than under a dedicated prefix, so the clone URL looks like what people expect. Go's mux always prefers more specific literal routes, so this can't shadow any other page.
This POST route is explicitly exempted from the same-origin CSRF check
(sameOriginPOST): a git client never sends an Origin/Referer
header. That's not a CSRF hole — the request carries no session cookie and
mutates no state.
8. Merge requests, and why a web-triggered merge doesn't reopen §6
§6 says writes only ever happen over SSH. Merge requests (/repo-mrs,
/repo-mr, cmd/gitfed-web/handlers_merge_requests.go) let a logged-in
user merge one branch into another from the web UI, which sounds like an
exception — it isn't, for a specific reason: the browser never sends git
data. A merge request stores nothing but a title, description, two
branch names and a status (store.MergeRequest); the diff shown on its
page is computed live from the two branches' current tips
(gitexec.BranchDiff), never cached. Clicking "Merge" doesn't upload
anything — it tells gitfed-server "combine these two refs you already
have," and every commit either ref points at only ever got into the repo
through a real SSH-authenticated push in the first place. Nothing resembling
git-receive-pack is reachable from this path.
Authorization is the same CheckAccess(repo, principal, RoleWrite) gate a
git push over SSH already goes through — a web merge grants no new
capability, since any write collaborator could reach the identical end
state by cloning over SSH, merging locally, and pushing back. The one
honest trade-off: the merge commit's author identity comes from the web
session (password login), not an SSH certificate — but that's already true
of every other web-only write (deleting a repo, changing its visibility,
granting/revoking a collaborator), so this isn't a new category of trust.
The merge itself (gitexec.MergeBranches) never touches a branch's real
checked-out state:
- It runs inside a throwaway
git worktree add --detach, discarded (git worktree remove --force) whether it succeeds, conflicts, or errors — the target branch's ref is untouched until the very last step. - A conflict (
git mergeexits non-zero with files still unmerged) is detected before anything is written back —CheckMergeableruns the same attempt in its own throwaway worktree purely to report conflicting files, then discards it. - The one real mutation is
git update-ref refs/heads/<target> <new> <old>— compare-and-swap, not a blind overwrite. If<target>moved between the merge being computed and this call (a concurrentgit push, most likely), the update is rejected instead of silently discarding that push.