Gitfed
bastien-mrq/gitfed / docs / security / AUDIT.md
AUDIT.md Code Preview

Rapport d'audit de sécurité — gitfed

  • Date : 2026-07-28
  • Périmètre : audit défensif complet avant mise en production, avec focus sur l'authentification et la certification (fédération / CA / SSH).
  • Cible : dépôt gitfed (branche main), commit 523f049 (Deploy v0.6.0).
  • Nature : revue de code statique + inspection de la configuration de déploiement (Docker / Kubernetes).

1. Synthèse

Le cœur de sécurité du projet est sain :

  • Aucune backdoor détectée.
  • La chaîne de confiance fédérée est cryptographiquement vérifiée : CertChecker.CheckCert vérifie la signature du certificat (golang.org/x/crypto/ssh/certs.go:456-459), et le code épingle la clé d'autorité avant vérification (internal/ssh/server.go:148-170).
  • Aucun secret commité : le dossier demo/ (clés CA, host keys, DB) est bien couvert par .gitignore et non suivi par git.
  • Pas d'injection shell : git est invoqué via exec.Command sans passer par un shell (internal/gitexec/gitexec.go).
  • Pas de XSS stocké : html/template échappe automatiquement, et goldmark (sans WithUnsafe) filtre le HTML brut et les URLs dangereuses (javascript:, data:, vbscript:) — vérifié dans goldmark@v1.8.4/renderer/html/html.go:518.
  • Mots de passe hachés en bcrypt, jamais renvoyés par l'API (bucket séparé auth).

Les vulnérabilités identifiées sont des durcissements classiques d'avant-production, pas des failles critiques exploitables à distance sans conditions. La plus prioritaire est l'absence de limitation des tentatives de connexion web.

Tableau récapitulatif

ID Sévérité Titre Fichier principal
H1 🔴 High Aucune limitation des tentatives de login web cmd/gitfed-web/handlers_auth.go:41
M1 🟠 Medium SSRF via la découverte de fédération internal/federation/wellknown.go:47
M2 🟠 Medium Pas de révocation de certificat internal/ssh/server.go:130
M3 🟠 Medium En-têtes HTTP de sécurité absents cmd/gitfed-web/render.go
M4 🟠 Medium gitfed-renew-cert : host key non vérifié par défaut cmd/gitfed-renew-cert/main.go:84
M5 🟠 Medium Timeouts HTTP absents + corps de réponse non borné cmd/gitfed-web/main.go:55, internal/federation/wellknown.go:53
L1 🟡 Low Énumération d'utilisateurs par timing internal/admin/admin.go:332
L2 🟡 Low Fuite de messages d'erreur internes vers l'utilisateur multiples handlers web
L3 🟡 Low Session : TTL 30 j, pas de rotation, pas de purge internal/admin/admin.go:353, internal/store/sessions.go
L4 🟡 Low bcrypt coût 10 ; politique de mot de passe minimale internal/admin/admin.go:312-322
L5 🟡 Low Squat de namespace de repo cmd/gitfed-web/handlers_dashboard.go:88
L6 🟡 Low Pas de quota repos/utilisateurs (DoS disque) internal/admin/admin.go:186
L7 🟡 Low Durcissement conteneur K8s incomplet deploy/k8s/deployment.yaml

2. Détail des vulnérabilités

🔴 H1 — Aucune limitation des tentatives de connexion web

Fichier : cmd/gitfed-web/handlers_auth.go:41 (handleLogin)

Le formulaire de login est exposé sur Internet (ingress /) sans rate-limiting, sans lockout de compte, sans délai ni captcha. bcrypt (coût 10) ralentit chaque essai, mais un brute-force en ligne ciblé reste réalisable.

Aggravant architectural : le socket admin (/data/admin.sock) est un accès « god-mode » sans authentification (protégé uniquement par le mode fichier 0600). Le conteneur web détient donc toute la capacité admin de l'instance ; les gardes de route (requireLogin / requireAdmin) sont la seule barrière entre un visiteur anonyme et le contrôle total. Toute faiblesse du login en amplifie l'impact.

Impact : compromission de compte (y compris admin) par force brute / bourrage d'identifiants.


🟠 M1 — SSRF via la découverte de fédération

Fichiers : internal/federation/wellknown.go:47 (Fetch), internal/admin/admin.go:291 (GrantCollaborator), internal/federation/resolver.go:63 (EnsureTrust)

Fetch(domain) construit https://<domain>/.well-known/gitfed.json et effectue une requête HTTP sortante. Le domain provient de GrantCollaborator, exposé via POST /repo-grant/{repo} à tout propriétaire de repo — pas uniquement à un admin d'instance (la garde est canAdminister sur le repo, handlers_repo.go:441).

