Gitfed
bastien-mrq/gitfed / docs / HOW_IT_WORKS.en.md
HOW_IT_WORKS.en.md Code Preview
**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); for the practical
how-to (setting up a real federated collaboration step by step), see
[`../FEDERATION.md`](../FEDERATION.md) *(French)*.

---

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

## 8. Special case: cloning a public repo with no account

Everything above describes *authenticated* access — required for any
private repo, and for any write, even to a public one. But a **public**
repo can also be cloned over HTTPS, with no certificate, no SSH key, no
account at all:

```sh
git clone https://chez-moi.fr/alice/mon-projet.git
```

This is strictly a read shortcut, not a second authentication path: no
write is possible this way (there is no `git-receive-pack` route over
HTTP, literally none), and a repo that goes back to private instantly
stops being reachable this way. See [`ARCHITECTURE.en.md`](ARCHITECTURE.en.md)
§7 for the implementation detail.

## 9. Special case: merging a branch from the web

Merge requests let a write collaborator merge one branch into another from
the web UI — a "write" that doesn't go over SSH, which sounds like a
second exception to §2's identity model. It isn't one: the browser never
sends git data, it only tells the server to combine two branches it
already has, and every commit either branch points at only got into the
repo through a real SSH-authenticated push in the first place. Access is
gated by the exact same write-role check a `git push` already goes
through — the web button grants no capability a write collaborator
couldn't already reach by cloning, merging locally, and pushing back. See
[`ARCHITECTURE.en.md`](ARCHITECTURE.en.md) §8 for how the merge itself is
isolated from a branch's real state (scratch worktree, conflict detection
before any write, compare-and-swap ref update).