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

Plan d'implémentation des correctifs de sécurité — gitfed

Référence : docs/security/AUDIT.md (audit du 2026-07-28).

État : ✅ intégralement implémenté (2026-07-28). Les 12 tâches (T1–T12) sont livrées. go build ./..., go vet ./... et go test ./... passent. Tests ajoutés : hash CSP vs script rendu, same-origin POST, validation de domaine anti-SSRF, blocage d'IP privées, révocation (store + admin).

Tâche État Fichiers clés
T1 rate-limit login ✅ cmd/gitfed-web/ratelimit.go, handlers_auth.go, main.go
T2 anti-SSRF fédération ✅ internal/federation/wellknown.go (+ ssrf_test.go)
T3 en-têtes + CSP ✅ cmd/gitfed-web/security_headers.go, render.go
T4 timeouts + corps borné ✅ cmd/gitfed-web/main.go, cmd/gitfed-server/main.go, wellknown.go
T5 révocation de cert ✅ internal/store/revocations.go, ssh/server.go, admin/admin.go, config.go
T6 host key obligatoire ✅ cmd/gitfed-renew-cert/main.go
T7 bcrypt temps constant ✅ internal/admin/admin.go (dummyHash)
T8 erreurs génériques + CSRF ✅ render.go (serverError), security_headers.go (sameOriginPOST)
T9 sessions ✅ internal/store/sessions.go, admin.go, cmd/gitfed-server/main.go, session.go
T10 bcrypt coût 12 ✅ internal/admin/admin.go
T11 namespace + quota ✅ cmd/gitfed-web/handlers_dashboard.go
T12 durcissement K8s ✅ deploy/k8s/deployment.yaml, networkpolicy.yaml, configmap.yaml

