Gitfed
bastien-mrq/gitfed/ Commits/ 6143712

Make HOW_IT_WORKS and ARCHITECTURE docs bilingual

Adds English translations (HOW_IT_WORKS.en.md, ARCHITECTURE.en.md) alongside the existing French versions, with a language-switch link at the top of each, matching the README pair. Updates cross-references in both READMEs to point at the matching language variant instead of always linking to the French original.

bastien-mrq 2026-07-28 19:36 commit 6143712fe6b4830764d7a38b23551da179dee658 parent 22d57d69044c5dec8f8108f25e7b1e91a5e4b8d5
5 files changed +291 −5
M README.md +5 −5
A docs/ARCHITECTURE.en.md +140 −0
M docs/ARCHITECTURE.md +2 −0
A docs/HOW_IT_WORKS.en.md +142 −0
M docs/HOW_IT_WORKS.md +2 −0
README.md
diff --git a/README.md b/README.md index 41e7cc5..962a6ec 100644 --- a/README.md +++ b/README.md @@ -37,9 +37,9 @@ pull, your home instance signs a short-lived SSH certificate that says who you are. Any instance that has chosen to trust your home instance's certificate authority accepts that certificate — no second account, no shared password, no per-instance registration. The full walkthrough is in -[`docs/HOW_IT_WORKS.md`](docs/HOW_IT_WORKS.md) — written in French, like the -rest of the project's internal docs — and the same explanation, with -diagrams, is served live at `/security` on any running instance. +[`docs/HOW_IT_WORKS.en.md`](docs/HOW_IT_WORKS.en.md), and the same +explanation, with diagrams, is served live at `/security` on any running +instance. ## What's included @@ -57,8 +57,8 @@ diagrams, is served live at `/security` on any running instance. | Doc | What's in it | |---|---| -| [`docs/HOW_IT_WORKS.md`](docs/HOW_IT_WORKS.md) *(French)* | The federation/identity model end to end: certificates, trust store, ACLs, a real push walked through step by step. | -| [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) *(French)* | How the code is organized: the four binaries, the `internal/` packages, why there's an admin RPC socket, the Kubernetes topology. | +| [`docs/HOW_IT_WORKS.en.md`](docs/HOW_IT_WORKS.en.md) | The federation/identity model end to end: certificates, trust store, ACLs, a real push walked through step by step. | +| [`docs/ARCHITECTURE.en.md`](docs/ARCHITECTURE.en.md) | How the code is organized: the four binaries, the `internal/` packages, why there's an admin RPC socket, the Kubernetes topology. | | [`docs/security/AUDIT.md`](docs/security/AUDIT.md) *(French)* | The pre-production security audit and what was fixed as a result. | | [`DESIGN.md`](DESIGN.md) *(French)* | The original design rationale — the "why" behind the architecture, written before implementation started. | | [`CHANGELOG.md`](CHANGELOG.md) | Version history (also served at `/changelog` in the web UI). |
docs/ARCHITECTURE.en.md
diff --git a/docs/ARCHITECTURE.en.md b/docs/ARCHITECTURE.en.md new file mode 100644 index 0000000..c863c8a --- /dev/null +++ b/docs/ARCHITECTURE.en.md @@ -0,0 +1,140 @@ +**Languages:** [Français](ARCHITECTURE.md) · English + +# Architecture + +How gitfed's code is organized, and why. For the identity/federation model +itself, see [`HOW_IT_WORKS.en.md`](HOW_IT_WORKS.en.md); for deployment +steps, see [`deploy/k8s/README.md`](../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. | +| `store` | bbolt persistence: users, repos, ACLs, sessions, trust store, audit log, revocations. | +| `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`, see `lang.go`) into the + template data; strings go through `{{t .Lang "key"}}`, which looks up the + translation in `internal/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 `hostPort` are all, by nature, + single-instance resources — `deployment.yaml` sets `replicas: 1` and + `strategy: Recreate` instead of a rolling update. +- **Two containers, one pod.** They share the same data volume; the `web` + container waits, via a shell loop, for `data/admin.sock` to 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-web` is 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`](../deploy/k8s/README.md). + +## 6. What protects the instance in production + +See [`docs/security/AUDIT.md`](security/AUDIT.md) *(French)* for the full +detail, but in short, what's structurally in place: + +- **No git-over-HTTP protocol** — everything goes through SSH, a single + network entry point for git itself. +- **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.
docs/ARCHITECTURE.md
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 46b354a..ddcf073 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,3 +1,5 @@ +**Langues :** Français · [English](ARCHITECTURE.en.md) + # Architecture Comment le code de gitfed est organisé, et pourquoi. Pour le modèle
docs/HOW_IT_WORKS.en.md
diff --git a/docs/HOW_IT_WORKS.en.md b/docs/HOW_IT_WORKS.en.md new file mode 100644 index 0000000..4ec8c43 --- /dev/null +++ b/docs/HOW_IT_WORKS.en.md @@ -0,0 +1,142 @@ +**Languages:** [Français](HOW_IT_WORKS.md) · English + +# How gitfed works + +This document explains gitfed's central mechanism: how two instances that +have never met can let their users collaborate without a shared account +ever existing between them. For the bigger picture (files, code, +deployment), see the [README](../README.md); for the detail of each Go +package, see [`ARCHITECTURE.en.md`](ARCHITECTURE.en.md). + +--- + +## 1. The problem this solves + +On GitHub or GitLab, collaborating with someone assumes you both have an +account on the same platform, or that one of you migrates. gitfed starts +from a different premise, closer to email or ActivityPub: **each person has +an account on exactly one instance — their own — and that instance can +prove their identity to any other instance that agrees to trust it.** + +Concretely: alice has an account on `chez-moi.fr`. Bob has an account on +`ailleurs.net`. Bob can push code to one of alice's repos without ever +creating an account with her — as long as alice's instance has chosen to +trust Bob's instance. + +## 2. Identity: a certificate instead of a shared password + +Every gitfed instance generates its own **certificate authority** when it's +created (an ed25519 key pair, stored on the data volume). That's the +foundation the whole model rests on: + +1. A local user registers an SSH public key with their instance (via + `gitfed-tui` for the very first account, or on their own afterward under + `Settings → SSH keys`). +2. When they need one, they ask their instance for a certificate + (`ssh git@instance gitfed-cert`, or via the `gitfed-renew-cert` tool on a + scheduled task). The instance checks that the key really belongs to + them, then signs an **SSH certificate** in OpenSSH format. +3. That certificate embeds a **principal** — `bob@ailleurs.net`, never a + bare username — and a short validity window, **24 hours by default**. +4. When Bob connects to alice's instance, he presents this certificate + instead of his bare key. Alice's instance checks two things: that the + certificate's signature matches an authority it trusts, and that the + certificate hasn't been revoked (§6). + +Neither instance needs to contact the other directly for this check — the +signature is enough, much like a passport doesn't need a phone call to the +issuing country to be checked at a border, as long as the border knows the +issuing authority's public key. + +## 3. Trust between instances: the trust store + +Trusting an authority is something that has to be built. The first time +alice's instance encounters the domain `ailleurs.net` (typically because +alice granted `bob@ailleurs.net` access to one of her repos), it triggers a +**federated discovery**: + +1. It fetches `https://ailleurs.net/.well-known/gitfed.json`, a public + document containing that instance's certificate authority's public key. +2. It checks that the document is consistent (the declared domain matches, + a key is present) and that the request doesn't point at an internal or + private address (anti-SSRF protection: IP literals, `127.0.0.0/8`, + `10.0.0.0/8`, `169.254.0.0/16`/cloud metadata, etc. are all refused, + including after DNS resolution). +3. Depending on the policy configured on alice's instance: + - **whitelist (default)** — the domain is recorded as **pending**; an + administrator has to explicitly approve it under + `Admin → Trust store` before any access is granted. + - **auto-trust (TOFU — "trust on first use")** — the domain is approved + immediately on first discovery, to enable only if you're comfortable + with that level of risk. +4. Discovery is capped at **10 new domains per minute** for the whole + instance, to keep a compromised account from using it as a large-scale + network probe. + +Once a domain is approved, verifying a certificate that comes from it is +**entirely local**: no network request is repeated on every `git push`. + +## 4. Authorization: who can do what on which repo + +Federated identity says *who* you are; it says nothing about what you're +allowed to do. That's the job of the ACL model, entirely local to each +repo: + +- Every repo has an **owner** (always admin) and, optionally, + **collaborators**, each with an explicit role: **read**, **write**, or + **admin**. +- A repo is also **public** or **private**. A public repo grants **read** + access to any authenticated principal, from any trusted instance, without + needing to be added as a collaborator. **Write**, on the other hand, is + never implicit — even on a public repo, pushing code requires an explicit + role. +- Granting access to a principal on a domain the instance doesn't know yet + is exactly what triggers the federated discovery described in §3. + +## 5. The full cycle, step by step + +Back to the example: alice wants to give Bob write access to +`alice/mon-projet`. + +1. **On alice's instance** — alice goes into the repo's settings and grants + the *write* role to `bob@ailleurs.net`. +2. If `ailleurs.net` is a domain alice's instance doesn't know yet, it + triggers discovery (§3) and the domain stays **pending** until an admin + approves it. +3. **On Bob's instance** — Bob asks his own instance for a certificate + (`gitfed-cert` or automatic renewal). His instance signs a certificate + carrying the principal `bob@ailleurs.net`. +4. Bob runs + `git push ssh://git@chez-moi.fr:2222/alice/mon-projet.git`, + authenticating with his certificate. +5. Alice's instance checks the certificate's signature against the + authority key it approved for `ailleurs.net`, checks it hasn't been + revoked, then checks in its local ACL that `bob@ailleurs.net` really has + the *write* role on `alice/mon-projet`. +6. If everything checks out, `git-receive-pack` runs normally, exactly as + it would for a local user. + +At no point did Bob need an account, a password, or a registered key on +alice's side. + +## 6. Expiry and revocation + +A 24h certificate lingering after an account is deleted is, in theory, a +residual access path. gitfed handles that with two complementary +mechanisms: + +- **The TTL is short** (24h by default, configurable) — the exposure + window of a compromised key is bounded even without any administrator + action. +- **Revocation is immediate.** Deleting a user or removing one of their SSH + keys records the corresponding revocation (by principal and/or by key + fingerprint); every SSH authentication checks this list before accepting + a certificate, even one that's still cryptographically valid. + +## 7. What is *not* federated + +To be clear about the model's limits: **a repo lives on exactly one +instance.** There's no replication, no automatic mirroring between +instances. Federation is only about identity — who's allowed to push or +read — never about the data itself. If alice's instance goes down, her +repo isn't available anywhere else unless it was cloned elsewhere first.
docs/HOW_IT_WORKS.md
diff --git a/docs/HOW_IT_WORKS.md b/docs/HOW_IT_WORKS.md index 0e83b15..48b52f4 100644 --- a/docs/HOW_IT_WORKS.md +++ b/docs/HOW_IT_WORKS.md @@ -1,3 +1,5 @@ +**Langues :** Français · [English](HOW_IT_WORKS.en.md) + # Comment fonctionne gitfed Ce document explique le mécanisme central de gitfed : comment deux instances