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 ./...etgo 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.goT2 anti-SSRF fédération ✅ internal/federation/wellknown.go(+ssrf_test.go)T3 en-têtes + CSP ✅ cmd/gitfed-web/security_headers.go,render.goT4 timeouts + corps borné ✅ cmd/gitfed-web/main.go,cmd/gitfed-server/main.go,wellknown.goT5 révocation de cert ✅ internal/store/revocations.go,ssh/server.go,admin/admin.go,config.goT6 host key obligatoire ✅ cmd/gitfed-renew-cert/main.goT7 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.goT10 bcrypt coût 12 ✅ internal/admin/admin.goT11 namespace + quota ✅ cmd/gitfed-web/handlers_dashboard.goT12 durcissement K8s ✅ deploy/k8s/deployment.yaml,networkpolicy.yaml,configmap.yamlChoix 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 deSameSite=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 attributsstyle="..."omniprésents.img-srcautorisehttps: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/ratepar 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) ouX-Real-IP. - Journaliser chaque échec dans l'audit (
AppendAudit, actionweb-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
- Valider le domaine avant tout accès réseau (
GrantCollaboratoretFetch) :- hostname RFC-conforme uniquement ; rejeter les IP littérales ; refuser un port autre que le port implicite HTTPS (ou n'autoriser que le 443).
- Bloquer les cibles internes via un
net.Dialer.Control(ouDialContextcustom) sur lehttp.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. - 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-Policystricte. 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) avantjson.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
KeyIdde cert et/ou par empreinte de clé publique + horodatage). - Renseigner
CertChecker.IsRevokeddanscheckCertpour la consulter. - Révoquer automatiquement à
DeleteUseretRemoveUserKey(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-keyest 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(+emptyDirpour les chemins temporaires si besoin),capabilities: { drop: [ALL] },seccompProfile: { type: RuntimeDefault }. NetworkPolicylimitant 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.