Gitfed
bastien-mrq/gitfed / docs / ARCHITECTURE.md
ARCHITECTURE.md Code Preview

Langues : Français · English

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 ; pour la marche à suivre de déploiement, voir 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.

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

Voir docs/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 §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.