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).