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.