Gitfed
bastien-mrq/gitfed / docs / ARCHITECTURE.md
ARCHITECTURE.md Code Aperçu
**Langues :** Français · [English](ARCHITECTURE.en.md)

# Architecture

Comment le code de gitfed est organisé, et pourquoi. Pour le modèle
d'identité/fédération lui-même, voir [`HOW_IT_WORKS.md`](HOW_IT_WORKS.md) ;
pour la marche à suivre de déploiement, voir
[`deploy/k8s/README.md`](../deploy/k8s/README.md).

---

## 1. Vue d'ensemble : quatre binaires, un cœur partagé

```
cmd/
  gitfed-server/       démon : serveur SSH + endpoint /.well-known
  gitfed-web/           interface web (navigation publique + self-service + admin)
  gitfed-tui/            outil d'administration en terminal
  gitfed-renew-cert/     outil client de renouvellement de certificat
```

`gitfed-server` est le seul processus qui touche directement la base de
données et les dépôts git. `gitfed-web` et `gitfed-tui` ne réimplémentent
rien : ils pilotent le même cœur métier via l'interface `admin.Ops`
(§3) — soit en parlant à un `gitfed-server` déjà lancé via un socket,
soit, s'il n'est pas lancé, en ouvrant directement la base.
`gitfed-renew-cert` est complètement à part : un petit client autonome,
pensé pour tourner sur la machine d'un·e utilisateur·ice (cron/systemd
timer), qui ne parle qu'en SSH à une instance distante.

## 2. Les paquets `internal/`

| Paquet | Rôle |
|---|---|
| `ca` | Génère la clé d'autorité de certification de l'instance et signe les certificats utilisateurs. |
| `ssh` | Le serveur SSH lui-même : authentification (clé nue ou certificat), vérification de révocation, exécution de `git-upload-pack`/`git-receive-pack`. |
| `federation` | Découverte `.well-known`, garde-fous anti-SSRF (`wellknown.go`), magasin de confiance et sa politique (`resolver.go`). |
| `acl` | Les règles d'autorisation par dépôt (public/privé, rôles read/write/admin). |
| `gitexec` | Tout ce qui invoque `git` en sous-processus : init, lecture d'arbre/fichier à HEAD, résolution de branche par défaut, historique/diffs de commits, et la plomberie branches/fusion derrière les merge requests (§8). |
| `store` | Persistance bbolt : utilisateurs, dépôts, ACL, sessions, magasin de confiance, journal d'audit, révocations, dépôts épinglés, notifications fédérées, merge requests et leurs commentaires. |
| `admin` | L'interface `Ops` : chaque opération d'administration ou de self-service, dans un seul endroit, implémentée une fois. |
| `adminrpc` | Protocole JSON-sur-socket-Unix qui expose `admin.Ops` à un client distant (voir §3). |
| `opsconnect` | Choisit automatiquement le mode socket-vivant ou base-directe selon qu'un `gitfed-server` tourne déjà. |
| `i18n` | Dictionnaires de traduction (français/anglais) de l'interface web et fonction de résolution de langue. |
| `config` | Chargement du fichier de configuration de l'instance (`gitfed.json`). |
| `version` | Numéro de version, injecté à la compilation. |

## 3. Pourquoi un socket RPC d'administration

bbolt (la base embarquée utilisée par `store`) impose un **verrou exclusif
mono-écrivain** sur son fichier : un seul processus peut l'ouvrir en
écriture à la fois. Si `gitfed-web` ouvrait la base directement pendant que
`gitfed-server` tourne déjà, l'un des deux échouerait à démarrer, ou pire,
les deux se disputeraient le verrou.

La solution : `gitfed-server` expose son `admin.Ops` sur un socket Unix
local (`data/admin.sock`), via un petit protocole JSON maison
(`internal/adminrpc`). `gitfed-web` et `gitfed-tui` s'y connectent comme
client au lieu de rouvrir la base — c'est `internal/opsconnect` qui décide,
au démarrage, s'il faut passer par ce socket (mode « live ») ou ouvrir la
base directement (mode « hors-ligne », utilisé par `gitfed-tui` quand aucun
serveur ne tourne, par exemple pour créer le tout premier compte).

Un piège corrigé au passage : les erreurs qui traversent ce socket perdent
leur identité si on n'y prend pas garde — `errors.New("not found")` côté
serveur redevient une simple chaîne de caractères côté client, qui ne peut
plus être comparée avec `== store.ErrNotFound`. `adminrpc.Client` reconstruit
donc explicitement les erreurs sentinelles connues par correspondance de
message plutôt que de les laisser passer telles quelles.

