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.
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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