Choix de conception à noter :

  • CSRF (T8) : implémenté par vérification d'origine (Origin/Referer) sur les méthodes non-sûres plutôt que par jetons par formulaire — robuste, uniforme, sans toucher les 15 formulaires. En complément de SameSite=Lax.
  • CSP (T3) : le script inline est autorisé par son hash SHA-256 (calculé sur le script rendu, pas la constante brute, car html/template réécrit < dans un <script>). style-src 'unsafe-inline' reste requis pour les attributs style="..." omniprésents. img-src autorise https: pour ne pas casser les images de README.
  • SSRF (T2) : double barrière — validation du format de domaine + blocage des IP non-publiques au moment du dial (avec pinning de l'IP validée, anti-DNS-rebinding). Désactivée uniquement en mode insecure_federation (dev/loopback).
  • NetworkPolicy (T12) : nécessite un CNI qui l'applique (Cilium/Calico) ; flannel de k3s ne l'applique pas — sinon elle vaut documentation d'intention.

Ce qui suit est le plan d'origine, conservé pour la traçabilité.

Le plan est découpé en 3 lots par priorité décroissante. Chaque tâche indique le fichier, l'approche, le test de validation et une estimation d'effort (S = < 1 h, M = 1–3 h, L = > 3 h).

Ordre de traitement recommandé : Lot 1 en entier (bloquant prod) → Lot 2 → Lot 3.


Lot 1 — Bloquant avant mise en production

T1 — Rate-limiting du login web (H1 — effort M)

Fichiers : cmd/gitfed-web/handlers_auth.go, nouveau cmd/gitfed-web/ratelimit.go

  • Implémenter un limiteur in-memory (map protégée par mutex, ou golang.org/x/time/rate par clé) indexé sur (IP source, nom d'utilisateur).
  • Politique : back-off progressif après N échecs (ex. 5 essais / 15 min par compte, 20 / 15 min par IP), avec réponse générique et délai constant.
  • Extraire l'IP réelle derrière l'ingress via X-Forwarded-For (en ne faisant confiance qu'au dernier proxy) ou X-Real-IP.
  • Journaliser chaque échec dans l'audit (AppendAudit, action web-login, Allowed:false).
  • Purge périodique des entrées expirées.

Validation : test unitaire simulant N+1 tentatives → la (N+1)ᵉ est refusée ; une IP différente n'est pas affectée après le lockout d'un compte.


T2 — Anti-SSRF sur la découverte de fédération (M1 — effort M)

Fichiers : internal/federation/wellknown.go, internal/admin/admin.go

  1. Valider le domaine avant tout accès réseau (GrantCollaborator et Fetch) :
    • hostname RFC-conforme uniquement ; rejeter les IP littérales ; refuser un port autre que le port implicite HTTPS (ou n'autoriser que le 443).
  2. Bloquer les cibles internes via un net.Dialer.Control (ou DialContext custom) sur le http.Client : après résolution, refuser toute connexion vers une IP privée / loopback / link-local / metadata (127.0.0.0/8, 10/8, 172.16/12, 192.168/16, 169.254/16, ::1, fc00::/7, fe80::/10, 100.64/10). Contrôle appliqué sur l'IP effectivement composée → protège aussi du DNS-rebinding.
  3. Restreindre la surface : n'autoriser la découverte d'un nouveau domaine que pour un admin d'instance (les propriétaires de repo non-admin ne déclenchent pas de fetch sortant), ou passer toute nouvelle découverte par la file d'approbation admin existante.

Validation : tests table-driven — 169.254.169.254, localhost, 10.0.0.1, [::1], domaine avec port arbitraire → tous rejetés ; domaine public valide → autorisé. Test anti-rebinding : nom résolvant vers une IP privée → refusé au dial.


T3 — En-têtes HTTP de sécurité (M3 — effort S)

Fichiers : nouveau cmd/gitfed-web/security_headers.go, cmd/gitfed-web/main.go

  • Middleware enveloppant le mux :
    • Content-Security-Policy stricte. Le <script> inline du shell impose soit un nonce par requête (injecté dans le template et la CSP), soit un hash du script. Préférer le nonce.
    • X-Frame-Options: DENY (+ frame-ancestors 'none' dans la CSP).
    • X-Content-Type-Options: nosniff.
    • Referrer-Policy: strict-origin-when-cross-origin.
    • Strict-Transport-Security: max-age=31536000; includeSubDomains (émis seulement derrière TLS).

Validation : test HTTP vérifiant la présence de chaque en-tête ; contrôle manuel via curl -I ; page fonctionne sans erreur CSP en console.


T4 — Timeouts serveur + corps de réponse borné (M5 — effort S)

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

  • Remplacer les deux http.ListenAndServe(...) par un &http.Server{ ReadHeaderTimeout: 5s, ReadTimeout: 15s, WriteTimeout: 30s, IdleTimeout: 60s, Handler: mux }.
  • Dans Fetch, envelopper le corps : io.LimitReader(resp.Body, 1<<20) (1 Mo) avant json.NewDecoder.

Validation : go build ; test qu'un corps > 1 Mo est tronqué/rejeté proprement.


Lot 2 — Renforcement

T5 — Révocation de certificat (M2 — effort L)

Fichiers : internal/store/ (nouveau bucket revocations), internal/ssh/server.go, internal/admin/admin.go

  • Ajouter une liste de révocation persistée (par KeyId de cert et/ou par empreinte de clé publique + horodatage).
  • Renseigner CertChecker.IsRevoked dans checkCert pour la consulter.
  • Révoquer automatiquement à DeleteUser et RemoveUserKey (invalider les certs portant la clé retirée).
  • Réduire le TTL par défaut à 12–24 h (config.go / InstanceMeta).
  • À défaut d'implémenter la KRL immédiatement : documenter explicitement la fenêtre de 48 h dans le README d'exploitation.

Validation : émettre un cert, révoquer, vérifier que l'auth SSH est refusée ; vérifier qu'un cert non révoqué passe toujours.


T6 — gitfed-renew-cert : host key obligatoire (M4 — effort S)

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

  • Faire échouer si -host-key est absent (au lieu d'InsecureIgnoreHostKey), ou implémenter un TOFU persistant (known_hosts local) avec avertissement à la première connexion seulement.
  • Documenter la récupération de l'empreinte du host key de l'instance.

Validation : sans -host-key → refus ; avec la bonne clé → succès ; avec une mauvaise clé → refus.


T7 — Comparaison bcrypt en temps constant (L1 — effort S)

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

  • Quand l'utilisateur ou le hash est absent, comparer le mot de passe fourni contre un hash bcrypt factice préchargé, puis retourner l'échec — pour égaliser le temps de réponse.

Validation : test comparant grossièrement les temps de réponse utilisateur existant vs inexistant.


T8 — Messages d'erreur génériques + jetons CSRF (défense en profondeur) (L2 — effort M)

Fichiers : handlers web, nouveau helper CSRF

  • Mapper les erreurs internes vers des messages utilisateurs génériques ; journaliser le détail côté serveur.
  • Ajouter un jeton CSRF (double-submit cookie ou jeton lié à la session) sur les formulaires POST, en priorité les actions admin (make-admin, delete-user, approve-domain).

Validation : POST sans jeton valide → rejeté ; parcours normal → OK.


Lot 3 — Défense en profondeur

T9 — Durcissement des sessions (L3 — effort S)

Fichiers : internal/admin/admin.go, internal/store/sessions.go

  • Réduire sessionTTL (ex. 7 j) ; rotation du token à l'élévation de privilège.
  • Tâche de purge périodique des sessions expirées.

T10 — Politique de mot de passe (L4 — effort S)

Fichier : internal/admin/admin.go

  • Passer bcrypt au coût 12.
  • Optionnel : contrôle contre une liste de mots de passe communs.

T11 — Namespace & quotas (L5/L6 — effort M)

Fichiers : internal/admin/admin.go, cmd/gitfed-web/handlers_dashboard.go

  • Préfixer/valider le nom de repo par l'utilisateur créateur, ou réserver le namespace.
  • Introduire un quota configurable de repos par utilisateur.

T12 — Durcissement K8s (L7 — effort S)

Fichier : deploy/k8s/deployment.yaml, nouveau deploy/k8s/networkpolicy.yaml

  • Ajouter readOnlyRootFilesystem: true (+ emptyDir pour les chemins temporaires si besoin), capabilities: { drop: [ALL] }, seccompProfile: { type: RuntimeDefault }.
  • NetworkPolicy limitant l'egress (renforce T2 contre la SSRF).

Séquencement suggéré

Sprint 1 (prod-ready) : T1 → T2 → T3 → T4
Sprint 2 (renfort)    : T5 → T6 → T7 → T8
Sprint 3 (hardening)  : T9 → T10 → T11 → T12

Chaque lot est indépendant et livrable séparément. Aucune tâche n'introduit de changement de rupture d'API. Recommandation : une PR par tâche (ou par lot pour les tâches S), avec les tests de validation associés, et exécution de go test ./... avant merge.