Un utilisateur authentifié peut donc ajouter un collaborateur du type x@169.254.169.254 ou x@10.0.0.5:6379 et déclencher une requête vers une IP interne ou l'endpoint de métadonnées cloud.

Limitations pour l'attaquant :

  • SSRF « aveugle » : seul le succès/échec fuit (via le log d'audit et le timing) ; la réponse est validée (doc.Domain == domain + ca_public_key requis) et n'est pas réfléchie.
  • Rate-limité à 10 nouvelles découvertes/minute (resolver.go:33).

Manques : aucune validation du format de domaine, aucun blocage des IP littérales / plages privées / link-local / metadata, pas de protection anti-DNS-rebinding.

Impact : scan de ports interne, sondage de services internes, atteinte potentielle aux métadonnées cloud.


🟠 M2 — Pas de révocation de certificat

Fichiers : internal/ssh/server.go:130 (checkCert), internal/ca/ca.go:101 (IssueUserCert)

Un certificat émis reste valide jusqu'à son expiration (48 h par défaut, config.go:39) même après :

  • la suppression de l'utilisateur (DeleteUser),
  • le retrait de la clé publique certifiée.

checkCert ne valide que la signature CA et le format du principal — il ne vérifie jamais que l'utilisateur ou la clé existent encore côté serveur. CertChecker est instancié sans IsRevoked (server.go:167), donc aucune KRL/CRL n'est consultée.

À l'usage, un utilisateur supprimé conserve, tant que son cert est valide, l'accès en lecture aux repos publics (l'autorisation par repo via acl.Check limite le reste). Mais la révocation immédiate d'un compte compromis est impossible.

Impact : fenêtre de révocation jusqu'à 48 h ; pas de kill-switch pour une clé/compte compromis.


🟠 M3 — En-têtes HTTP de sécurité absents

Fichier : cmd/gitfed-web/render.go, ensemble du serveur web

Aucun en-tête de sécurité n'est émis :

  • Content-Security-Policy (pertinent : le shell contient un <script> inline, render.go:439),
  • X-Frame-Options: DENY / frame-ancestors → clickjacking possible sur les formulaires admin (make-admin, delete-user, approve-domain),
  • X-Content-Type-Options: nosniff,
  • Referrer-Policy,
  • Strict-Transport-Security (HSTS).

Impact : clickjacking, absence de défense en profondeur contre l'exécution de contenu injecté, fuite de referrer.


🟠 M4 — gitfed-renew-cert : host key non vérifié par défaut

Fichier : cmd/gitfed-renew-cert/main.go:84

Sans l'option -host-key, l'outil utilise gossh.InsecureIgnoreHostKey(). Il émet un avertissement mais poursuit la connexion. Un attaquant en position de MITM peut intercepter le renouvellement de certificat.

Impact : interception du canal de renouvellement ; usurpation de l'instance d'origine. Outil côté client, mais distribué avec le projet et documenté comme point d'entrée officiel.


🟠 M5 — Timeouts HTTP absents + corps de réponse non borné

Fichiers : cmd/gitfed-web/main.go:55, cmd/gitfed-server/main.go:127, internal/federation/wellknown.go:53

  • Les serveurs web et well-known utilisent http.ListenAndServe sans ReadHeaderTimeout / ReadTimeout / WriteTimeout / IdleTimeout → exposition Slowloris.
  • Fetch décode resp.Body via json.NewDecoder sans io.LimitReader → un pair distant malveillant peut renvoyer un corps arbitrairement grand (DoS mémoire pendant la découverte).

Impact : épuisement de connexions / mémoire.


🟡 L1 — Énumération d'utilisateurs par timing

Fichier : internal/admin/admin.go:332 (VerifyPassword)

Quand l'utilisateur ou le hash est absent, la fonction retourne sans appeler bcrypt. La différence de temps de réponse permet de distinguer un utilisateur existant d'un inexistant.

Correctif : comparer systématiquement contre un hash bcrypt factice.


🟡 L2 — Fuite de messages d'erreur internes

Fichiers : handlers_auth.go:49,60, handlers_settings.go, handlers admin

Plusieurs chemins renvoient err.Error() brut à l'utilisateur, pouvant divulguer des détails internes (chemins, structure du store).

Correctif : messages génériques côté client, détail journalisé côté serveur.


🟡 L3 — Gestion de session

Fichiers : internal/admin/admin.go:353 (sessionTTL = 30 j), internal/store/sessions.go

  • TTL de 30 jours, sans rotation à l'élévation de privilège, sans expiration d'inactivité.
  • Les sessions expirées ne sont supprimées que paresseusement (à l'accès), pas purgées — croissance non bornée du bucket.

🟡 L4 — Politique de mot de passe / coût bcrypt

Fichier : internal/admin/admin.go:312-322

  • bcrypt DefaultCost (10) ; recommandation 2026 : 12.
  • Longueur minimale 8, sans vérification de mot de passe compromis.

🟡 L5 — Squat de namespace de repo

Fichier : cmd/gitfed-web/handlers_dashboard.go:88 (handleCreateRepo)

Le nom du repo est libre : un utilisateur alice peut créer bob/x. L'owner reste alice@domain (pas d'usurpation d'identité), mais le nom est trompeur. Le path traversal est bien bloqué (ResolvePath rejette ..).


🟡 L6 — Pas de quota

Fichier : internal/admin/admin.go:186 (CreateRepo)

Aucun plafond sur le nombre de repos/utilisateurs. Chaque repo déclenche un git init --bare → DoS disque possible par un utilisateur authentifié.


🟡 L7 — Durcissement conteneur K8s incomplet

Fichier : deploy/k8s/deployment.yaml

Bon socle (non-root, allowPrivilegeEscalation: false, limites de ressources). Manquent :

  • readOnlyRootFilesystem: true,
  • capabilities: { drop: [ALL] },
  • seccompProfile: { type: RuntimeDefault },
  • une NetworkPolicy restreignant les flux sortants (renforce M1).

3. Contrôles vérifiés et jugés sains

Domaine Constat
Certificats Signature vérifiée + autorité épinglée (server.go:163-170)
Fédération Réponse well-known validée (domaine + clé CA présente)
Injection git via exec.Command sans shell ; noms de repo validés par regex + rejet ..
Path traversal Chemins de fichiers bornés par git (git show ref:path)
XSS html/template + goldmark filtre HTML brut et URLs dangereuses
Open redirect sanitizeNext restreint next aux chemins locaux
CSRF Cookies HttpOnly + Secure + SameSite=Lax (couvre le POST cross-site)
Secrets au repos Clés CA / host / DB / socket en 0600 ; demo/ gitignoré
Conteneurs Exécution non-root, privilège non escaladable
TLS Terminaison Traefik + cert-manager (Let's Encrypt) en prod

Prérequis de déploiement à documenter : le endpoint well-known est servi en HTTP clair sur :8443 et dépend entièrement de la terminaison TLS de l'ingress. En Kubernetes c'est correct (insecure_federation: false), mais toute exécution hors de ce chemin ferait transiter la clé publique CA en clair (MITM sur la découverte de fédération). Ne jamais exposer :8443 directement sur Internet sans TLS devant.


4. Note sur le CSRF

L'absence de jetons CSRF explicites est largement compensée par SameSite=Lax : les navigateurs n'envoient pas le cookie de session sur une requête POST cross-site, ce qui protège tous les endpoints de mutation (qui sont en POST). Le seul endpoint mutateur en GET est /lang/{lang} (changement de langue, sans impact sécurité). Le risque résiduel est donc faible, mais l'ajout de jetons CSRF reste recommandé en défense en profondeur pour les actions admin (voir plan, tâche 8).


5. Addendum (2026-07-29) — clone HTTPS anonyme

Ce rapport date du commit 523f049 et reste exact pour ce qu'il décrit à cette date. Depuis, une fonctionnalité délibérée a changé un fait qui n'était pas remis en cause à l'époque, à savoir que gitfed n'exposait strictement aucun protocole git en HTTP : la v0.8.0 ajoute un endpoint HTTP git-upload-pack en lecture seule, réservé aux dépôts déjà publics, pour permettre git clone https://<domaine>/<owner>/<repo>.git sans compte ni clé SSH (demande explicite du mainteneur — voir ARCHITECTURE.md §7 pour le détail d'implémentation et HOW_IT_WORKS.md §8 pour l'explication utilisateur).

Aucune des vulnérabilités listées ci-dessus n'est directement affectée : la surface ajoutée est strictement lecture seule (aucune route git-receive-pack n'existe côté HTTP), repo.Public est revérifié à chaque requête sans mise en cache, et l'exemption CSRF qui l'accompagne (un client git n'envoie jamais Origin/Referer) est sans risque puisque la requête ne porte aucun cookie de session et ne modifie aucun état. Un futur audit devrait néanmoins revalider ce chemin spécifiquement (parsing du protocole git smart-HTTP, gestion des corps gzip, limites de taille/temps).