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, voirlang.go) dans les données du template ; les chaînes de caractères passent par{{t .Lang "clé"}}, qui va chercher la traduction dansinternal/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
hostPortsont tous, par nature, des ressources à instance unique —deployment.yamlfixereplicas: 1etstrategy: Recreateplutôt qu'un rolling update. - Deux conteneurs, un seul pod. Ils partagent le même volume de
données ; le conteneur
webattend, via une boucle shell, quedata/admin.sockexiste 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-webseul 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-packn'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-packen HTTP — pas de vérification à contourner, le code pour écrire n'existe pas sur ce chemin. repo.Publicrevé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 --detachjetable, 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 mergesort en erreur avec des fichiers non fusionnés) est détecté avant toute écriture —CheckMergeabletente 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 (ungit pushconcurrent, le plus probable), la mise à jour est rejetée plutôt que de silencieusement perdre ce push.