## 4. L'interface web (`cmd/gitfed-web`)

Chaque page est un handler HTTP qui : vérifie les droits d'accès via
`admin.Ops`, exécute un petit template Go pour produire le corps de la
page, puis appelle `server.render()` qui enveloppe ce corps dans le
"shell" commun (barre de navigation, pied de page, feuille de style,
sprite d'icônes SVG) défini dans `render.go`.

Points notables :

- **Pas de framework JS.** Tout le rendu est fait côté serveur avec
  `html/template` ; le peu de JavaScript (menu profil, bascule d'onglets,
  copie dans le presse-papiers) est un unique bloc inline, dont le hash
  SHA-256 est épinglé dans la politique CSP (voir §6).
- **i18n par fonction de template.** Chaque page passe sa langue courante
  (résolue par cookie, puis `Accept-Language`, voir `lang.go`) dans les
  données du template ; les chaînes de caractères passent par
  `{{t .Lang "clé"}}`, qui va chercher la traduction dans `internal/i18n`.
- **Authentification web indépendante de git.** Le mot de passe web
  (haché bcrypt, coût 12) n'ouvre qu'une session opaque côté web — il
  n'intervient jamais dans l'authentification SSH/git, qui repose
  uniquement sur les clés et certificats (voir `HOW_IT_WORKS.md`).

## 5. Topologie de déploiement

```
┌─────────────────────────── Pod (1 réplique) ───────────────────────────┐
│                                                                          │
│   conteneur "server"              conteneur "web"                      │
│   gitfed-server                   gitfed-web                            │
│   ├─ SSH :2222 (hostPort)         ├─ HTTP :8088                         │
│   ├─ well-known :8443             └─ socket data/admin.sock (client)    │
│   └─ socket data/admin.sock (serveur)                                   │
│                                                                          │
│   volume "data" (PVC) monté dans les deux conteneurs :                  │
│   base bbolt, dépôts bare, clé de CA, clé d'hôte SSH, socket admin      │
└──────────────────────────────────────────────────────────────────────┘
```

- **Une seule réplique, toujours.** bbolt (verrou mono-écrivain), le socket
  admin et le port SSH exposé en `hostPort` sont tous, par nature, des
  ressources à instance unique — `deployment.yaml` fixe `replicas: 1` et
  `strategy: Recreate` plutôt qu'un rolling update.
- **Deux conteneurs, un seul pod.** Ils partagent le même volume de
  données ; le conteneur `web` attend, via une boucle shell, que
  `data/admin.sock` existe avant de démarrer.
- **SSH sur `hostPort: 2222`**, pas 22 — le port 22 du nœud est déjà pris
  par le sshd de la VPS elle-même.
- **`gitfed-web` seul est exposé par une Ingress** (Traefik + cert-manager
  déjà en place sur le cluster pour un autre projet) ; c'est sûr de
  l'exposer publiquement car l'authentification web est réelle (mot de
  passe + session), pas un simple accès admin non protégé.

Le détail pas-à-pas (DNS, premier déploiement, amorçage du compte admin)
est dans [`deploy/k8s/README.md`](../deploy/k8s/README.md).

## 6. Ce qui protège l'instance en production

Voir [`docs/security/AUDIT.md`](security/AUDIT.md) pour le détail complet,
mais en résumé, ce qui est en place structurellement :

- **Écriture uniquement par SSH** — `git-receive-pack` n'est jamais exposé
  autrement, authentifié par clé ou certificat fédéré uniquement (voir §7
  pour la seule exception HTTP, strictement en lecture).
- **Anti-SSRF au moment de la connexion**, pas seulement à la validation du
  nom de domaine (`internal/federation/wellknown.go`) — protège aussi
  contre le DNS-rebinding.
- **CSP stricte** avec script inline épinglé par hash, en-têtes de
  durcissement standards, vérification d'origine sur les requêtes de
  mutation (défense CSRF en profondeur, en complément de `SameSite=Lax`).
- **Révocation de certificat immédiate**, consultée à chaque authentification
  SSH (`internal/ssh/server.go`).
- **Limitation de débit** sur le login web, par compte et par IP.

## 7. Clone HTTPS anonyme pour les dépôts publics

Depuis la version 0.8.0, `cmd/gitfed-web/handlers_git_http.go` implémente
une petite partie du protocole « smart HTTP » de git
(`git-upload-pack` uniquement) pour que
`git clone https://<domaine>/<owner>/<dépôt>.git` fonctionne sans compte ni
clé SSH — voir [`HOW_IT_WORKS.md`](HOW_IT_WORKS.md) §8 pour le détail
utilisateur. Trois garanties structurelles :

- **Lecture seule, point final.** Il n'y a tout simplement aucune route
  `git-receive-pack` en HTTP — pas de vérification à contourner, le code
  pour écrire n'existe pas sur ce chemin.
- **`repo.Public` revérifié à chaque requête**, jamais mis en cache — un
  dépôt qui redevient privé cesse instantanément d'être clonable en HTTP,
  et un dépôt privé ou inexistant renvoie exactement le même 404, comme
  partout ailleurs dans l'app (`canView`).
- **Route au niveau racine** (`/{owner}/{repo}.git/...`) plutôt que sous un
  préfixe dédié, pour que l'URL de clone ressemble à ce que tout le monde
  attend. Le mux de Go priorise toujours les routes littérales plus
  spécifiques, donc ça ne peut pas masquer une autre page.

Cette route POST est explicitement exemptée de la vérification
same-origin CSRF (`sameOriginPOST`) : un client `git` n'envoie jamais
d'en-tête `Origin`/`Referer`. Ce n'est pas une brèche CSRF — cette requête
ne porte aucun cookie de session et ne modifie aucun état.

## 8. Merge requests, et pourquoi une fusion déclenchée depuis le web ne rouvre pas le §6

Le §6 dit que les écritures ne se font que par SSH. Les merge requests
(`/repo-mrs`, `/repo-mr`, `cmd/gitfed-web/handlers_merge_requests.go`)
permettent à un·e utilisateur·ice connecté·e de fusionner une branche dans
une autre depuis l'interface web, ce qui ressemble à une exception — ça
n'en est pas une, pour une raison précise : **le navigateur n'envoie
jamais de donnée git.** Une merge request ne stocke qu'un titre, une
description, deux noms de branche et un statut (`store.MergeRequest`) ; le
diff affiché sur sa page est calculé en direct à partir des pointes
actuelles des deux branches (`gitexec.BranchDiff`), jamais mis en cache.
Cliquer sur « Fusionner » n'envoie rien — ça dit à `gitfed-server`
« combine ces deux refs que tu as déjà », et chaque commit que l'une ou
l'autre ref pointe n'est arrivé dans le dépôt que via un vrai push
authentifié par SSH au départ. Rien qui ressemble à `git-receive-pack`
n'est joignable par ce chemin.

L'autorisation passe par le même verrou `CheckAccess(repo, principal,
RoleWrite)` qu'un `git push` par SSH traverse déjà — une fusion web
n'ouvre aucune capacité nouvelle, puisque n'importe quel collaborateur en
écriture pourrait atteindre le même état final en clonant par SSH, en
fusionnant en local, puis en repoussant. Le seul compromis honnête :
l'identité de l'auteur du commit de fusion vient de la session web
(connexion par mot de passe), pas d'un certificat SSH — mais c'est déjà
vrai de toute autre écriture web (supprimer un dépôt, changer sa
visibilité, accorder/révoquer un collaborateur), donc ce n'est pas une
nouvelle catégorie de confiance.

La fusion elle-même (`gitexec.MergeBranches`) ne touche jamais l'état
réellement extrait (checked-out) d'une branche :

- Elle tourne dans un `git worktree add --detach` jetable, supprimé (`git
  worktree remove --force`) qu'elle réussisse, entre en conflit, ou
  échoue — la ref de la branche cible n'est touchée qu'à la toute
  dernière étape.
- Un conflit (`git merge` sort en erreur avec des fichiers non fusionnés)
  est détecté *avant* toute écriture — `CheckMergeable` tente la même
  fusion dans son propre worktree jetable uniquement pour rapporter les
  fichiers en conflit, puis le jette.
- La seule vraie mutation est `git update-ref refs/heads/<cible> <nouveau>
  <ancien>` — un compare-and-swap, pas un écrasement aveugle. Si `<cible>`
  a bougé entre le calcul de la fusion et cet appel (un `git push`
  concurrent, le plus probable), la mise à jour est rejetée plutôt que de
  silencieusement perdre ce push.