Introduction
Qu’est-ce que gitrust ?
gitrust est une forge Git self-hosted écrite en Rust, conçue pour les équipes de 3 à 20 personnes qui souhaitent maîtriser leur infrastructure de développement. Elle offre un serveur HTTP (:4000) et SSH (:2222), une CI intégrée via Dagger, la génération de SBOM avec Syft et l’intégration Dependency-Track, une gestion fine des permissions (4 rôles), la double authentification, des labels hiérarchiques et une conformité ANSSI PA-074.
Une instance publique de démonstration est accessible sur demo.gitrust.eu.
À qui s’adresse cette documentation ?
Cette documentation est organisée en quatre parties selon leurs publics respectifs :
- Manuel utilisateur — pour les développeurs qui utilisent une instance gitrust existante : push/pull, pull requests, issues, CI, notifications. Prérequis : savoir utiliser Git en ligne de commande.
- Manuel d’administration — pour les ops et self-hosters qui déploient, configurent et maintiennent une instance en production. Prérequis : administration Linux, notions de réseau et TLS.
- Manuel développeur — pour les contributeurs au code gitrust et les développeurs qui construisent des intégrations sur l’API publique. Prérequis : Rust (pour contribuer) ou un langage client (pour l’API).
- Bibliothèque de templates — pour tous les profils, lorsqu’ils cherchent un point de départ prêt à l’emploi (fichiers
.env,docker-compose.yml, configs reverse-proxy, scripts de backup, exemples de clients API).
Comment cette documentation est-elle structurée ?
Cette documentation suit le cadre Diátaxis, qui distingue quatre types de contenu selon le besoin du lecteur :
| Type | Quand l’utiliser | Exemples |
|---|---|---|
| Tutoriels | Quand vous apprenez | Installer gitrust, créer votre premier dépôt |
| Guides pratiques | Quand vous résolvez un problème précis | Configurer SMTP, gérer les clés SSH |
| Référence | Quand vous cherchez une information technique | Variables d’environnement, schéma API REST |
| Explication | Quand vous voulez comprendre le pourquoi | Architecture des crates, modèle RBAC |
Chaque manuel (utilisateur, administration, développeur) contient ces quatre types de contenu organisés de manière indépendante.
Naviguer dans la documentation
- Manuel utilisateur — collaborer sur gitrust en tant que développeur
- Manuel d’administration — déployer et exploiter une instance
- Manuel développeur — contribuer au code ou consommer l’API
- Bibliothèque de templates — configs et scripts copiables
Manuel utilisateur
Ce manuel t’accompagne depuis ta première connexion à une instance gitrust jusqu’à l’autonomie complète : pousser du code, collaborer via les pull requests, gérer les issues et automatiser tes workflows avec la CI intégrée. Tu n’as pas besoin d’administrer ni de comprendre l’architecture interne — ce manuel suppose seulement que tu sais utiliser Git en ligne de commande.
Parcours d’apprentissage
Le manuel est structuré comme un parcours progressif de 4 tutoriels. Chaque tutoriel te laisse dans un état stable, vérifié, avant de passer au suivant. L’aide fournie recule d’un cran à chaque étape (du copier-coller intégral vers la composition autonome).
graph LR
A[Compte créé] --> B[01-premier-pas<br/>20 min]
B --> C[02-cloner-pousser<br/>25 min]
C --> D[03-collaborer<br/>25 min]
D --> E[04-automatiser-ci<br/>20 min]
E --> F[Utilisateur autonome]
Checkpoint de parcours : avant de passer au tutoriel 02, tu dois pouvoir te connecter à gitrust, voir ton dépôt dans l’interface et l’accès SSH configuré. Si ce n’est pas le cas, relis le tutoriel 01.
Checkpoint de parcours : avant le tutoriel 03, tu dois avoir poussé au moins un commit sur ta branche via SSH. Si ce n’est pas le cas, relis le tutoriel 02.
Checkpoint de parcours : avant le tutoriel 04, tu dois avoir ouvert et mergé une pull request. Si ce n’est pas le cas, relis le tutoriel 03.
Tutoriels — apprendre en faisant
Les tutoriels sont guidés pas-à-pas avec des sorties verbatim attendues. Suis-les dans l’ordre — chacun suppose le précédent complété.
- 01 — Premiers pas : 2FA, clé SSH, premier dépôt — ~20 min
- 02 — Cloner et pousser du code — ~25 min
- 03 — Collaborer : équipes, issues, pull requests — ~25 min
- 04 — Automatiser avec la CI — ~20 min
How-to — recettes pour les tâches courantes
Les how-to répondent à « comment faire X » sans explication de fond. Tu peux les consulter dans n’importe quel ordre selon ton besoin immédiat.
Ces guides supposent que tu as suivi le parcours tutoriels. Ils ne répètent pas les étapes de base.
- Gérer ses clés SSH
- Configurer la 2FA
- Créer un Personal Access Token
- Gérer les équipes et permissions
- Ouvrir une pull request
- Fusionner : fast-forward, squash, merge commit
- Utiliser les labels hiérarchiques
- Importer un dépôt externe
- Consulter le SBOM de ton projet
Référence — informations techniques exactes
La référence est destinée à la consultation ponctuelle, pas à la lecture linéaire. Elle documente les formats, schémas et comportements exacts.
- Modèle de permissions (4 rôles)
- API REST v1 — authentification, pagination, codes d’erreur
- Schéma YAML
.gitrust-ci.yml(Easy Mode) - Syntaxe Markdown supportée
- Notifications : SSE, e-mail, préférences
Explication — comprendre le pourquoi
Les explanations t’aident à construire des modèles mentaux durables. Lis-les quand tu veux comprendre pourquoi gitrust fonctionne ainsi, pas seulement comment.
- Modèle de collaboration gitrust
- Sécurité utilisateur : 2FA, PAT, audit log
- Cycle de vie d’une pull request
Tutoriels — Apprendre gitrust pas à pas
Les tutoriels sont des parcours d’apprentissage guidés. Leur rôle est de te faire acquérir des compétences en te faisant réaliser quelque chose de concret — pas de t’informer sur des fonctionnalités. Chaque tutoriel te laisse dans un état précis, avec quelque chose qui fonctionne réellement.
Mode Diátaxis : Tutorial. Si tu cherches à accomplir une tâche précise que tu sais déjà faire, consulte les guides pratiques. Si tu veux comprendre pourquoi gitrust fonctionne ainsi, consulte les explications.
Parcours utilisateur (~90 min)
graph LR
A[Compte créé] --> B["01 — Premiers pas<br/>~20 min"]
B --> C["02 — Cloner et pousser<br/>~25 min"]
C --> D["03 — Collaborer<br/>~25 min"]
D --> E["04 — Automatiser CI<br/>~20 min"]
E --> F[Utilisateur autonome]
style A fill:#e8f4e8
style F fill:#e8f4e8
État initial : tu as un compte gitrust actif et Git installé sur ta machine. État final : tu pousses du code, collabores en équipe via issues et PRs, et bénéficies d’une CI automatique.
Le niveau d’aide diminue progressivement d’un tutoriel à l’autre — c’est volontaire :
| Tutoriel | Niveau d’aide | Nature |
|---|---|---|
| 01 — Premiers pas | 100 % fourni | Copier-coller intégral |
| 02 — Cloner et pousser | 100 % fourni | Copier-coller intégral |
| 03 — Collaborer | 80 % fourni | Quelques trous à compléter |
| 04 — Automatiser CI | 80 % fourni | Configuration à adapter |
Les quatre tutoriels
01 — Premiers pas : active la 2FA, ajoute ta clé SSH et crée ton premier dépôt
Active l’authentification à deux facteurs, enregistre ta première clé SSH et crée un dépôt vide. Point d’entrée obligatoire du parcours.
Bloom : O1 Activer (2FA) · O2 Enregistrer (clé SSH) · O3 Créer (dépôt)
02 — Cloner et pousser du code
Génère une clé SSH ed25519, clone ton dépôt en local et pousse ton premier commit. Tu vérifies ensuite le résultat dans l’interface web.
Bloom : O1 Générer (clé SSH) · O2 Cloner (dépôt via SSH) · O3 Pousser (commit)
Avant de commencer : le tutoriel 01 doit être complété — tu dois avoir un dépôt créé et un compte actif.
03 — Collaborer : équipes, issues et pull requests
Crée une équipe, invite un coéquipier, ouvre une issue, travaille sur une branche et soumets ta première pull request. Tu la fusionnes et vérifies que l’issue se ferme automatiquement.
Bloom : O1 Créer (équipe) · O2 Ouvrir (issue) · O3 Ouvrir et fusionner (PR)
Avant de commencer : le tutoriel 02 doit être complété — tu dois avoir un dépôt cloné en local.
04 — Automatiser les tests avec la CI intégrée
Écris un fichier .gitrust-ci.yml minimal, pousse-le et observe le pipeline s’exécuter en temps réel. Tu lis le statut et les logs sur /{owner}/{repo}/ci.
Bloom : O1 Écrire (config CI) · O2 Déclencher (pipeline) · O3 Lire (résultats)
Avant de commencer : le tutoriel 03 doit être complété — tu dois avoir collaboré sur au moins une PR.
Checkpoint de parcours
Tu es prêt pour le tutoriel 03 si : tu peux exécuter
git push origin maindepuis ton dépôt local sans erreur d’authentification. Sinon, relis le tutoriel 02.
Tu es prêt pour le tutoriel 04 si : tu peux ouvrir une PR, la faire reviewer et la fusionner. Sinon, relis le tutoriel 03.
Après les tutoriels
Une fois les quatre tutoriels complétés, tu n’as plus besoin d’un guide pas-à-pas. Consulte :
- Les guides pratiques pour des recettes ciblées (gérer les clés SSH, configurer la 2FA, importer un dépôt…)
- La référence pour les tableaux de permissions, le schéma CI, l’API REST
- Les explications pour comprendre le modèle de collaboration, la sécurité et le cycle de vie d’une PR
Premiers pas : active la 2FA, ajoute ta clé SSH et crée ton premier dépôt
Objectifs
À la fin de ce tutoriel, tu sauras :
- O1. Activer l’authentification à deux facteurs (2FA) sur ton compte gitrust
- O2. Enregistrer une clé SSH publique dans ton profil pour te connecter sans mot de passe
- O3. Créer un dépôt vide et vérifier qu’il est accessible à l’URL attendue
Pré-requis
- Technique : un compte gitrust existant (e-mail + mot de passe), Git installé (
git --versionrépond), une paire de clés SSH générée ou à générer (ssh-keygen) - Pédagogique : aucun tutoriel préalable requis — c’est le point de départ du parcours
- Temps estimé : ~20 minutes
Vue d’ensemble
Avant de taper la première commande, prenons 2 minutes pour comprendre ce que tu vas faire et pourquoi.
Un dépôt (repository), c’est comme un dossier de projet avec tout l’historique de ses modifications — une boîte d’archives vivante que Git surveille. Sur gitrust, ce dossier vit sur le serveur et peut être partagé avec ton équipe.
Une clé SSH fonctionne comme une serrure et une clé : tu gardes la clé privée sur ta machine (ne la partage jamais), et tu déposes la clé publique dans ton profil gitrust. Ensuite, Git se connecte automatiquement sans te redemander de mot de passe — gitrust reconnaît ta machine.
La 2FA (authentification à deux facteurs) ajoute un second verrou à ton compte : même si quelqu’un vole ton mot de passe, il ne peut pas se connecter sans le code de ton téléphone. C’est une protection essentielle pour tout compte qui héberge du code.
Voici le flux que tu vas configurer :
sequenceDiagram
participant T as Ton navigateur
participant G as gitrust (HTTPS :4000)
participant S as Ton terminal (SSH :2222)
T->>G: Connexion avec e-mail + mdp + code 2FA
T->>G: Dépôt de la clé SSH publique
S->>G: git push via SSH (authentifié par clé)
G-->>T: Dépôt visible dans l'interface web
Étape 1 : Connecte-toi et accède aux paramètres de sécurité
Ouvre ton navigateur et va sur l’URL de ton instance gitrust (par exemple https://demo.gitrust.eu). Connecte-toi avec ton e-mail et ton mot de passe.
Une fois connecté, clique sur ton avatar en haut à droite, puis sur Paramètres (/settings). Dans le menu de gauche, sélectionne Sécurité.
Checkpoint : tu dois voir la page Sécurité avec une section « Authentification à deux facteurs » et le statut « Désactivée ». Si tu ne la vois pas, vérifie que tu es bien connecté avec le bon compte.
Étape 2 : Active la 2FA avec une application d’authentification
Sur la page Sécurité, clique sur Activer la 2FA. gitrust affiche un QR code.
- Ouvre ton application d’authentification (Google Authenticator, Authy, ou tout équivalent compatible TOTP).
- Scanne le QR code avec l’application.
- L’application affiche un code à 6 chiffres qui change toutes les 30 secondes.
- Saisis ce code dans le champ Code de vérification et clique Confirmer.
Sortie attendue (message dans l’interface) :
La 2FA a été activée avec succès.
Conservez vos codes de secours dans un endroit sûr.
gitrust affiche également 10 codes de secours (format XXXX-XXXX). Copie-les et conserve-les hors ligne — ils te permettent de récupérer l’accès si tu perds ton téléphone.
Checkpoint : retourne sur Paramètres → Sécurité. La section 2FA doit maintenant afficher « Activée » en vert. Si elle affiche toujours « Désactivée », recommence l’étape en t’assurant que le code à 6 chiffres est saisi avant qu’il expire.
Étape 3 : Génère ou localise ta clé SSH
Si tu n’as pas encore de paire de clés SSH, génère-en une maintenant dans ton terminal :
ssh-keygen -t ed25519 -C "mon-compte@example.com"
Sortie attendue :
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/toi/.ssh/id_ed25519):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/toi/.ssh/id_ed25519
Your public key has been saved in /home/toi/.ssh/id_ed25519.pub
The key fingerprint is:
SHA256:xXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXx mon-compte@example.com
Appuie sur Entrée pour accepter le chemin par défaut. Tu peux choisir une passphrase ou laisser vide (appuie deux fois sur Entrée). Une passphrase ajoute une couche de sécurité.
Affiche maintenant ta clé publique (le fichier .pub — celui-ci est fait pour être partagé) :
cat ~/.ssh/id_ed25519.pub
Sortie attendue (une seule ligne) :
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx mon-compte@example.com
Copie cette ligne entière dans le presse-papier.
Checkpoint : la commande cat doit afficher une ligne commençant par ssh-ed25519 (ou ssh-rsa si tu as une clé plus ancienne). Si elle affiche « No such file or directory », la génération n’a pas abouti — relance ssh-keygen.
Étape 4 : Enregistre la clé SSH dans ton profil gitrust
Dans ton navigateur, retourne sur Paramètres et clique sur Clés SSH dans le menu de gauche.
Clique sur Ajouter une clé SSH. Un formulaire apparaît :
- Titre : donne un nom descriptif à cette clé (ex.
laptop-perso-2026,poste-bureau) - Clé : colle la ligne copiée à l’étape précédente
Clique Enregistrer la clé.
Sortie attendue (message dans l’interface) :
Clé SSH ajoutée avec succès.
Teste maintenant la connexion SSH depuis ton terminal :
ssh -T git@demo.gitrust.eu -p 2222
Sortie attendue :
Bonjour tonpseudo ! Vous êtes authentifié, mais gitrust ne fournit pas d'accès shell.
Checkpoint : si tu obtiens ce message, ta clé SSH est correctement enregistrée. Si tu obtiens Permission denied (publickey), vérifie que tu as bien copié la clé publique (.pub) et non la clé privée. Consulte la section « Et si ça ne marche pas » ci-dessous.
Étape 5 : Crée ton premier dépôt vide
Dans ton navigateur, clique sur le + en haut à droite (ou navigue vers /repo/new).
Remplis le formulaire :
- Propriétaire : ton pseudo
- Nom du dépôt :
mon-premier-depot(lettres, chiffres, tirets uniquement) - Visibilité : Privé (ou Public selon ta préférence)
- Initialiser le dépôt : coche « Ajouter un README »
Clique sur Créer le dépôt.
Sortie attendue : gitrust te redirige vers la page du dépôt à l’URL :
https://demo.gitrust.eu/tonpseudo/mon-premier-depot
La page affiche un fichier README.md avec le nom du dépôt, et la barre latérale montre 1 commit, 1 branche (main).
Checkpoint : note l’URL affichée dans la barre d’adresse de ton navigateur. Elle doit correspondre au patron https://<instance>/<pseudo>/<nom-depot>. Si tu vois une erreur 404 ou 500, relis le formulaire de création.
Récapitulatif
- ✓ O1 accompli : la 2FA est activée sur ton compte — tu l’as vérifiée sur la page Sécurité qui affiche « Activée »
- ✓ O2 accompli : ta clé SSH publique est enregistrée — tu l’as confirmée avec
ssh -T git@... -p 2222qui a répondu « Vous êtes authentifié » - ✓ O3 accompli : ton premier dépôt est créé et visible à l’URL
/<pseudo>/mon-premier-depot
Et si ça ne marche pas
| Symptôme | Cause probable | Correction |
|---|---|---|
Permission denied (publickey) lors du test SSH | Tu as collé la clé privée au lieu de la clé publique, ou le fichier .pub ne correspond pas à la clé active | Dans gitrust, supprime la clé ajoutée. Retourne au terminal et exécute ssh-add -l pour voir les clés actives. Recopie la bonne clé .pub avec cat ~/.ssh/id_ed25519.pub |
Connection refused lors du test SSH | Le port SSH de l’instance est différent de 2222, ou l’instance est inaccessible | Vérifie avec l’administrateur le port SSH réel. Essaie ssh -T git@<instance> -p 22 en alternative |
| Le code 2FA est refusé alors qu’il semble correct | L’horloge de ton téléphone est désynchronisée (décalage > 30 s) | Active la synchronisation automatique de l’heure sur ton téléphone (Paramètres → Date et heure → Automatique). Réessaie immédiatement après |
| Le dépôt redirige vers une erreur 422 lors de la création | Le nom contient des caractères interdits (espaces, accents, /) | Utilise uniquement des lettres non accentuées, chiffres et tirets. Évite les points en début ou fin de nom |
Prochaine étape
→ 02 — Cloner et pousser du code : clone ton dépôt en local avec SSH, fais ton premier commit et pousse-le sur gitrust (~25 min)
Cloner un dépôt et pousser ton premier commit
Objectifs
À la fin de ce tutoriel, tu sauras :
- O1. Générer une clé SSH ed25519 et la déclarer dans ton profil gitrust (
/settings/keys) - O2. Cloner un dépôt gitrust en local via SSH (
git clone git@…) - O3. Pousser un commit vers gitrust et vérifier le résultat dans l’interface web
Pré-requis
- Technique : Git installé (
git --versionrépond), un terminal, ton instance gitrust accessible - Pédagogique : tutoriel 01 — Premiers pas complété (compte actif, 2FA configurée, un dépôt créé)
- Temps estimé : ~25 minutes
Vue d’ensemble
Avant de taper la première commande, voici ce que tu vas mettre en place et pourquoi.
SSH (Secure Shell) est le protocole que Git utilise pour envoyer et recevoir du code de manière sécurisée. La différence avec HTTPS : au lieu de saisir un mot de passe à chaque fois, tu déposes une clé publique sur le serveur gitrust, et ta machine prouve son identité grâce à la clé privée correspondante que tu gardes chez toi. C’est à la fois plus sûr et plus pratique.
Voici les trois acteurs de ce tutoriel :
sequenceDiagram
participant M as Ta machine (terminal)
participant A as Agent SSH (~/.ssh/)
participant G as gitrust (:2222 SSH)
participant W as gitrust (navigateur)
M->>M: ssh-keygen → génère id_ed25519 + id_ed25519.pub
M->>W: dépôt de id_ed25519.pub dans /settings/keys
M->>A: ssh-add id_ed25519 (optionnel)
M->>G: git clone git@gitrust.example.com:owner/repo.git
G-->>M: copie locale du dépôt
M->>M: édite un fichier, git add, git commit
M->>G: git push origin main
G-->>W: commit visible dans l'interface
Étape 1 : Génère une clé SSH ed25519
Note : si tu as déjà généré une clé ed25519 pendant le tutoriel 01, passe directement à l’étape 2.
Dans ton terminal, lance la commande suivante (remplace l’adresse e-mail par la tienne) :
ssh-keygen -t ed25519 -C "ton@email.com"
Sortie attendue :
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/toi/.ssh/id_ed25519):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/toi/.ssh/id_ed25519
Your public key has been saved in /home/toi/.ssh/id_ed25519.pub
The key fingerprint is:
SHA256:xXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXx ton@email.com
The key's randomart image is:
+--[ED25519 256]--+
| .o+ |
| ... |
+----[SHA256]-----+
Appuie sur Entrée pour accepter le chemin par défaut. Une passphrase est recommandée — elle chiffre la clé privée sur disque.
Affiche ta clé publique (le fichier .pub — celui-là est fait pour être partagé) :
cat ~/.ssh/id_ed25519.pub
Sortie attendue :
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ton@email.com
Copie cette ligne complète dans le presse-papier.
Checkpoint : la ligne commence obligatoirement par ssh-ed25519. Si elle commence par -----BEGIN tu affiches la clé privée par erreur — arrête-toi et relis la commande.
Étape 2 : Déclare la clé dans ton profil gitrust
Dans ton navigateur, va sur Paramètres → Clés SSH (/settings/keys).

Clique Ajouter une clé SSH. Remplis le formulaire :
- Titre : un nom qui identifie la machine (ex.
laptop-maison-2026) - Clé : colle la ligne
ssh-ed25519 …copiée à l’étape 1
Clique Enregistrer.
Sortie attendue (bannière verte dans l’interface) :
Clé SSH ajoutée avec succès.
Vérifie la connexion depuis le terminal :
ssh -T git@gitrust.example.com -p 2222
Sortie attendue :
Bonjour tonpseudo ! Vous êtes authentifié, mais gitrust ne fournit pas d'accès shell.
Checkpoint : tu dois obtenir ce message de bienvenue. Si tu obtiens Permission denied (publickey), relis l’encart « Et si ça ne marche pas » en bas de ce tutoriel.
Étape 3 : Récupère l’URL SSH de ton dépôt
Dans ton navigateur, ouvre la page de ton dépôt (/<tonpseudo>/mon-premier-depot).
Sur la page d’accueil du dépôt, clique sur le bouton Cloner (ou Code). Sélectionne l’onglet SSH et copie l’URL affichée.
Elle a la forme :
git@gitrust.example.com:tonpseudo/mon-premier-depot.git

Checkpoint : l’URL SSH commence par git@ (pas par https://). Si tu ne vois pas d’onglet SSH, l’instance n’a pas de serveur SSH configuré — demande à ton administrateur.
Étape 4 : Clone le dépôt en local
Dans ton terminal, place-toi dans le dossier où tu veux stocker tes projets, puis clone :
cd ~/projets
git clone git@gitrust.example.com:tonpseudo/mon-premier-depot.git
Sortie attendue :
Cloning into 'mon-premier-depot'...
remote: Enumerating objects: 3, done.
remote: Counting objects: 100% (3/3), done.
remote: Compressing objects: 100% (2/2), done.
Receiving objects: 100% (3/3), done.
Entre dans le dossier cloné :
cd mon-premier-depot
ls
Sortie attendue :
README.md
Checkpoint : le fichier README.md est présent — ton dépôt est bien cloné. Si Git affiche fatal: repository not found, vérifie l’URL SSH copiée à l’étape précédente.
Étape 5 : Crée un fichier, commite et pousse
Crée un nouveau fichier dans le dépôt :
echo "# Notes de développement" > NOTES.md
Ajoute-le à l’index Git et crée un commit :
git add NOTES.md
git commit -m "ajoute NOTES.md"
Sortie attendue :
[main 3a8c21f] ajoute NOTES.md
1 file changed, 1 insertion(+)
create mode 100644 NOTES.md
Pousse vers gitrust :
git push origin main
Sortie attendue :
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 8 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 298 bytes | 298.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
To git@gitrust.example.com:tonpseudo/mon-premier-depot.git
a1b2c3d..3a8c21f main -> main
Checkpoint : la dernière ligne montre main -> main avec deux SHA différents, ce qui confirme que les objets ont bien été envoyés.
Étape 6 : Vérifie dans l’interface web
Retourne dans ton navigateur sur la page du dépôt (/<tonpseudo>/mon-premier-depot). Rafraîchis la page.
Tu dois voir :
- Le fichier
NOTES.mdapparaît dans la liste des fichiers - La barre de commits affiche 2 commits (le README initial + ton nouveau commit)
- Le message de commit
ajoute NOTES.mdest visible à côté du fichier

Checkpoint : si le fichier NOTES.md n’est pas visible après rafraîchissement, assure-toi que le git push a bien terminé sans erreur (relis la sortie de l’étape 5).
Récapitulatif
- ✓ O1 accompli : tu as généré une clé SSH ed25519 et vérifié son empreinte SHA256 — elle est enregistrée dans
/settings/keys - ✓ O2 accompli : tu as cloné le dépôt en local avec
git clone git@…et obtenu une copie fonctionnelle - ✓ O3 accompli : tu as poussé un commit avec
git push origin mainet vérifié sa présence dans l’interface web
Et si ça ne marche pas
| Symptôme | Cause probable | Correction |
|---|---|---|
Permission denied (publickey) au git clone ou git push | La clé privée locale ne correspond pas à la clé publique enregistrée sur gitrust, ou l’agent SSH ne la connaît pas | Exécute ssh-add ~/.ssh/id_ed25519 pour ajouter la clé à l’agent, puis réessaie. Vérifie aussi que la bonne clé .pub a été copiée dans gitrust |
Host key verification failed | L’empreinte SSH du serveur gitrust a changé ou est inconnue | Exécute ssh-keyscan -p 2222 gitrust.example.com >> ~/.ssh/known_hosts, puis réessaie. En environnement de test, tu peux vérifier l’empreinte avec l’administrateur |
fatal: repository 'git@…' not found | L’URL SSH est incorrecte, ou le dépôt n’existe pas (typo dans le nom), ou tu n’as pas les droits | Vérifie l’URL sur la page web du dépôt. Assure-toi que le dépôt est bien accessible avec ton compte |
error: failed to push some refs / rejected | La branche distante contient des commits que tu n’as pas en local | Exécute git pull origin main --rebase d’abord, résous les conflits éventuels, puis git push à nouveau |
Prochaine étape
→ 03 — Collaborer : équipes, issues et pull requests : crée une équipe, invite un coéquipier, ouvre une issue et ta première pull request (~25 min)
Collaborer : équipes, issues et pull requests
Objectifs
À la fin de ce tutoriel, tu sauras :
- O1. Créer une équipe et y inviter un coéquipier depuis
/teams/new - O2. Ouvrir une issue pour décrire un problème ou une tâche à réaliser
- O3. Ouvrir une pull request depuis une branche et la fusionner après review
Pré-requis
- Technique : dépôt cloné en local (tutoriel 02 complété), accès à un second compte gitrust (ou un coéquipier disponible)
- Pédagogique : tutoriel 02 — Cloner et pousser complété
- Temps estimé : ~25 minutes
Vue d’ensemble
gitrust organise la collaboration autour de trois concepts qui s’enchaînent naturellement :
- Une équipe (team) regroupe des personnes et définit leur niveau d’accès à un ou plusieurs dépôts. C’est le point d’entrée pour inviter quelqu’un sur un projet.
- Une issue est une unité de travail : un bug à corriger, une feature à ajouter, une question à trancher. Elle porte un numéro (
#N) qui sert de référence partout. - Une pull request (PR) est une demande de fusion d’une branche vers une autre. Elle lie le code proposé à l’issue qu’il résout et déclenche la review de l’équipe.
flowchart LR
T[Équipe créée<br/>/teams/new] --> R[Dépôt assigné<br/>à l'équipe]
R --> I[Issue ouverte<br/>#1]
I --> B[Branche créée<br/>feat/fix-readme]
B --> PR[Pull Request<br/>ouverte]
PR --> Rev[Review approuvée]
Rev --> M[Fusion dans main]
M --> C[Issue fermée<br/>automatiquement]
Scaffolding niveau 2 : dans ce tutoriel, certaines commandes comportent un trou
___à compléter. L’aide est donnée juste après le bloc de code — essaie d’abord sans regarder.
Étape 1 : Crée une équipe
Dans ton navigateur, navigue vers /teams/new.
Remplis le formulaire :
- Nom de l’équipe :
equipe-alpha(lettres minuscules, chiffres, tirets) - Description :
Équipe de développement principale(optionnel)
Clique Créer l’équipe.
Sortie attendue : gitrust te redirige vers la page de l’équipe /teams/equipe-alpha. La page affiche l’équipe avec toi comme premier membre avec le rôle Owner.

Checkpoint : tu vois la page /teams/equipe-alpha avec ton pseudo listé en tant que membre. Si tu obtiens une erreur 422, le nom contient probablement des caractères interdits — utilise uniquement des minuscules et des tirets.
Étape 2 : Invite un coéquipier et assigne le dépôt
Sur la page de l’équipe, dans la section Membres, saisis le pseudo de ton coéquipier et clique Inviter.
Sortie attendue :
Membre ajouté à l'équipe.
Dans la section Dépôts, clique Ajouter un dépôt. Sélectionne mon-premier-depot dans la liste et choisis le niveau d’accès Developer. Clique Confirmer.
Sortie attendue :
Accès au dépôt accordé.
Checkpoint : ton coéquipier doit maintenant voir mon-premier-depot dans son tableau de bord. Demande-lui de vérifier. Le niveau Developer lui permet de cloner, pousser sur les branches non-protégées et ouvrir des PRs.
Étape 3 : Ouvre une issue
Dans ton navigateur, navigue vers /<tonpseudo>/mon-premier-depot/issues/new.
Remplis le formulaire :
- Titre :
Améliorer le README avec une description du projet - Description :
Le fichier README.md est vide. Il faudrait ajouter : - Une description du projet - Les instructions d'installation
Clique Ouvrir l’issue.
Sortie attendue : gitrust crée l’issue et te redirige vers /<tonpseudo>/mon-premier-depot/issues/1. La page affiche l’issue avec le statut Ouverte et le numéro #1.

Checkpoint : l’URL dans ton navigateur doit se terminer par /issues/1. Note ce numéro — tu l’utiliseras dans le message de commit pour fermer l’issue automatiquement.
Étape 4 : Crée une branche pour travailler
Dans ton terminal, depuis le dossier mon-premier-depot, crée une nouvelle branche :
git checkout -b ___
Complète
___avec le nom de branchefeat/ameliorer-readme. La conventiontype/description-courterend les branches lisibles dans la liste.
Réponse :
git checkout -b feat/ameliorer-readme
Sortie attendue :
Switched to a new branch 'feat/ameliorer-readme'
Modifie le fichier README.md :
cat > README.md << 'EOF'
# mon-premier-depot
Un dépôt de démonstration gitrust.
## Installation
Cloner le dépôt :
```bash
git clone git@gitrust.example.com:tonpseudo/mon-premier-depot.git
EOF
Commite et pousse la branche :
```bash
git add README.md
git commit -m "améliore le README - closes #1"
git push origin feat/ameliorer-readme
Sortie attendue :
[feat/ameliorer-readme 7c3d4e5] améliore le README - closes #1
1 file changed, 10 insertions(+), 1 deletion(-)
...
To git@gitrust.example.com:tonpseudo/mon-premier-depot.git
* [new branch] feat/ameliorer-readme -> feat/ameliorer-readme
Checkpoint : la sortie de git push montre [new branch] feat/ameliorer-readme. Si elle montre rejected, exécute d’abord git pull origin main --rebase.
Étape 5 : Ouvre une pull request
Dans ton navigateur, navigue vers /<tonpseudo>/mon-premier-depot/pulls/new.
gitrust peut avoir détecté ta branche et proposer une bannière « Ouvrir une PR pour feat/ameliorer-readme » — clique dessus si elle apparaît.
Remplis le formulaire :
- Titre :
Améliore le README avec description et installation - Branche source :
feat/ameliorer-readme - Branche cible :
main - Description :
Closes #1(gitrust fermera l’issue automatiquement à la fusion) - Reviewers : sélectionne ton coéquipier (s’il est disponible)
Clique Ouvrir la pull request.
Sortie attendue : gitrust crée la PR et te redirige vers /<tonpseudo>/mon-premier-depot/pulls/1.

Checkpoint : la PR est bien à l’état Ouverte et l’onglet Fichiers modifiés montre les changements dans README.md. Si la PR est vide (pas de diff), vérifie que tu as bien sélectionné feat/ameliorer-readme comme branche source.
Étape 6 : Fusionne la pull request
Si ton coéquipier peut review, demande-lui d’approuver la PR avant de fusionner. Dans ce tutoriel, tu peux fusionner directement si tu es seul.
Sur la page de la PR, fais défiler jusqu’à la section Fusion. Sélectionne la stratégie Merge commit (par défaut). Clique Fusionner la pull request.
Sortie attendue (message dans l’interface) :
Pull request fusionnée avec succès.
La PR passe au statut Fusionnée (violet). Navigue vers /<tonpseudo>/mon-premier-depot/issues/1 — l’issue doit être Fermée automatiquement grâce au closes #1 dans le message de commit.

Checkpoint : l’issue #1 est au statut Fermée. La page principale du dépôt affiche le nouveau contenu du README.md. Si l’issue est encore ouverte, vérifie que le message de commit contenait exactement closes #1 (respecter la casse n’est pas requis, mais le numéro doit être correct).
Récapitulatif
- ✓ O1 accompli : tu as créé l’équipe
equipe-alphavia/teams/new, invité un membre et assigné le dépôt avec le niveau Developer - ✓ O2 accompli : tu as ouvert l’issue #1 pour décrire la tâche, obtenant une unité de travail traçable et numérotée
- ✓ O3 accompli : tu as ouvert une PR depuis
feat/ameliorer-readme, la fusionner a fermé l’issue automatiquement grâce àcloses #1
Et si ça ne marche pas
| Symptôme | Cause probable | Correction |
|---|---|---|
| Le coéquipier ne voit pas le dépôt dans son tableau de bord | L’accès au dépôt n’a pas été correctement accordé à l’équipe | Sur la page de l’équipe, vérifie que le dépôt apparaît dans la section Dépôts avec un niveau d’accès. Reconfirme si nécessaire |
| La PR n’a pas de diff (onglet Fichiers modifiés vide) | La branche source est identique à la branche cible | Vérifie que tu as bien poussé depuis feat/ameliorer-readme et non depuis main. Exécute git log --oneline main..feat/ameliorer-readme pour voir les commits en attente |
| L’issue #1 reste ouverte après fusion | Le message de commit ne contient pas le mot-clé reconnu, ou le numéro est incorrect | Les mots-clés acceptés sont closes, fixes, resolves (insensibles à la casse) suivi de #N. Vérifie le message exact avec git log --oneline |
git push refusé avec remote: push blocked | La branche main est protégée et le push direct y est interdit | C’est normal : travaille toujours sur une branche séparée et utilise une PR pour fusionner dans main |
Prochaine étape
→ 04 — Automatiser avec la CI : écris ton premier .gitrust-ci.yml, déclenche un pipeline et lis le résultat (~20 min)
Automatiser les tests avec la CI intégrée
Objectifs
À la fin de ce tutoriel, tu sauras :
- O1. Écrire un fichier
.gitrust-ci.ymlminimal en mode Easy pour un projet Rust ou Node - O2. Déclencher un pipeline CI en poussant un commit
- O3. Lire le résultat d’un pipeline (statut, logs) sur la page
/{owner}/{repo}/ci
Pré-requis
- Technique : dépôt cloné en local avec au moins un fichier source (tutoriel 02 complété), Git installé
- Pédagogique : tutoriel 03 — Collaborer complété
- Temps estimé : ~20 minutes
Vue d’ensemble
La CI (Continuous Integration, intégration continue) automatise l’exécution de tes tests à chaque push. Au lieu de vérifier manuellement que le code compile et que les tests passent, gitrust le fait pour toi — et signale immédiatement si quelque chose est cassé.
Le mode Easy de gitrust CI utilise un fichier .gitrust-ci.yml que tu places à la racine de ton dépôt. Ce fichier décrit en quelques lignes ce que gitrust doit exécuter : compiler, linter, tester. Le serveur de build reçoit le code, l’exécute dans un container Docker isolé, et renvoie les logs en temps réel.
flowchart LR
subgraph "Ta machine"
C[Commit +<br/>git push]
end
subgraph "gitrust (:4000)"
W[CiWorker<br/>Tokio task]
DB[(Pipeline<br/>en base)]
end
subgraph "Serveur de build"
D[Docker<br/>+ Dagger]
CI[ci-engine]
end
C -->|push SSH/HTTP| W
W --> DB
W -->|rsync + SSH| D
D --> CI
CI -->|logs ligne par ligne| W
W --> DB
DB -->|SSE stream| C
Scaffolding niveau 2 : la configuration CI comporte une section incomplète à compléter. Essaie d’abord sans regarder l’indice.
Étape 1 : Vérifie que la CI est activée sur le dépôt
Dans ton navigateur, ouvre ton dépôt et navigue vers Settings (/{owner}/{repo}/settings).
Fais défiler jusqu’à la section CI. Si tu ne vois pas cette section, l’administrateur de l’instance n’a pas encore activé le CI — contacte-le.
Coche CI activé et Déclencher sur push. Clique Enregistrer.

Checkpoint : la case CI activé est cochée et sauvegardée. Si la section CI n’apparaît pas dans les paramètres, la fonctionnalité n’est pas activée sur cette instance — consulte la section « Et si ça ne marche pas ».
Étape 2 : Crée le fichier .gitrust-ci.yml
Dans ton terminal, depuis la racine du dépôt cloné, crée le fichier de configuration CI.
Pour un projet Rust :
# .gitrust-ci.yml
language: rust
build:
command: "cargo build"
checks:
lint: "cargo clippy -- -D warnings"
format: "cargo fmt -- --check"
tests:
command: "___"
Complète
___avec la commande de test Rust standard. Réponse :cargo test.
Pour un projet Node.js :
# .gitrust-ci.yml
language: node
build:
command: "npm install"
checks:
lint: "npm run lint"
tests:
command: "npm test"
Pour ce tutoriel, utilise le fichier le plus adapté à ton projet, ou crée un exemple minimaliste avec Node si tu n’as pas de projet existant :
# Exemple minimal Node — crée les fichiers nécessaires
echo '{"name":"demo","scripts":{"test":"node -e \"console.log(42===42)\"","lint":"echo ok"}}' > package.json
Puis crée .gitrust-ci.yml avec le contenu Node ci-dessus.
Checkpoint : exécute ls -la dans le dépôt — tu dois voir .gitrust-ci.yml dans la liste. La taille du fichier doit être supérieure à 0 octet.
Étape 3 : Commite et pousse pour déclencher la CI
Ajoute le fichier et crée un commit :
git add .gitrust-ci.yml
git commit -m "ajoute configuration CI gitrust"
git push origin main
Sortie attendue :
[main a4f7b8c] ajoute configuration CI gitrust
1 file changed, 10 insertions(+)
create mode 100644 .gitrust-ci.yml
...
To git@gitrust.example.com:tonpseudo/mon-premier-depot.git
3a8c21f..a4f7b8c main -> main
Immédiatement après le push, dans ton navigateur, navigue vers /{owner}/{repo}/ci.
Sortie attendue : une entrée de pipeline apparaît avec le statut En attente ou En cours, associée au commit a4f7b8c et au message ajoute configuration CI gitrust.

Checkpoint : le pipeline apparaît dans la liste. S’il n’apparaît pas après 10 secondes, rafraîchis la page. S’il n’apparaît toujours pas, vérifie que la CI est bien activée (étape 1) et que le fichier .gitrust-ci.yml est bien à la racine du dépôt (pas dans un sous-dossier).
Étape 4 : Lis les logs du pipeline
Clique sur le pipeline dans la liste pour ouvrir la page de détail.
La page affiche :
- Le statut en cours (Pending → Running → Success ou Failure)
- Le commit et la branche associés
- Les logs en temps réel, mis à jour ligne par ligne
Attends la fin de l’exécution. Pour un projet minimal, cela prend 1 à 3 minutes selon la rapidité du serveur de build.
Sortie attendue pour un pipeline réussi (dernières lignes des logs) :
[build] cargo build: ok
[checks] cargo clippy: ok
[tests] cargo test: ok
Pipeline terminé avec succès.
Pour un pipeline en échec, les logs montrent la commande qui a échoué et le code de retour :
[tests] cargo test: FAILED (exit code 1)
error[E0277]: the trait bound `...` is not satisfied
Pipeline terminé en échec.

Checkpoint : le pipeline passe au statut Réussi (vert) ou Échoué (rouge). Dans les deux cas, tu as atteint l’objectif O3 — tu sais lire le résultat. Si le statut reste bloqué sur En cours plus de 10 minutes, consulte la section « Et si ça ne marche pas ».
Récapitulatif
- ✓ O1 accompli : tu as écrit un fichier
.gitrust-ci.ymlavec les sectionslanguage,build,checksettests, adapté à ton langage - ✓ O2 accompli : tu as déclenché le pipeline en poussant un commit — gitrust l’a automatiquement détecté et lancé l’exécution
- ✓ O3 accompli : tu as lu le statut et les logs sur
/{owner}/{repo}/ci, identifiant si les étapes build/checks/tests ont réussi ou échoué
Et si ça ne marche pas
| Symptôme | Cause probable | Correction |
|---|---|---|
| Aucun pipeline n’apparaît après le push | La CI n’est pas activée sur le dépôt, ou le fichier .gitrust-ci.yml est absent/mal placé | Vérifie Settings → CI (case CI activé cochée). Vérifie que .gitrust-ci.yml est à la racine avec git ls-files .gitrust-ci.yml |
| Le pipeline reste en statut En attente indéfiniment | Le serveur de build n’est pas accessible depuis gitrust, ou CI_MAX_CONCURRENT est atteint | Demande à l’administrateur de vérifier la connectivité SSH vers le builder et les logs journalctl -u gitrust |
Le pipeline échoue à l’étape build avec command not found | Le langage sélectionné n’a pas les outils installés sur le serveur de build | Vérifie avec l’administrateur que le profil du langage (rust, node) est disponible. Consulte la référence des schémas CI |
| Les logs s’affichent puis le pipeline reste en statut En cours | Timeout atteint (CI_DEFAULT_TIMEOUT) ou processus bloqué | Vérifie si le test boucle indéfiniment. Contacte l’administrateur pour ajuster CI_DEFAULT_TIMEOUT |
Prochaine étape
Tu as maintenant complété le parcours utilisateur de base : compte sécurisé → code versionné → collaboration en équipe → CI automatisée.
Pour aller plus loin :
- Comment gérer les clés SSH — plusieurs machines, rotation des clés
- Référence du schéma
.gitrust-ci.yml— toutes les options disponibles - Comprendre le cycle de vie d’une pull request — les états et transitions d’une PR
Guides pratiques — Recettes orientées tâche
Les guides pratiques (how-to guides) sont des recettes : ils répondent à la question « comment faire X ? » quand tu sais déjà ce que tu veux accomplir. Ils supposent que tu es déjà opérationnel — pas besoin d’explication du contexte ou de prise en main.
Mode Diátaxis : How-to. Si tu apprends gitrust pour la première fois, commence par les tutoriels. Si tu veux comprendre pourquoi gitrust fonctionne ainsi, consulte les explications. Pour les tableaux de référence exhaustifs, consulte la référence.
Les neuf guides pratiques
Sécurité et accès
Gérer ses clés SSH
Générer une clé ed25519 ou RSA-4096, l’ajouter ou la supprimer via /settings/keys, vérifier l’empreinte SHA256, diagnostiquer ssh -T git@….
Configurer la double authentification (2FA)
Activer le TOTP via /settings/security/2fa/setup, scanner le QR code, sauvegarder les codes de secours, désactiver ou régénérer.
Créer un Personal Access Token (PAT)
Créer un token via /settings/tokens, choisir les scopes repo:read/repo:write, l’utiliser avec git clone https:// ou le header Authorization: Bearer, révoquer.
Collaboration et code review
Gérer les équipes et les permissions Créer une équipe, inviter des membres, attribuer des rôles (Owner/Maintainer/Member), assigner un dépôt avec un niveau d’accès (Reader/Developer/Maintainer/Owner).
Ouvrir une pull request
Depuis une branche, ouvrir une PR, rédiger la description avec Closes #N, assigner des reviewers, suivre les statuts CI.
Choisir une stratégie de fusion Comparer fast-forward, squash et merge commit — quand utiliser chacune, permissions requises, équivalent API.
Utiliser les labels et les issues
Labels à deux niveaux (classification + sujet), filtrer les issues, fermer automatiquement via closes #N dans un commit ou une PR.
Gestion des dépôts
Importer un dépôt externe
Importer via l’UI /import, URL HTTPS publique, suivre la progression SSE, gérer les erreurs courantes (timeout, taille, auth).
Consulter le SBOM d’un dépôt Page Security d’un dépôt, vulnérabilités via Dependency-Track, télécharger le fichier CycloneDX.
Après les guides pratiques
Pour les tableaux de référence complets (permissions, schéma CI, API REST, notifications) : consulte la référence.
Comment gérer ses clés SSH
Quand utiliser ce guide
Utilise ce guide quand tu veux :
- Ajouter une clé SSH depuis une nouvelle machine
- Générer une clé plus sécurisée (ed25519 au lieu de RSA)
- Vérifier l’empreinte d’une clé avant de l’ajouter
- Supprimer une clé d’une machine perdue ou compromise
- Diagnostiquer une erreur
Permission denied (publickey)
Pré-requis
- Un compte gitrust actif
- Un terminal avec
ssh-keygendisponible (ssh-keygen --versiondoit répondre)
Étapes
1. Générer une clé ed25519 (recommandé)
ed25519 est l’algorithme recommandé : clés courtes, très rapides, résistantes aux attaques modernes.
ssh-keygen -t ed25519 -C "description-de-la-machine"
Exemple de description : laptop-maison-2026, serveur-ci, poste-bureau.
Sortie attendue :
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/toi/.ssh/id_ed25519):
Enter passphrase (empty for no passphrase):
Your identification has been saved in /home/toi/.ssh/id_ed25519
Your public key has been saved in /home/toi/.ssh/id_ed25519.pub
The key fingerprint is:
SHA256:xXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXx description-de-la-machine
Une passphrase est fortement recommandée : elle chiffre la clé privée sur le disque. Si ta machine est volée, la clé reste inutilisable sans la passphrase.
2. Générer une clé RSA-4096 (alternative)
Si un outil ou un service impose RSA :
ssh-keygen -t rsa -b 4096 -C "description-de-la-machine"
La clé sera dans ~/.ssh/id_rsa et ~/.ssh/id_rsa.pub. RSA-4096 est sûr mais plus lent qu’ed25519.
3. Afficher la clé publique à copier
La clé publique (fichier .pub) est faite pour être partagée :
cat ~/.ssh/id_ed25519.pub
Sortie (une ligne) :
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx description-de-la-machine
Copie cette ligne entière.
4. Ajouter la clé dans gitrust (/settings/keys)
Dans ton navigateur, navigue vers Paramètres → Clés SSH (/settings/keys).
Clique Ajouter une clé SSH :
- Titre : le même que la description (ex.
laptop-maison-2026) - Clé : colle la ligne
ssh-ed25519 …copiée
Clique Enregistrer.

5. Lister les clés enregistrées
La page /settings/keys affiche toutes tes clés avec :
- Leur titre
- Leur empreinte SHA256 (pour vérification)
- La date d’ajout
- Un bouton Supprimer
6. Vérifier l’empreinte SHA256 d’une clé locale
Pour confirmer qu’une clé locale correspond bien à une clé enregistrée dans gitrust :
ssh-keygen -lf ~/.ssh/id_ed25519.pub
Sortie :
256 SHA256:xXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXx description-de-la-machine (ED25519)
Compare ce SHA256 avec celui affiché dans /settings/keys. Ils doivent être identiques.
7. Tester la connexion SSH
ssh -T git@gitrust.example.com -p 2222
Sortie attendue :
Bonjour tonpseudo ! Vous êtes authentifié, mais gitrust ne fournit pas d'accès shell.
Si tu as plusieurs clés, spécifie laquelle utiliser :
ssh -T -i ~/.ssh/id_ed25519 git@gitrust.example.com -p 2222
8. Supprimer une clé
Dans /settings/keys, clique Supprimer à côté de la clé concernée. La suppression est immédiate — toute connexion utilisant cette clé sera refusée dès la prochaine tentative.
Variantes
Plusieurs machines, plusieurs clés
Tu peux enregistrer autant de clés que nécessaire dans gitrust. Chaque machine a sa propre paire de clés — tu ne copies jamais une clé privée d’une machine à l’autre.
Pour avoir plusieurs clés sur la même machine et choisir laquelle utiliser selon le serveur, crée un fichier ~/.ssh/config :
Host gitrust.example.com
HostName gitrust.example.com
User git
Port 2222
IdentityFile ~/.ssh/id_ed25519_gitrust
Ensuite git clone git@gitrust.example.com:owner/repo.git utilisera automatiquement cette clé.
Ajouter la clé à l’agent SSH (évite de saisir la passphrase à chaque push)
ssh-add ~/.ssh/id_ed25519
Sur macOS, ajoute --apple-use-keychain pour que la passphrase soit mémorisée dans le trousseau système.
Dépannage ssh -T
| Symptôme | Cause probable | Correction |
|---|---|---|
Permission denied (publickey) | La clé privée locale ne correspond pas à une clé publique enregistrée, ou l’agent ne la connaît pas | Exécute ssh-add ~/.ssh/id_ed25519, puis réessaie. Vérifie que la bonne clé .pub est dans /settings/keys |
Connection refused | Le port 2222 est bloqué ou le service SSH de gitrust est arrêté | Essaie le port 22 : ssh -T git@gitrust.example.com -p 22. Contacte l’administrateur |
Host key verification failed | L’empreinte du serveur a changé ou est inconnue | Exécute ssh-keyscan -p 2222 gitrust.example.com >> ~/.ssh/known_hosts. Vérifie l’empreinte avec l’administrateur avant d’accepter |
Too many authentication failures | L’agent SSH propose trop de clés | Spécifie explicitement : ssh -i ~/.ssh/id_ed25519 -o IdentitiesOnly=yes -T git@… -p 2222 |
Voir aussi
- Tutoriel 02 — Cloner et pousser : mise en place complète SSH + premier clone
- Comprendre la sécurité côté utilisateur : pourquoi les clés SSH sont plus sûres que les mots de passe
- Créer un Personal Access Token : alternative SSH pour les accès HTTPS et les scripts
Comment configurer la double authentification (2FA)
Quand utiliser ce guide
Utilise ce guide quand tu veux :
- Activer la 2FA TOTP sur ton compte gitrust
- Scanner le QR code avec une nouvelle application
- Sauvegarder tes codes de secours
- Désactiver la 2FA (par exemple pour changer d’application)
- Régénérer tes codes de secours après les avoir utilisés
Pré-requis
- Un compte gitrust actif
- Une application d’authentification TOTP installée sur ton téléphone : Google Authenticator, Authy, Bitwarden Authenticator, ou tout équivalent compatible RFC 6238
Étapes
1. Accéder à la page de sécurité
Dans ton navigateur, navigue vers Paramètres → Sécurité (/settings/security).
La section Authentification à deux facteurs affiche l’état actuel : « Désactivée » ou « Activée ».

2. Lancer la configuration TOTP
Clique Activer la 2FA. gitrust génère un secret TOTP et affiche un QR code.
3. Scanner le QR code
Dans ton application d’authentification :
- Ouvre l’application
- Ajoute un nouveau compte (icône
+ou « Ajouter un compte ») - Choisis Scanner un QR code
- Pointe l’appareil photo vers le QR code affiché dans gitrust
L’application ajoute un compte nommé gitrust (tonpseudo) et affiche un code à 6 chiffres qui se renouvelle toutes les 30 secondes.
Si tu ne peux pas scanner le QR code (appareil photo indisponible) : clique sur « Afficher la clé secrète ». Copie la chaîne de 32 caractères et saisis-la manuellement dans ton application (option « Saisir une clé »).
4. Vérifier le code et valider
Dans gitrust, saisis le code à 6 chiffres affiché par ton application dans le champ Code de vérification. Clique Confirmer.
Le code est valide pendant 30 secondes. Si l’horloge de ton téléphone n’est pas synchronisée, le code peut être rejeté — active la synchronisation automatique de l’heure dans les paramètres de ton téléphone.
Sortie attendue :
La 2FA a été activée avec succès.
Conservez vos codes de secours dans un endroit sûr.
5. Sauvegarder les codes de secours
gitrust affiche 10 codes de secours au format XXXX-XXXX. Ces codes permettent de te connecter si tu perds accès à ton application d’authentification.
Actions à effectuer immédiatement :
- Copie les 10 codes
- Stocke-les dans un gestionnaire de mots de passe (Bitwarden, KeePass…) ou sur papier dans un endroit sécurisé
- Ne les stocke pas dans le même appareil que ton application 2FA
Chaque code ne peut être utilisé qu’une seule fois.
Variantes
Désactiver la 2FA
Sur /settings/security, dans la section 2FA, clique Désactiver la 2FA. gitrust te demande de confirmer avec ton mot de passe.
Après désactivation, ton compte n’est protégé que par ton mot de passe. Si ton instance force la 2FA globalement (décision administrateur), tu ne pourras pas la désactiver.
Régénérer les codes de secours
Si tu as utilisé tous tes codes ou crains qu’ils soient compromis :
Sur /settings/security, clique Régénérer les codes de secours. Les anciens codes sont immédiatement invalidés. gitrust affiche 10 nouveaux codes — sauvegarde-les comme à l’étape 5.
Changer d’application d’authentification
- Désactive la 2FA (voir ci-dessus)
- Installe la nouvelle application
- Réactive la 2FA depuis le début (étape 1)
Si tu as encore accès à l’ancienne application, tu peux aussi simplement ajouter le même compte dans la nouvelle application en rescannant le QR code pendant l’activation.
Voir aussi
- Tutoriel 01 — Premiers pas : activation de la 2FA dans le contexte du parcours complet
- Comprendre la sécurité côté utilisateur : pourquoi la 2FA protège même si ton mot de passe est volé
- Créer un Personal Access Token : accès API sans code 2FA interactif
Comment créer un Personal Access Token (PAT)
Quand utiliser ce guide
Utilise ce guide quand tu veux :
- Accéder à l’API gitrust depuis un script ou un outil CI sans exposer ton mot de passe
- Cloner un dépôt via HTTPS depuis un environnement où SSH n’est pas disponible
- Révoquer un accès accordé à un outil tiers sans changer ton mot de passe
- Comprendre quels scopes attribuer à un token selon l’usage
Pré-requis
- Un compte gitrust actif
Étapes
1. Accéder à la gestion des tokens
Dans ton navigateur, navigue vers Paramètres → Tokens d’accès (/settings/tokens).
La page liste tes tokens existants avec leur nom, leur date de création et leur date d’expiration.

2. Créer un nouveau token
Clique Créer un token. Remplis le formulaire :
- Nom : un nom descriptif qui rappelle l’usage (ex.
script-backup-2026,ci-github-actions,cli-perso) - Expiration : choisis une durée (30 jours, 90 jours, 1 an, ou sans expiration). Pour les scripts de CI, préfère une expiration explicite.
- Scopes : sélectionne uniquement les permissions nécessaires (principe du moindre privilège)
| Scope | Permet |
|---|---|
repo:read | Lire les dépôts, les issues, les PRs, les commits |
repo:write | Créer des issues, commenter, pousser du code, ouvrir des PRs |
user:read | Lire les informations de ton profil |
Clique Générer le token.
3. Copier le token immédiatement
gitrust affiche le token une seule fois :
gr_pat_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Copie-le immédiatement dans ton gestionnaire de mots de passe. Cette valeur ne sera plus jamais affichée — si tu la perds, tu devras révoquer le token et en créer un nouveau.
Utiliser le token
Avec git clone via HTTPS
git clone https://tonpseudo:gr_pat_xxxx@gitrust.example.com/owner/repo.git
Pour éviter que le token apparaisse dans l’historique du shell, stocke-le dans une variable d’environnement :
export GITRUST_TOKEN="gr_pat_xxxx" git clone https://tonpseudo:${GITRUST_TOKEN}@gitrust.example.com/owner/repo.git
Avec l’API REST (header Authorization)
curl -H "Authorization: Bearer gr_pat_xxxx" \
https://gitrust.example.com/api/v1/user
Réponse attendue :
{
"id": 42,
"username": "tonpseudo",
"email": "ton@email.com"
}
Dans un script Python
import urllib.request
import json
token = "gr_pat_xxxx"
req = urllib.request.Request(
"https://gitrust.example.com/api/v1/user",
headers={"Authorization": f"Bearer {token}"}
)
with urllib.request.urlopen(req) as resp:
user = json.load(resp)
print(user["username"])
Révoquer un token
Dans /settings/tokens, clique Révoquer à côté du token concerné. La révocation est immédiate — toute requête utilisant ce token recevra une réponse 401 Unauthorized.
Cas où révoquer immédiatement :
- Token exposé accidentellement dans un dépôt public ou des logs
- Machine ou outil auquel le token était destiné est compromise
- Token inutilisé depuis longtemps
Variantes
Token sans expiration
Acceptable pour les outils personnels sur une machine de confiance. À déconseiller pour les environnements partagés (CI d’entreprise, scripts déployés sur un serveur). Préfère dans ces cas un token avec expiration de 90 jours renouvelé via un secret manager.
Plusieurs tokens pour un même outil
Crée un token par environnement (local, staging, prod) avec des scopes distincts. Ainsi, compromettre le token de staging n’expose pas la production.
Voir aussi
- Référence API REST v1 : liste des endpoints et authentification JWT vs PAT
- Comprendre la sécurité côté utilisateur : comment gitrust stocke les tokens (hash, jamais en clair)
- Gérer ses clés SSH : alternative SSH pour les opérations Git
Comment gérer les équipes et les permissions
Quand utiliser ce guide
Utilise ce guide quand tu veux :
- Créer une équipe et y regrouper des collaborateurs
- Inviter ou retirer un membre d’une équipe
- Assigner une équipe à un dépôt avec un niveau d’accès précis
- Choisir le bon rôle (Owner / Maintainer / Member) pour chaque membre
- Comprendre la différence entre les rôles d’équipe et les niveaux d’accès aux dépôts
Pré-requis
- Un compte gitrust actif
- Être propriétaire (Owner) de l’équipe ou de l’organisation pour gérer les membres et accès
Étapes
1. Créer une équipe
Navigue vers Équipes → Nouvelle équipe (/teams/new).
Remplis le formulaire :
- Nom : identifiant de l’équipe (lettres minuscules, chiffres, tirets — ex.
backend-core,devops-ops) - Description : optionnel, visible par les membres
Clique Créer l’équipe. gitrust te redirige vers /teams/{slug}.

2. Ajouter des membres
Sur la page de l’équipe (/teams/{slug}), dans la section Membres :
- Saisis le pseudo du collaborateur dans le champ de recherche
- Sélectionne son compte dans la liste déroulante
- Choisis son rôle dans l’équipe (voir tableau ci-dessous)
- Clique Inviter
| Rôle dans l’équipe | Ce que le membre peut faire |
|---|---|
| Member | Utiliser les dépôts assignés selon le niveau d’accès |
| Maintainer | Gérer les dépôts assignés à l’équipe |
| Owner | Gérer les membres, les dépôts et les paramètres de l’équipe |
Un seul Owner par équipe est requis. Désigne un Owner de secours pour éviter le blocage en cas d’absence.
3. Assigner un dépôt à l’équipe
Sur la page de l’équipe, dans la section Dépôts :
- Clique Ajouter un dépôt
- Sélectionne le dépôt dans la liste
- Choisis le niveau d’accès au dépôt (voir tableau ci-dessous)
- Clique Confirmer
| Niveau d’accès dépôt | Lire | Pousser | Ouvrir/merger PR | Gérer le dépôt |
|---|---|---|---|---|
| Reader | ✓ | — | — | — |
| Developer | ✓ | ✓ (branches) | ✓ | — |
| Maintainer | ✓ | ✓ | ✓ | partiel |
| Owner | ✓ | ✓ | ✓ | ✓ |
Pour la référence complète des permissions par action, consulte le modèle de permissions.
4. Retirer un membre
Sur la page de l’équipe, dans la section Membres, clique Retirer à côté du membre concerné. Le retrait est immédiat — le membre perd l’accès aux dépôts de l’équipe.
5. Modifier le niveau d’accès d’un dépôt
Pour changer le niveau d’accès d’un dépôt déjà assigné à une équipe : retire le dépôt (bouton Retirer dans la section Dépôts), puis réassigne-le avec le nouveau niveau.
Variantes
Accès direct à un dépôt (sans équipe)
Il est possible d’ajouter un collaborateur directement sur un dépôt via /{owner}/{repo}/collaborators. Cette approche est adaptée aux cas ponctuels (ex. un prestataire externe sur un seul dépôt). Pour des équipes permanentes, l’approche par équipe est préférable : un seul point de gestion pour plusieurs dépôts.
Un utilisateur appartient à plusieurs équipes
Si un utilisateur appartient à plusieurs équipes qui ont des niveaux d’accès différents sur un même dépôt, c’est le niveau le plus élevé qui s’applique. Par exemple : Team A avec Reader + Team B avec Developer → l’utilisateur a Developer sur ce dépôt.
Voir aussi
- Tutoriel 03 — Collaborer : mise en pratique complète avec une équipe réelle
- Modèle de permissions (référence) : matrice complète des 4 rôles × actions
- Ouvrir une pull request : prérequis d’accès pour ouvrir et fusionner une PR
Comment ouvrir une pull request
Quand utiliser ce guide
Utilise ce guide quand tu veux :
- Proposer des modifications de code pour review avant fusion
- Lier une PR à une issue pour la fermer automatiquement à la fusion
- Assigner des reviewers à une PR
- Vérifier que la CI est verte avant de fusionner
Pré-requis
- Un dépôt avec au moins deux branches (ta branche de travail +
mainou autre branche cible) - Le niveau d’accès Developer minimum sur le dépôt
Étapes
1. Pousser ta branche de travail
Avant d’ouvrir une PR, ta branche doit être poussée sur gitrust :
git push origin ma-branche
Sortie attendue :
To git@gitrust.example.com:owner/repo.git
* [new branch] ma-branche -> ma-branche
2. Ouvrir le formulaire de nouvelle PR
Navigue vers /{owner}/{repo}/pulls/new.
gitrust peut aussi afficher une bannière de suggestion au sommet de la page du dépôt juste après un push récent : « Ouvrir une pull request pour ma-branche ». Clique dessus si elle apparaît.

3. Remplir le formulaire
| Champ | Valeur recommandée |
|---|---|
| Titre | Phrase courte à l’impératif : Ajoute la validation email, Corrige le bug #42 |
| Branche source | Ta branche de travail (ma-branche) |
| Branche cible | main (ou la branche de destination souhaitée) |
| Description | Contexte, lien vers l’issue (Closes #N), captures d’écran si pertinent |
| Reviewers | Pseudo(s) des coéquipiers à notifier |
| Labels | Optionnel — pour catégoriser (voir Utiliser les labels) |
Mot-clé Closes #N : si tu écris Closes #3 dans la description ou dans un message de commit sur cette branche, gitrust fermera automatiquement l’issue #3 lors de la fusion.
Clique Ouvrir la pull request.
4. Vérifier les onglets de la PR
Une fois créée, la PR s’affiche sur /{owner}/{repo}/pulls/{num} avec plusieurs onglets :
- Conversation : fil de discussion, commentaires généraux
- Commits : liste des commits inclus dans la PR
- Fichiers modifiés : diff complet, ligne par ligne — c’est ici que les reviewers commentent
- CI : statut du pipeline CI (si configuré)

5. Suivre le statut CI
Si le dépôt a une configuration CI (.gitrust-ci.yml), gitrust exécute automatiquement le pipeline sur la branche de la PR. Le statut apparaît en bas de la PR :
- En attente / En cours : le pipeline tourne
- Réussi (vert) : tous les checks passent
- Échoué (rouge) : au moins un check a échoué — clique sur « Voir les détails » pour lire les logs
6. Répondre à une review
Quand un reviewer laisse des commentaires :
- Pousse de nouveaux commits sur la même branche pour adresser les remarques
- Réponds aux commentaires dans l’interface pour indiquer ce qui a été fait
- Le reviewer peut alors approuver la PR
Variantes
PR en mode brouillon
Si ton code n’est pas encore prêt pour review, ouvre la PR en cochant Brouillon lors de la création. Une PR en brouillon ne peut pas être fusionnée. Elle est visible par l’équipe pour discussion préalable. Passe-la en « Prête pour review » quand tu es prêt.
Mettre à jour la branche avant fusion
Si des commits ont été ajoutés sur main depuis que tu as créé ta branche, rebaser avant de fusionner :
git fetch origin
git rebase origin/main
git push origin ma-branche --force-with-lease
Voir aussi
- Tutoriel 03 — Collaborer : workflow complet issue → branche → PR → fusion
- Stratégies de fusion : choisir entre fast-forward, squash et merge commit
- Cycle de vie d’une pull request : comprendre les états et transitions
Comment choisir une stratégie de fusion
Quand utiliser ce guide
Utilise ce guide quand tu veux :
- Comprendre la différence entre fast-forward, squash et merge commit
- Choisir la stratégie adaptée à ton projet ou à ta PR
- Savoir quelles permissions sont requises pour chaque stratégie
Pré-requis
- Une pull request ouverte sur le dépôt
- Le niveau d’accès Developer minimum (pour fusionner une PR dont tu es l’auteur) ou Maintainer (pour fusionner les PRs des autres)
Les trois stratégies
Fast-forward (--ff-only)
Ce que ça fait : déplace simplement le pointeur de la branche cible vers le dernier commit de la branche source. Aucun commit de merge n’est créé. L’historique reste parfaitement linéaire.
Avant : main: A → B
feat: B → C → D
Après : main: A → B → C → D
Quand l’utiliser :
- Branches courtes avec peu de commits
- Tu veux un historique linéaire lisible avec
git log - La branche n’a pas divergé de
main(pas de commits surmainpendant que tu travaillais)
Limitation : impossible si la branche cible a avancé depuis la création de la branche source — il faut d’abord rebaser.
Squash
Ce que ça fait : regroupe tous les commits de la branche en un seul commit sur la branche cible. L’historique de la branche est condensé.
Avant : main: A → B
feat: B → C → D → E (3 commits)
Après : main: A → B → F
(F = C+D+E fusionnés en un seul commit)
Quand l’utiliser :
- Branche avec de nombreux micro-commits de type
fix typo,wip,encore un fix - Tu veux que
mainne contienne que des commits propres et signifiants - La PR correspond à une seule unité logique de travail
Limitation : les commits individuels sont perdus pour main — difficile de retrouver quel commit précis a introduit un changement avec git bisect.
Merge commit (--no-ff)
Ce que ça fait : crée un commit de merge explicite qui unit les deux branches. L’historique conserve la trace de la branche et de quand elle a été intégrée.
Avant : main: A → B
feat: B → C → D
Après : main: A → B → C → D → M
↑___↑
(M = commit de merge)
Quand l’utiliser :
- Branches longues avec historique riche qu’on veut conserver
- Tu veux pouvoir
git revertl’ensemble de la feature d’un seul coup (revert du commit M) - Les conventions de l’équipe exigent un historique non-rebasé
Limitation : l’historique du projet devient un graphe orienté (DAG) moins lisible avec git log --oneline.
Tableau de comparaison
| Critère | Fast-forward | Squash | Merge commit |
|---|---|---|---|
| Historique linéaire | ✓ | ✓ | — |
| Conserve les commits individuels | ✓ | — | ✓ |
| Commit de merge visible | — | — | ✓ |
git revert d’une feature entière | difficile | par commit | ✓ (revert M) |
git bisect précis | ✓ | difficile | ✓ |
| Idéal pour | petites branches propres | branches bruyantes | features longues |
Étapes pour fusionner dans gitrust
Sur la page de la PR (/{owner}/{repo}/pulls/{num}), fais défiler jusqu’à la section Fusion :
- Sélectionne la stratégie dans le menu déroulant (Merge commit, Squash, ou Fast-forward)
- Si fast-forward est choisi mais impossible (branche divergente), gitrust indique l’erreur — rebaser d’abord
- Clique Fusionner la pull request
- Confirme si demandé
Sortie attendue :
Pull request fusionnée avec succès.
Variantes
Fusionner via l’API
curl -X POST \
-H "Authorization: Bearer gr_pat_xxxx" \
-H "Content-Type: application/json" \
-d '{"merge_method": "squash"}' \
https://gitrust.example.com/api/v1/repos/owner/repo/pulls/1/merge
Valeurs valides pour merge_method : merge, squash, rebase (fast-forward).
Stratégie par défaut de l’équipe
Conventionner une stratégie par dépôt réduit les décisions ad hoc. Les équipes optant pour trunk-based development préfèrent le squash. Les équipes avec des branches longues (GitFlow) préfèrent le merge commit.
Voir aussi
- Ouvrir une pull request : créer la PR avant de la fusionner
- Cycle de vie d’une pull request : comprendre les transitions d’états
- Modèle de collaboration : philosophie gitrust sur les petites PRs
Comment utiliser les labels et les issues
Quand utiliser ce guide
Utilise ce guide quand tu veux :
- Créer et organiser des labels sur un dépôt
- Assigner des labels à une issue pour la catégoriser
- Filtrer les issues par label
- Fermer automatiquement une issue depuis un message de commit ou une PR
- Comprendre le système de labels à deux niveaux de gitrust
Pré-requis
- Un dépôt gitrust avec le niveau d’accès Developer minimum
Comprendre les labels à deux niveaux
gitrust organise les labels en deux niveaux : classification et sujet.
- Niveau 1 — Classification : la nature du ticket (
bug,feature,docs,chore) - Niveau 2 — Sujet : la zone fonctionnelle concernée (
auth,ci,ui,api)
Combiner les deux niveaux donne une issue catégorisée précisément : une issue avec bug + auth est un bug dans le module d’authentification. Le filtrage par combinaison de labels permet de retrouver rapidement les issues pertinentes.
Étapes
1. Accéder à la gestion des labels
Navigue vers /{owner}/{repo}/labels.
La page liste les labels existants avec leur couleur et leur description.

2. Créer un label
Sur la page des labels, remplis le formulaire en haut :
- Nom : court, en minuscules, sans espaces (ex.
bug,feature,auth,ci) - Couleur : choisis une couleur hexadécimale ou utilise le sélecteur
- Description : optionnel — explique en une phrase quand appliquer ce label
Clique Créer le label.
Labels recommandés pour démarrer :
| Label | Niveau | Couleur suggérée | Usage |
|---|---|---|---|
bug | Classification | #d73a4a (rouge) | Comportement inattendu |
feature | Classification | #0075ca (bleu) | Nouvelle fonctionnalité |
docs | Classification | #0052cc (bleu foncé) | Documentation uniquement |
chore | Classification | #e4e669 (jaune) | Maintenance, dépendances |
auth | Sujet | #7057ff (violet) | Module authentification |
ci | Sujet | #008672 (vert) | Pipeline CI |
api | Sujet | #e99695 (rose) | API REST |
3. Assigner des labels à une issue
Sur la page d’une issue (/{owner}/{repo}/issues/{num}), dans la barre latérale droite, clique sur Labels.
Sélectionne un ou plusieurs labels dans la liste déroulante. La sélection est appliquée immédiatement.
Ou depuis l’API :
curl -X POST \
-H "Authorization: Bearer gr_pat_xxxx" \
-H "Content-Type: application/json" \
-d '{"label_ids": [1, 3]}' \
https://gitrust.example.com/api/v1/repos/owner/repo/issues/5/labels
4. Filtrer les issues par label
Sur la page des issues (/{owner}/{repo}/issues), clique sur le menu Labels dans la barre de filtres. Sélectionne un ou plusieurs labels — seules les issues portant ces labels s’affichent.
Pour combiner des filtres (ex. toutes les issues bug dans la zone auth) : sélectionne les deux labels dans le même filtre.
5. Fermer une issue automatiquement
Pour fermer automatiquement une issue lors de la fusion d’une PR ou d’un push sur main, utilise un mot-clé de fermeture dans :
- Le message de commit (si poussé directement sur
main) - La description de la PR (fermeture effective à la fusion)
- Un commentaire de commit dans la branche
Mots-clés acceptés (insensibles à la casse) :
| Mot-clé | Exemple |
|---|---|
closes | closes #5 |
fixes | fixes #5 |
resolves | resolves #5 |
Exemple de message de commit :
git commit -m "corrige la validation email - fixes #5"
Après fusion de la PR contenant ce commit, l’issue #5 passe automatiquement au statut Fermée.
Variantes
Modifier ou supprimer un label
Sur /{owner}/{repo}/labels, chaque label a un bouton Modifier (pour changer le nom, la couleur ou la description) et un bouton Supprimer. La suppression retire le label de toutes les issues auxquelles il était assigné.
Labels suggérés automatiquement
gitrust peut suggérer des labels en fonction du contenu d’une issue. Cette suggestion s’affiche lors de la création d’une issue. Accepte ou ignore selon le contexte.
Voir aussi
- Ouvrir une pull request : utiliser
closes #Ndans la description de PR - Tutoriel 03 — Collaborer : ouverture d’issue dans le contexte d’un workflow complet
- Syntaxe Markdown dans les issues : formatage des descriptions d’issues
Comment importer un dépôt externe
Quand utiliser ce guide
Utilise ce guide quand tu veux :
- Migrer un dépôt depuis GitHub, GitLab ou tout autre forge Git vers gitrust
- Copier un dépôt public accessible en HTTPS sans avoir à le cloner/pousser manuellement
- Suivre la progression d’un import en cours
- Diagnostiquer un import qui échoue (timeout, dépôt trop volumineux)
Pré-requis
- Un compte gitrust actif
- L’URL HTTPS publique du dépôt source (ex.
https://github.com/owner/repo.git) - Le dépôt source accessible sans authentification (ou les credentials si privé)
Étapes
1. Ouvrir le formulaire d’import
Navigue vers /import dans ton navigateur.
Le formulaire d’import apparaît.

2. Remplir le formulaire
- URL du dépôt source : l’URL HTTPS du dépôt à importer (ex.
https://github.com/torvalds/linux.git) - Nom du dépôt cible : le nom que le dépôt aura dans gitrust (rempli automatiquement depuis l’URL, modifiable)
- Visibilité : Public ou Privé
- Credentials (si dépôt privé) : nom d’utilisateur + mot de passe ou token du service source
Clique Lancer l’import.
3. Suivre la progression
gitrust crée un job d’import asynchrone et te redirige vers /imports/{id}.
La page affiche la progression en temps réel via SSE (Server-Sent Events) :
Connexion au dépôt source…
Récupération des objets : 100% (1234/1234)
Résolution des deltas : 100% (456/456)
Import terminé avec succès.
Les statuts possibles :
| Statut | Signification |
|---|---|
| En attente | Job en file d’attente |
| En cours | Clonage en cours, logs en direct |
| Réussi | Dépôt disponible sur gitrust |
| Échoué | Voir les logs pour la cause |
| Annulé | Import annulé manuellement |
4. Accéder au dépôt importé
Une fois le statut Réussi, clique sur le lien vers le dépôt dans l’interface. Le dépôt est disponible à /{tonpseudo}/{nom-depot} avec tout l’historique Git préservé.
5. Annuler un import en cours
Si l’import prend trop longtemps ou a été lancé par erreur, clique Annuler sur la page /imports/{id}.
Variantes
Dépôt privé avec token
Pour importer un dépôt privé depuis GitHub, utilise un token GitHub dans le champ Credentials :
- Nom d’utilisateur : ton pseudo GitHub
- Mot de passe : ton Personal Access Token GitHub (pas ton mot de passe GitHub)
Le token n’est jamais stocké par gitrust après la fin de l’import.
Importer un dépôt volumineux
gitrust impose un timeout par défaut sur les imports. Pour les dépôts très volumineux (plusieurs Go), l’administrateur peut ajuster IMPORT_TIMEOUT dans la configuration de l’instance. Si l’import échoue systématiquement par timeout, cloner manuellement et pousser directement sur gitrust est une alternative :
git clone --mirror https://github.com/owner/repo.git repo.git
cd repo.git
git remote add gitrust git@gitrust.example.com:tonpseudo/repo.git
git push gitrust --mirror
Erreurs courantes
| Symptôme | Cause probable | Correction |
|---|---|---|
Connexion refusée ou timeout | URL inaccessible depuis le serveur gitrust (firewall, dépôt supprimé) | Vérifie que l’URL est accessible depuis un navigateur. Signale à l’administrateur si le serveur gitrust est derrière un proxy |
Authentification échouée | Credentials incorrects ou token expiré | Vérifie le token source. Pour GitHub, génère un nouveau PAT avec le scope repo |
Dépôt trop volumineux | Dépassement de la taille limite configurée | Contacte l’administrateur pour ajuster la limite, ou utilise l’import miroir manuel décrit ci-dessus |
| L’import reste en statut En attente longtemps | File d’attente saturée ou worker d’import non démarré | Rafraîchis la page. Si le problème persiste, contacte l’administrateur |
Voir aussi
- Tutoriel 02 — Cloner et pousser : pousser manuellement du code sur un dépôt gitrust
- Modèle de permissions : droits nécessaires pour créer un dépôt
Comment consulter le SBOM d’un dépôt
Quand utiliser ce guide
Utilise ce guide quand tu veux :
- Voir les composants logiciels détectés dans un dépôt (SBOM)
- Consulter les vulnérabilités identifiées par Dependency-Track
- Télécharger le fichier SBOM au format SPDX ou CycloneDX
- Comprendre ce qu’affiche l’onglet Security d’un dépôt
Pré-requis
- Un dépôt gitrust avec au moins un push récent
- La fonctionnalité SBOM activée par l’administrateur de l’instance (
CI_SBOM_ENABLED=true) - Le niveau d’accès Reader minimum sur le dépôt
Étapes
1. Accéder à l’onglet Security du dépôt
Navigue vers /{owner}/{repo}/security.
La page Security s’affiche avec l’encart SBOM.

2. Lire les informations du SBOM
L’encart SBOM affiche pour le dernier push analysé :
| Information | Signification |
|---|---|
| Statut | success, pending, processing, failed |
| Composants détectés | Nombre de dépendances identifiées par Syft |
| Commit analysé | SHA du dernier commit scanné |
| Hash SHA256 du BOM | Empreinte du fichier SBOM généré (traçabilité) |
| Dernière analyse | Date et heure du scan |
3. Consulter les vulnérabilités (si Dependency-Track est configuré)
Si l’administrateur a configuré Dependency-Track, l’encart affiche en plus les compteurs par sévérité :
| Sévérité | Signification |
|---|---|
| Critical | Vulnérabilité critique, exploitation active connue |
| High | Vulnérabilité haute, correctif disponible |
| Medium | Impact modéré |
| Low | Impact faible |
Un lien Voir dans Dependency-Track permet d’accéder au détail de chaque vulnérabilité (CVE, description, composant affecté, version corrigée).
4. Télécharger le fichier SBOM
Si le SBOM a été généré avec succès, un bouton Télécharger le SBOM est disponible dans l’encart. Le fichier téléchargé est au format CycloneDX JSON — le format standard utilisé par gitrust pour générer et transmettre les SBOM.
Variantes
Statut processing qui ne se résout pas
Si le statut reste processing longtemps, cela signifie que Dependency-Track n’a pas terminé son analyse dans le délai imparti (30 secondes). L’analyse continue en arrière-plan sur Dependency-Track. Pour voir les résultats définitifs, consulte directement l’interface Dependency-Track en cliquant sur le lien de l’encart.
Pas de données SBOM sur un dépôt
Deux raisons possibles :
- Aucun push depuis l’activation du SBOM : le scan ne se déclenche qu’à chaque push. Pousse un commit pour déclencher le premier scan.
- Fonctionnalité non activée : l’administrateur n’a pas activé
CI_SBOM_ENABLED. Contacte-le.
Voir aussi
- Tutoriel 04 — Automatiser avec la CI : la CI et le SBOM sont deux fonctionnalités distinctes déclenchées par le même push
- Comprendre la sécurité côté utilisateur : pourquoi surveiller les dépendances
Référence — Tableaux et spécifications
La section référence contient des informations factuelles précises destinées à la consultation. Elle ne t’apprend pas à faire quelque chose — pour ça, consulte les tutoriels ou les guides pratiques.
Mode Diátaxis : Reference. Ces pages sont conçues pour être parcourues rapidement, pas lues de bout en bout. Utilise la recherche ou les tableaux pour trouver ce que tu cherches.
Les cinq références
Modèle de permissions Matrice complète des 4 rôles dépôt (Reader / Developer / Maintainer / Owner) croisés avec toutes les actions possibles. Règles d’héritage utilisateur × équipe. À consulter quand tu ne sais pas si un rôle permet une action précise.
API REST v1
Tous les endpoints publics groupés par domaine (auth, user, repos, issues, pulls, CI). Authentification JWT vs PAT, pagination, codes d’erreur. Lien vers l’interface Swagger interactive /api/docs.
Schéma .gitrust-ci.yml
Référence complète du fichier de configuration CI en mode Easy : champs language, build, checks, tests, deploy, artifacts, env. Profils disponibles (Rust, Node, Python, Go) avec exemples complets.
Markdown et syntaxe supportée
Toute la syntaxe Markdown acceptée dans les issues, PRs et README : titres, code, tableaux, listes de tâches, mentions @, références #N, mots-clés de fermeture d’issue, emojis, Mermaid.
Notifications
Matrice des événements (PR assignée, issue mentionnée, review demandée, CI échouée…) × canaux (SSE in-app, email) × préférences via /settings/notifications. Endpoint du stream SSE.
Modèle de permissions — 4 rôles par dépôt
Cette page est une référence factuelle. Pour comprendre comment assigner ces rôles, consulte Gérer les équipes et les permissions.
Les 4 rôles de dépôt
| Rôle | Description |
|---|---|
| Reader | Lecture seule — consulte le code, les issues et les PRs |
| Developer | Contribue — pousse sur les branches non protégées, ouvre et commente des PRs |
| Maintainer | Administre partiellement — fusionne les PRs, gère les branches protégées, les labels |
| Owner | Contrôle total — paramètres du dépôt, visibilité, suppression, gestion des accès |
Matrice des actions × rôles
| Action | Reader | Developer | Maintainer | Owner |
|---|---|---|---|---|
| Lire le code (branches, commits, blobs) | ✓ | ✓ | ✓ | ✓ |
| Lire les issues et les PRs | ✓ | ✓ | ✓ | ✓ |
| Cloner via SSH ou HTTPS | ✓ | ✓ | ✓ | ✓ |
| Ouvrir une issue | — | ✓ | ✓ | ✓ |
| Commenter une issue / une PR | — | ✓ | ✓ | ✓ |
| Fermer / rouvrir une issue | — | ✓ | ✓ | ✓ |
| Pousser sur une branche non protégée | — | ✓ | ✓ | ✓ |
| Ouvrir une pull request | — | ✓ | ✓ | ✓ |
| Fusionner une pull request (propre) | — | ✓ | ✓ | ✓ |
| Fusionner la PR d’un autre contributeur | — | — | ✓ | ✓ |
Pousser sur une branche protégée (main) | — | — | ✓ | ✓ |
| Créer / supprimer des labels | — | — | ✓ | ✓ |
| Créer / supprimer des branches | — | ✓ | ✓ | ✓ |
Forcer le push (--force) | — | — | — | ✓ |
| Modifier les paramètres du dépôt | — | — | — | ✓ |
| Changer la visibilité (public/privé) | — | — | — | ✓ |
| Gérer les collaborateurs et les équipes | — | — | — | ✓ |
| Supprimer le dépôt | — | — | — | ✓ |
| Activer / désactiver la CI | — | — | — | ✓ |
| Lire les pipelines CI | ✓ | ✓ | ✓ | ✓ |
| Déclencher un pipeline manuellement | — | ✓ | ✓ | ✓ |
| Lire le SBOM (onglet Security) | ✓ | ✓ | ✓ | ✓ |
Héritage utilisateur × équipe
Un utilisateur peut avoir un accès à un dépôt par deux voies :
- Accès direct : ajouté comme collaborateur sur
/{owner}/{repo}/collaboratorsavec un niveau explicite - Via une équipe : membre d’une équipe à laquelle le dépôt a été assigné avec un niveau
Règle de résolution : le niveau d’accès effectif est le plus élevé parmi tous les accès (directs + équipes). Il n’y a pas de rôle négatif (interdit explicite) dans gitrust.
Exemple :
| Source | Niveau |
|---|---|
Équipe frontend | Developer |
Équipe leads | Maintainer |
| Accès direct | Reader |
| Effectif | Maintainer |
Rôles d’équipe vs niveaux d’accès dépôt
Ne pas confondre les deux systèmes :
| Système | Valeurs | Portée |
|---|---|---|
| Rôle dans l’équipe | Member / Maintainer / Owner | Qui peut gérer l’équipe elle-même |
| Niveau d’accès dépôt | Reader / Developer / Maintainer / Owner | Ce que les membres de l’équipe peuvent faire sur un dépôt |
Un membre avec le rôle Member dans une équipe peut avoir le niveau Owner sur un dépôt — ces deux dimensions sont indépendantes.
Voir aussi
- Gérer les équipes et les permissions : appliquer ces rôles dans l’interface
- Comprendre le modèle de collaboration : philosophie derrière les revues obligatoires
API REST v1
Référence des endpoints publics de l’API gitrust v1. Pour tester interactivement, accède à l’interface Swagger de ton instance sur /api/docs.
Authentification
L’API accepte deux méthodes d’authentification :
JWT Bearer (session web)
Obtenu via POST /api/v1/auth/login. Durée de vie courte, renouvelable via /api/v1/auth/refresh.
Authorization: Bearer <jwt_token>
Personal Access Token (PAT)
Créé dans /settings/tokens. Durée de vie configurable, révocable à tout moment.
Authorization: Bearer gr_pat_xxxx...
Les deux méthodes utilisent le même header Authorization: Bearer. gitrust détecte automatiquement le type par le préfixe du token.
Pagination
Tous les endpoints de liste supportent la pagination par curseur de page :
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
page | int | 1 | Numéro de page (commence à 1) |
limit | int | 20 | Résultats par page (max 100) |
Exemple :
GET /api/v1/repos/owner/repo/issues?page=2&limit=50
Les réponses paginées incluent les headers :
X-Total-Count: 142
X-Page: 2
X-Per-Page: 50
Codes de réponse
| Code | Signification |
|---|---|
200 OK | Succès |
201 Created | Ressource créée |
204 No Content | Succès sans corps de réponse |
400 Bad Request | Paramètres invalides |
401 Unauthorized | Token absent ou expiré |
403 Forbidden | Authentifié mais permissions insuffisantes |
404 Not Found | Ressource introuvable ou non accessible |
422 Unprocessable Entity | Validation échouée (corps JSON avec détails) |
429 Too Many Requests | Rate limit atteint |
Les erreurs retournent un corps JSON :
{
"error": "repository not found",
"code": "REPO_NOT_FOUND"
}
Endpoints — Authentification
| Méthode | Endpoint | Description |
|---|---|---|
POST | /api/v1/auth/login | Connexion email + password (+ code 2FA si activé) |
POST | /api/v1/auth/register | Créer un compte |
POST | /api/v1/auth/refresh | Renouveler le JWT |
POST | /api/v1/auth/logout | Invalider la session |
GET | /api/v1/auth/me | Infos de l’utilisateur connecté (alias de /api/v1/user) |
POST | /api/v1/auth/forgot-password | Envoyer le lien de réinitialisation |
POST | /api/v1/auth/reset-password | Changer le mot de passe via token email |
POST | /api/v1/auth/2fa/verify | Vérifier le code TOTP lors de la connexion |
Endpoints — Utilisateur
| Méthode | Endpoint | Description |
|---|---|---|
GET | /api/v1/user | Profil de l’utilisateur authentifié |
GET | /api/v1/user/repos | Dépôts accessibles par l’utilisateur authentifié |
Exemple de réponse GET /api/v1/user :
{
"id": 42,
"username": "tonpseudo",
"email": "ton@email.com",
"avatar_url": "https://gitrust.example.com/avatars/42.png",
"created_at": "2026-01-15T10:30:00Z"
}
Endpoints — Dépôts
| Méthode | Endpoint | Description |
|---|---|---|
GET | /api/v1/repos/{owner}/{repo} | Détail d’un dépôt |
GET | /api/v1/repos/{owner}/{repo}/branches | Liste des branches |
GET | /api/v1/repos/{owner}/{repo}/tags | Liste des tags |
GET | /api/v1/repos/{owner}/{repo}/commits | Liste des commits (paginée) |
Exemple de réponse GET /api/v1/repos/owner/repo :
{
"id": 7,
"owner": "owner",
"name": "repo",
"full_name": "owner/repo",
"description": "Mon dépôt",
"private": false,
"default_branch": "main",
"stars_count": 3,
"forks_count": 0,
"created_at": "2026-02-01T09:00:00Z",
"updated_at": "2026-04-15T14:22:00Z"
}
Endpoints — Issues
| Méthode | Endpoint | Description |
|---|---|---|
GET | /api/v1/repos/{owner}/{repo}/issues | Lister les issues (filtres : state, labels, page, limit) |
POST | /api/v1/repos/{owner}/{repo}/issues | Créer une issue |
GET | /api/v1/repos/{owner}/{repo}/issues/{num} | Détail d’une issue |
PATCH | /api/v1/repos/{owner}/{repo}/issues/{num} | Modifier une issue |
POST | /api/v1/repos/{owner}/{repo}/issues/{num}/comments | Ajouter un commentaire |
Exemple POST /api/v1/repos/owner/repo/issues :
{
"title": "Bug: validation email incorrecte",
"body": "La validation rejette les adresses avec un + dans le local-part."
}
Endpoints — Pull Requests
| Méthode | Endpoint | Description |
|---|---|---|
GET | /api/v1/repos/{owner}/{repo}/pulls | Lister les PRs |
POST | /api/v1/repos/{owner}/{repo}/pulls | Créer une PR |
GET | /api/v1/repos/{owner}/{repo}/pulls/{num} | Détail d’une PR |
POST | /api/v1/repos/{owner}/{repo}/pulls/{num}/merge | Fusionner une PR |
POST | /api/v1/repos/{owner}/{repo}/pulls/{num}/comments | Commenter une PR |
Exemple POST /api/v1/repos/owner/repo/pulls/{num}/merge :
{
"merge_method": "squash",
"commit_title": "feat: améliore la validation email (#5)"
}
Valeurs merge_method : merge (merge commit), squash, rebase (fast-forward).
Endpoints — CI
| Méthode | Endpoint | Description |
|---|---|---|
GET | /api/v1/repos/{owner}/{repo}/ci/pipelines | Lister les pipelines |
POST | /api/v1/repos/{owner}/{repo}/ci/pipelines/trigger | Déclencher un pipeline manuellement |
GET | /api/v1/repos/{owner}/{repo}/ci/pipelines/{id} | Détail d’un pipeline |
GET | /api/v1/repos/{owner}/{repo}/ci/pipelines/{id}/logs | Logs d’un pipeline |
GET | /api/v1/repos/{owner}/{repo}/ci/config | Lire la config CI |
PUT | /api/v1/repos/{owner}/{repo}/ci/config | Mettre à jour la config CI |
GET | /api/v1/repos/{owner}/{repo}/ci/variables | Lister les variables CI |
POST | /api/v1/repos/{owner}/{repo}/ci/variables | Créer une variable CI |
DELETE | /api/v1/repos/{owner}/{repo}/ci/variables/{id} | Supprimer une variable CI |
Documentation interactive
L’interface Swagger complète est disponible sur ton instance à /api/docs. Elle permet de tester chaque endpoint directement depuis le navigateur avec ton token d’authentification.
La spécification OpenAPI brute (YAML) est disponible sur /api/docs/openapi.yaml.
Voir aussi
- Créer un Personal Access Token : obtenir les credentials pour l’API
- Référence des notifications : événements SSE disponibles
Schéma du fichier .gitrust-ci.yml (mode Easy)
Référence complète du fichier de configuration CI pour le mode Easy de gitrust. Ce fichier doit se trouver à la racine du dépôt.
Pour une introduction pratique, consulte le tutoriel 04 — Automatiser avec la CI.
Structure générale
language: <string> # Profil de langage (requis)
build:
command: <string> # Commande de build
checks:
<nom>: <string> # Une ou plusieurs commandes de vérification
<nom>: <string>
tests:
command: <string> # Commande de test principale
deploy: # Optionnel
command: <string>
only_on: <string> # Branche déclenchant le déploiement (ex. "main")
artifacts: # Optionnel
paths:
- <chemin>
env: # Variables d'environnement pour toutes les étapes
<NOM>: <valeur>
Champ language
Type : string — Requis
Charge un profil préconfigué avec les outils et l’image Docker adaptés au langage.
| Valeur | Image Docker de base | Outils pré-installés |
|---|---|---|
rust | rust:latest | cargo, rustfmt, clippy |
node | node:lts | npm, npx |
python | python:3-slim | pip, pytest |
go | golang:latest | go, gofmt, golangci-lint |
Si ton langage n’est pas listé, utilise language: node ou language: python comme base générique et surcharge les commandes manuellement. Les profils sont définis sur le serveur de build dans deployment/ci-engine/profiles/.
Section build
Optionnel
Exécutée en premier. Arrête le pipeline en cas d’échec (code de sortie non nul).
build:
command: "cargo build --release"
Section checks
Optionnel
Dictionnaire de vérifications statiques (lint, format, sécurité). Chaque clé est un nom arbitraire affiché dans les logs. Les checks sont exécutés en parallèle si le serveur de build le supporte.
checks:
lint: "cargo clippy -- -D warnings"
format: "cargo fmt -- --check"
audit: "cargo audit"
Section tests
Optionnel
Commande de test principale. Exécutée après build et checks.
tests:
command: "cargo test --release"
Section deploy
Optionnel
Déploiement conditionnel. Exécuté uniquement sur la branche spécifiée dans only_on.
deploy:
command: "./scripts/deploy.sh production"
only_on: "main"
Si only_on est omis, le déploiement s’exécute sur toutes les branches — généralement non souhaitable.
Section artifacts
Optionnel
Chemins des fichiers ou dossiers à conserver après le pipeline. Les artefacts sont accessibles depuis la page de détail du pipeline dans l’UI.
artifacts:
paths:
- target/release/mon-binaire
- dist/
- coverage/
Section env
Optionnel
Variables d’environnement disponibles dans toutes les commandes du pipeline. Ne jamais placer de secrets ici — utilise les variables CI chiffrées via l’UI (/{owner}/{repo}/ci/variables).
env:
RUST_BACKTRACE: "1"
NODE_ENV: "test"
Les variables définies dans l’UI ont priorité sur celles du fichier YAML en cas de conflit de nom.
Exemples complets par langage
Rust
language: rust
build:
command: "cargo build --release"
checks:
lint: "cargo clippy -- -D warnings"
format: "cargo fmt -- --check"
tests:
command: "cargo test --release"
artifacts:
paths:
- target/release/mon-binaire
Node.js
language: node
build:
command: "npm ci"
checks:
lint: "npm run lint"
typecheck: "npm run typecheck"
tests:
command: "npm test"
artifacts:
paths:
- dist/
Python
language: python
build:
command: "pip install -r requirements.txt"
checks:
lint: "flake8 src/"
format: "black --check src/"
tests:
command: "pytest --tb=short"
Go
language: go
build:
command: "go build ./..."
checks:
lint: "golangci-lint run ./..."
vet: "go vet ./..."
tests:
command: "go test ./..."
Détection automatique du mode
gitrust détecte le mode à exécuter lors d’un push selon la règle suivante :
| Contenu de l’arbre du commit | Mode |
|---|---|
.gitrust-ci.yml présent | Easy — ce fichier |
.dagger/ présent | Power — module Dagger complet |
| Aucun des deux | Pas de pipeline |
Si les deux sont présents, le mode Power a la priorité.
Voir aussi
- Tutoriel 04 — Automatiser avec la CI : mise en pratique pas à pas
- Référence API REST — CI : déclencher et interroger les pipelines via l’API
Markdown et syntaxe supportée
Référence de la syntaxe Markdown acceptée dans les issues, les pull requests, les commentaires et les fichiers README des dépôts gitrust.
Titres
# Titre niveau 1
## Titre niveau 2
### Titre niveau 3
#### Titre niveau 4
Les titres génèrent des ancres de navigation (ex. #titre-niveau-2).
Mise en forme du texte
| Syntaxe | Rendu |
|---|---|
**gras** | gras |
*italique* | italique |
~~barré~~ | |
`code inline` | code inline |
Blocs de code
Délimiter par trois backticks. Spécifier le langage pour la coloration syntaxique :
```rust
fn main() {
println!("Hello, gitrust!");
}
```
Langages supportés pour la coloration : rust, go, python, javascript, typescript, json, yaml, toml, bash, sql, html, css, markdown, et la plupart des langages courants.
Listes
Liste non ordonnée
- Premier élément
- Deuxième élément
- Élément imbriqué
Liste ordonnée
1. Première étape
2. Deuxième étape
3. Troisième étape
Liste de tâches (cases à cocher)
- [x] Tâche accomplie
- [ ] Tâche en attente
- [ ] Autre tâche
Rendu : cases cliquables dans les issues et les PRs. Cocher une case met à jour le texte en temps réel.
Tableaux
| Colonne A | Colonne B | Colonne C |
|-----------|-----------|-----------|
| Valeur 1 | Valeur 2 | Valeur 3 |
| Valeur 4 | Valeur 5 | Valeur 6 |
Alignement des colonnes :
| Gauche | Centre | Droite |
|:-------|:------:|-------:|
| texte | texte | texte |
Liens et images
[Texte du lien](https://example.com)
[Lien relatif](../how-to/gerer-cles-ssh.md)

Citations
> Ceci est une citation.
> Elle peut s'étendre sur plusieurs lignes.
Ligne de séparation horizontale
---
Références aux issues et aux PRs
gitrust transforme automatiquement les références en liens cliquables :
| Syntaxe | Rendu |
|---|---|
#5 | Lien vers l’issue ou PR numéro 5 du dépôt courant |
owner/repo#5 | Lien vers l’issue 5 d’un autre dépôt |
Mentions d’utilisateurs
@tonpseudo
gitrust envoie une notification à l’utilisateur mentionné et crée un lien vers son profil.
Mots-clés de fermeture d’issue
Utilisés dans les messages de commit ou les descriptions de PR pour fermer automatiquement une issue lors de la fusion :
Closes #5
Fixes #12
Resolves #8
Insensibles à la casse. Plusieurs issues peuvent être référencées :
Closes #5, fixes #7
Emojis
La syntaxe :nom_emoji: est supportée :
| Syntaxe | Rendu |
|---|---|
:rocket: | 🚀 |
:bug: | 🐛 |
:white_check_mark: | ✅ |
:warning: | ⚠️ |
:information_source: | ℹ️ |
Labels dans les descriptions
Les labels assignés à une issue sont affichés dans la barre latérale et non directement dans le corps Markdown. Pour référencer un label dans le texte, utilise son nom entre backticks : `bug`.
Diagrammes Mermaid
gitrust supporte les blocs Mermaid dans les README et les wikis (pas dans les commentaires d’issues) :
```mermaid
graph LR
A[Début] --> B[Étape 1]
B --> C[Fin]
```
Types supportés : graph, sequenceDiagram, flowchart, stateDiagram-v2, classDiagram, gantt, pie.
Voir aussi
- Utiliser les labels et les issues : appliquer les labels et références d’issues
- Ouvrir une pull request : rédiger une description de PR efficace
Notifications
Référence des événements déclenchant des notifications, des canaux disponibles et des préférences configurables via /settings/notifications.
Canaux de notification
gitrust envoie les notifications par deux canaux :
| Canal | Description | Configuration |
|---|---|---|
| SSE in-app | Notifications en temps réel dans l’interface web, via Server-Sent Events. Icône cloche en haut à droite. | Toujours actif quand tu es connecté |
| Emails envoyés à l’adresse de ton compte | Configurable par type d’événement dans /settings/notifications |
Matrice des événements × canaux
| Événement | SSE in-app | Déclencheur | |
|---|---|---|---|
| PR assignée (tu es reviewer) | ✓ | ✓ | Quelqu’un t’assigne comme reviewer d’une PR |
Issue mentionnée (tu es mentionné par @pseudo) | ✓ | ✓ | Ton pseudo apparaît dans une issue ou un commentaire |
| Review demandée | ✓ | ✓ | L’auteur d’une PR te demande explicitement une review |
| CI échouée (sur ton push) | ✓ | ✓ | Un pipeline CI lancé par ton push se termine en échec |
| CI réussie (sur ton push) | ✓ | — | Un pipeline CI lancé par ton push se termine en succès |
| Commentaire sur ta PR | ✓ | ✓ | Quelqu’un commente une PR dont tu es l’auteur |
| Commentaire sur ton issue | ✓ | ✓ | Quelqu’un commente une issue que tu as ouverte |
| PR fusionnée (tu es auteur) | ✓ | — | Ta PR est fusionnée |
| PR fermée (tu es auteur) | ✓ | — | Ta PR est fermée sans fusion |
| Issue fermée (tu es assigné) | ✓ | — | Une issue qui t’est assignée est fermée |
| Membre ajouté à une équipe | ✓ | ✓ | Tu es ajouté à une équipe |
| Accès dépôt accordé | ✓ | — | Un dépôt t’est rendu accessible (équipe ou direct) |
✓ = activé par défaut — — = non envoyé par défaut (peut être activé dans les préférences)
Configurer les préférences
Navigue vers Paramètres → Notifications (/settings/notifications).
La page affiche un tableau de cases à cocher pour chaque type d’événement × canal. Décoche les emails que tu ne veux pas recevoir et sauvegarde.

Stream SSE en temps réel
Les notifications SSE sont disponibles sur le endpoint :
GET /notifications/stream
Connexion en text/event-stream. Chaque notification est un événement JSON :
data: {"id": 42, "type": "pr_assigned", "repo": "owner/repo", "pr_num": 7, "created_at": "2026-04-17T10:15:00Z"}
Ce stream alimente l’icône cloche de l’interface. Il peut aussi être consommé par des outils externes (scripts, bots) via un PAT.
Marquer les notifications comme lues
- Une notification :
POST /notifications/{id}/read - Toutes les notifications :
POST /notifications/read-all - Via l’interface : clique sur une notification dans la liste
/notifications, ou utilise le bouton Tout marquer comme lu
Voir aussi
- Créer un Personal Access Token : accéder au stream SSE par script
- API REST v1 : les endpoints de notifications ne sont pas listés dans l’API v1 publique — le stream SSE est le seul canal programmatique exposé
Explications — Comprendre gitrust en profondeur
Les explications sont des textes de compréhension : elles répondent à la question « pourquoi gitrust fonctionne-t-il ainsi ? » et construisent des modèles mentaux durables. Elles ne te font pas réaliser quelque chose — pour ça, consulte les tutoriels ou les guides pratiques.
Mode Diátaxis : Explanation. Les explications s’adressent à quelqu’un qui maîtrise déjà les opérations de base et veut comprendre les principes sous-jacents pour mieux décider, diagnostiquer et argumenter.
Les trois explications
Modèle de collaboration sur gitrust Philosophie des petites PRs, review obligatoire, auto-merge refusé. Analogie avec la relecture scientifique. Diagramme du cycle de travail. Comparaison avec les alternatives (trunk-based sans PR, GitFlow).
Bloom ciblé : expliquer la philosophie (2) · analyser les compromis PR courtes vs PR longues (4) · évaluer l’adéquation au contexte (5)
Sécurité côté utilisateur Pourquoi la 2FA protège même si le mot de passe est volé. Comment gitrust stocke les clés SSH (clé publique uniquement), les PAT (hash SHA-256) et les secrets TOTP. Analogie coffre-fort bancaire. Matrice vecteurs d’attaque × protections.
Bloom ciblé : expliquer le rôle de la 2FA (2) · analyser la surface d’attaque de chaque mécanisme (4) · évaluer la combinaison de protections adaptée (5)
Cycle de vie d’une pull request Diagramme Mermaid complet des états (Draft → Open → ReviewRequested → ChangesRequested → Approved → Merged / Closed) avec toutes les transitions manuelles et automatiques. Pourquoi une approbation est invalidée après un nouveau push. Comparaison review obligatoire vs optionnelle.
Bloom ciblé : identifier les états et transitions (2) · analyser les conditions de fusion (4) · évaluer les stratégies de review selon le contexte (5)
Comprendre le modèle de collaboration sur gitrust
Ce que tu vas comprendre
- Expliquer pourquoi gitrust impose une review avant toute fusion sur
main(Bloom 2) - Analyser les compromis entre petites PRs fréquentes et grosses PRs rares (Bloom 4)
- Évaluer si le modèle de review obligatoire convient à un contexte donné (Bloom 5)
Le problème concret
Tu travailles seul sur une feature pendant deux semaines. La branche diverge, s’accumule. Quand tu ouvres enfin une PR, elle contient 800 lignes de diff. Personne dans l’équipe ne veut (ni ne peut) la reviewer sérieusement — le review se réduit à « ça a l’air bien, LGTM ». Le code est fusionné avec des bugs qui auraient été détectés si quelqu’un avait lu attentivement 50 lignes.
gitrust est conçu pour rendre ce scénario difficile et le modèle opposé — petites PRs fréquentes, review courte et sincère — naturel et peu coûteux.
L’analogie
Imagine un journal scientifique avec comité de lecture. Chaque article soumis est lu par deux relecteurs indépendants avant publication. Les articles courts et bien ciblés obtiennent des retours rapides et précis. Les articles de 80 pages avec 200 références prennent six mois et reviennent avec des commentaires superficiels.
La code review dans gitrust fonctionne exactement comme le comité de lecture : plus le changement proposé est petit et focalisé, plus la review est rapide, sincère et utile. L’auto-merge (fusionner sa propre PR sans review externe) est interdit pour la même raison qu’un auteur ne peut pas valider lui-même son propre article : le biais de l’auteur rend les erreurs invisibles.
Le modèle
flowchart TD
subgraph "Philosophie gitrust"
A[Issue tracée<br/>avant tout travail]
B[Branche courte<br/>1 issue = 1 branche]
C[PR petite<br/>≤200 lignes idéalement]
D[Review sincère<br/>par au moins 1 pair]
E[CI verte<br/>avant fusion]
F[Fusion dans main<br/>historique propre]
end
A --> B --> C --> D --> E --> F
F -->|Nouvelle issue| A
Trois principes structurants :
1. Toute modification passe par une PR
gitrust n’interdit pas le push direct sur main au niveau du protocole Git, mais le modèle de collaboration encourage systématiquement la PR : c’est le seul endroit où la CI est déclenchée, où la review est documentée, où l’issue est liée et fermée automatiquement.
Les équipes qui veulent appliquer cela strictement protègent la branche main dans les paramètres du dépôt (niveau Maintainer requis pour le push direct).
2. L’auto-merge est refusé par convention
gitrust ne force pas techniquement la review externe — un Owner peut fusionner sa propre PR. Mais la convention du projet, renforcée par l’outillage (assignation de reviewers, statut « En attente de review »), crée une pression sociale pour attendre une approbation externe.
L’exception acceptable : un dépôt solo où l’on est seul contributeur. Dans ce cas, la CI joue le rôle du reviewer.
3. La taille d’une PR est un signal de qualité
| Taille du diff | Signal | Action recommandée |
|---|---|---|
| < 100 lignes | Excellent | Review en 5-10 min, commentaires précis |
| 100-300 lignes | Acceptable | Review en 30 min, décomposer si possible |
| 300-600 lignes | Attention | Décomposer en sous-tâches |
| > 600 lignes | Problème | Presque impossible à reviewer sérieusement |
Alternatives et compromis
Trunk-based development sans PR
Certaines équipes poussent directement sur main (trunk) avec feature flags pour isoler les fonctionnalités en développement. Plus rapide, moins de friction, mais nécessite une discipline de test très forte et une culture de confiance élevée. Adapté aux petites équipes seniors avec une couverture de tests élevée.
GitFlow avec longues branches de release
GitFlow maintient des branches develop, release/*, hotfix/* avec des cycles de merge longs. Adapté aux logiciels avec releases versionnées (ex. bibliothèques), inadapté aux services web déployés en continu.
Le choix de gitrust
gitrust adopte une variante de trunk-based development avec PR courtes : pas de develop séparée, pas de longues branches de feature, mais une PR obligatoire même pour les petits changements. C’est le compromis entre la vitesse du trunk-based et la traçabilité des PRs.
Vérifier ta compréhension
-
Un développeur propose une PR de 900 lignes modifiant le système d’authentification. Quelles questions lui poser pour l’aider à la découper ? (Bloom 2 — expliquer)
-
Ton équipe travaille sur un projet open-source avec des contributeurs externes que tu ne connais pas. Quels mécanismes de gitrust utiliserais-tu pour maintenir la qualité du code sans bloquer les contributions ? (Bloom 4 — analyser)
Pour aller plus loin
- Ouvrir une pull request : mettre en pratique ce modèle
- Stratégies de fusion : choisir la stratégie cohérente avec l’historique souhaité
- Cycle de vie d’une pull request : les états et transitions d’une PR de l’ouverture à la fusion
Comprendre la sécurité côté utilisateur
Ce que tu vas comprendre
- Expliquer pourquoi la 2FA protège même si ton mot de passe est volé (Bloom 2)
- Analyser les différences entre clé SSH, mot de passe et PAT en termes de surface d’attaque (Bloom 4)
- Évaluer quelle combinaison de protections adopter selon ton niveau d’exposition (Bloom 5)
Le problème concret
Ton mot de passe a été compromis. Peut-être lors d’une fuite de données d’un autre service où tu utilisais le même mot de passe. L’attaquant se connecte à ton compte gitrust, clone tous tes dépôts privés, y dépose un backdoor et efface l’historique. Tu ne t’en aperçois que trois jours plus tard.
gitrust fournit quatre mécanismes de protection indépendants pour rendre ce scénario soit impossible, soit détectable immédiatement. Ce document explique comment ils s’articulent.
L’analogie
Imagine un coffre-fort bancaire avec plusieurs couches de sécurité :
- La porte principale (mot de passe) : nécessaire mais insuffisante seule
- Le code secret temporaire (2FA TOTP) : change toutes les 30 secondes, connu uniquement de toi
- La clé physique nominative (clé SSH) : associée à une machine précise, inopérante sans la clé privée
- Le badge d’accès limité (PAT) : n’ouvre que certaines salles, révocable instantanément
Chaque couche est indépendante. Compromettre l’une n’ouvre pas les autres.
Le modèle
graph TD
subgraph "Vecteurs d'attaque"
V1[Mot de passe volé<br/>phishing / fuite DB]
V2[Machine compromise<br/>malware / vol]
V3[Token exposé<br/>commit accidentel / log]
end
subgraph "Protections gitrust"
P1[2FA TOTP<br/>code valide 30s]
P2[Clé SSH + passphrase<br/>liée à la machine]
P3[PAT à durée limitée<br/>scope minimal]
P4[Audit log<br/>connexions tracées]
end
V1 -->|bloqué par| P1
V2 -->|atténué par| P2
V3 -->|atténué par| P3
V1 & V2 & V3 -->|détectable via| P4
2FA — pourquoi le mot de passe seul ne suffit plus
Le mot de passe est un secret statique : une fois volé, il est valable indéfiniment jusqu’à ce que tu le changes. Un code TOTP (Time-based One-Time Password) est un secret dynamique : il expire en 30 secondes et ne peut être utilisé qu’une seule fois. Même si un attaquant intercepte ton code TOTP, il est déjà expiré au moment où il essaie de l’utiliser.
gitrust stocke uniquement le secret TOTP (une chaîne de 32 caractères) qui permet de générer des codes — jamais les codes eux-mêmes. Ce secret est chiffré en base.
Clés SSH — une identité par machine
Une clé SSH ed25519 est une paire mathématique (clé privée + clé publique) liée à une machine. gitrust ne stocke que la clé publique — celle-ci ne permet pas de reconstruire la clé privée. Pour s’authentifier, ta machine doit prouver qu’elle possède la clé privée en signant un challenge.
Ce que gitrust stocke : clé publique + empreinte SHA256 (pour identification) + date d’ajout.
Ce que gitrust ne stocke jamais : la clé privée (elle ne quitte jamais ta machine).
Si ta machine est volée, supprimer la clé dans /settings/keys révoque immédiatement l’accès — même si le voleur a copié les fichiers ~/.ssh/.
PAT — accès minimal et révocable
Un Personal Access Token est haché (SHA-256) dès sa création. gitrust ne stocke que le hash — si un attaquant accède à la base de données, les tokens en clair ne sont pas exposés. La valeur réelle du token (gr_pat_xxxx) n’est affichée qu’une seule fois à la création.
Le scope minimal (repo:read uniquement pour un script de lecture) limite les dégâts si un token est exposé. La date d’expiration garantit qu’un token oublié dans un vieux script ne reste pas valide indéfiniment.
Audit log — tout est tracé
Gitrust enregistre les connexions réussies et les tentatives échouées dans un journal d’audit accessible via /settings/activity. Si tu constates des connexions depuis des IP inconnues ou à des heures inhabituelles, c’est le signal d’une compromission.
Alternatives et compromis
Mot de passe fort sans 2FA
Un mot de passe long et unique (généré par un gestionnaire de mots de passe) réduit considérablement le risque de compromission par brute-force ou réutilisation. Mais il ne protège pas contre le phishing (tu es trompé pour le saisir sur un faux site) ni contre les fuites de base de données. La 2FA couvre ces deux vecteurs.
Clé SSH sans passphrase
Plus pratique (pas de saisie à chaque push), mais si ta machine est volée ou compromise par un malware, la clé privée est directement utilisable. Une passphrase chiffre la clé sur le disque — seule une combinaison vol physique + connaissance de la passphrase permet l’accès.
PAT sans expiration
Acceptable pour les outils personnels sur une machine de confiance. Inacceptable pour un token partagé dans un environnement CI, un script de serveur, ou un dépôt accessible à plusieurs personnes.
Vérifier ta compréhension
-
Un collègue dit « je n’ai pas besoin de la 2FA, mon mot de passe fait 30 caractères aléatoires ». Quels scénarios d’attaque son mot de passe fort ne couvre-t-il pas ? (Bloom 2 — expliquer)
-
Tu découvres que ton token PAT a été accidentellement commité dans un dépôt public il y a 6 heures. Quelles actions effectues-tu dans quel ordre, et pourquoi ? (Bloom 4 — analyser)
Pour aller plus loin
- Configurer la double authentification (2FA) : activer le TOTP pas à pas
- Gérer ses clés SSH : générer, déclarer et révoquer des clés
- Créer un Personal Access Token : scopes, expiration, révocation
Comprendre le cycle de vie d’une pull request
Ce que tu vas comprendre
- Identifier les états possibles d’une PR et les transitions entre eux (Bloom 2)
- Analyser pourquoi certaines transitions sont automatiques et d’autres manuelles (Bloom 4)
- Évaluer à quel moment une PR est prête à être fusionnée (Bloom 5)
Le problème concret
Tu as ouvert une PR il y a trois jours. Un reviewer a laissé des commentaires. Tu as poussé des corrections. La CI a repassé au vert. Mais tu ne sais pas si tu dois re-demander une review, si tu peux fusionner maintenant, ou si quelque chose bloque encore. L’état de la PR n’est pas clair.
Comprendre le cycle de vie d’une PR te permet de lire son état actuel d’un coup d’œil et de savoir exactement quelle action effectuer ensuite.
L’analogie
Une pull request ressemble à un dossier de demande de permis de construire :
- Ouvert : le dossier est déposé, les voisins (reviewers) peuvent faire des remarques
- En cours de review : l’architecte (reviewer) examine les plans et demande des modifications
- Approuvé : l’architecte signe les plans modifiés
- CI verte : les normes techniques sont vérifiées automatiquement (sécurité incendie, etc.)
- Fusionné : le permis est accordé, les travaux peuvent commencer sur
main - Refusé / fermé : le dossier est archivé sans suite
Comme pour un permis, une PR peut être rouverte si elle a été fermée par erreur ou prématurément.
Le modèle — diagramme de séquence complet
stateDiagram-v2
[*] --> Draft : créer en brouillon
[*] --> Open : créer directement
Draft --> Open : marquer "Prête pour review"
Open --> Draft : repasser en brouillon
Open --> ReviewRequested : assigner un reviewer
ReviewRequested --> ChangesRequested : reviewer demande modifications
ReviewRequested --> Approved : reviewer approuve
ChangesRequested --> ReviewRequested : auteur pousse corrections<br/>+ re-demande review
Approved --> Open : nouveau push (invalidation auto)
Open --> Closed : fermer sans fusionner
Closed --> Open : rouvrir
Approved --> Merged : fusionner (CI verte requise si configurée)
Open --> Merged : fusionner (si permissions suffisantes)
Merged --> [*]
Closed --> [*]
Les états en détail
Draft (Brouillon)
La PR est visible par l’équipe mais ne peut pas être fusionnée. Utilise cet état pour :
- Partager du code en cours de travail pour des commentaires précoces
- Réserver un numéro de PR pour une issue
- Travailler en collaboration sur la même branche
Transition manuelle : l’auteur clique Marquer comme prête pour review.
Open (Ouverte)
État par défaut à la création. La PR peut être commentée, reviewée et fusionnée (selon les permissions). C’est l’état de travail normal.
ReviewRequested (Review demandée)
Un reviewer a été assigné. gitrust lui envoie une notification. La PR attend son verdict. L’auteur peut continuer à pousser des commits pendant ce temps.
ChangesRequested (Modifications demandées)
Le reviewer a explicitement demandé des changements. La PR est bloquée — elle ne peut pas être fusionnée tant que le reviewer n’a pas approuvé ou n’a pas retiré son refus.
Transition de sortie : l’auteur pousse les corrections et re-demande une review (bouton Re-request review).
Approved (Approuvée)
Au moins un reviewer a approuvé. La PR peut être fusionnée si la CI est verte.
Invalidation automatique : si l’auteur pousse un nouveau commit après l’approbation, le statut repasse à Open — le reviewer doit approuver à nouveau. Ce comportement garantit que ce qui est fusionné correspond exactement à ce qui a été approuvé.
Merged (Fusionnée)
État terminal. Le code est dans la branche cible. La PR est archivée en lecture seule. Si la description contenait closes #N, l’issue N est fermée automatiquement à ce moment.
Closed (Fermée)
Fermée sans fusion — le travail a été abandonné ou la PR était incorrecte. Peut être rouverte si nécessaire. Les commits de la branche ne sont pas supprimés.
Transitions automatiques
| Déclencheur | Transition |
|---|---|
| Nouveau commit poussé après approbation | Approved → Open (invalidation de l’approbation) |
| Fusion dans la branche cible | Open/Approved → Merged |
Fusion + closes #N dans le commit ou la description | Issue N → Closed |
| CI configurée et pipeline échoué | Bouton Fusionner désactivé (blocage soft) |
Alternatives et compromis
Review obligatoire vs optionnelle
gitrust permet de fusionner une PR sans review externe si l’on a les permissions Owner ou Maintainer. Certaines équipes configurent une branche protégée exigeant au moins une approbation — ce qui rend la review techniquement obligatoire. D’autres s’appuient sur la convention sans protection forcée. La protection forcée convient aux projets critiques ou réglementés ; la convention suffit pour les petites équipes de confiance.
Invalidation automatique après push
Certaines forges (GitHub) ont rendu ce comportement optionnel. gitrust invalide systématiquement l’approbation après un nouveau push — c’est plus strict mais garantit que le reviewer a vu exactement le code fusionné. Inconvénient : un correctif de typo force une re-review complète.
Vérifier ta compréhension
-
Une PR est à l’état ChangesRequested. L’auteur pousse ses corrections mais ne re-demande pas de review. Que se passe-t-il ? Peut-on fusionner ? (Bloom 2 — identifier l’état)
-
Tu es Maintainer d’un dépôt. Une PR approuvée contient un conflit de merge introduit par un commit récent sur
main. Quelles options as-tu, et quels sont les avantages et inconvénients de chacune ? (Bloom 4 — analyser les compromis)
Pour aller plus loin
- Ouvrir une pull request : créer et suivre une PR
- Stratégies de fusion : choisir entre fast-forward, squash et merge commit
- Modèle de collaboration : pourquoi la review est centrale dans gitrust
Manuel d’administration
Ce manuel s’adresse aux opérateurs et self-hosters qui déploient, configurent et maintiennent une instance gitrust en production. Il couvre l’intégralité du cycle de vie d’une instance : premier démarrage, mise en production, sauvegarde, mise à jour et dépannage. Il suppose des connaissances en administration Linux, en réseau et en TLS — il n’explique pas ces prérequis.
Parcours d’apprentissage
Le parcours administrateur est structuré en 4 tutoriels qui représentent environ 3 heures au total. L’étayage est dégressif : le premier tutoriel fournit tout clé en main, le dernier demande de composer soi-même à partir des recettes disponibles.
graph LR
A[VM prête<br/>Docker installé] --> B[01-installation-docker<br/>30 min]
B --> C[02-installation-systemd<br/>45 min]
C --> D[03-premier-demarrage<br/>20 min]
D --> E[04-mise-en-production<br/>90 min]
E --> F[Admin opérationnel]
Checkpoint de parcours : avant de passer au tutoriel 02, vous devez avoir une instance gitrust accessible sur
http://localhost:4000avec un compte administrateur fonctionnel. Si ce n’est pas le cas, relisez le tutoriel 01.
Checkpoint de parcours : avant le tutoriel 04, vous devez avoir une instance démarrée avec une configuration SMTP et un reverse-proxy TLS opérationnel. Si ce n’est pas le cas, relisez le tutoriel 03.
Tutoriels — apprendre en faisant
Les tutoriels sont guidés pas-à-pas avec des sorties verbatim attendues. Suivez-les dans l’ordre — chacun suppose le précédent complété.
- 01 — Installation Docker : stack docker-compose complète — ~30 min
- 02 — Installation systemd : binaire natif + service — ~45 min
- 03 — Premier démarrage : admin bootstrap, migrations — ~20 min
- 04 — Mise en production — ~90 min
How-to — recettes pour les tâches courantes
Les how-to répondent à « comment faire X » sans explication de fond. Consultez-les dans n’importe quel ordre selon votre besoin.
- Sauvegarder et restaurer
- Mettre à jour l’instance
- Configurer SMTP
- Configurer OAuth (Google, GitHub)
- Configurer un CI runner distant
- Intégrer Dependency-Track (SBOM)
- Durcir avec Fail2ban
- Tuner le rate limiting
- Forcer la 2FA globalement
- Dépanner SSH
- Dépanner la CI
- Gérer les utilisateurs depuis /admin
- Auditer l’instance
Référence — informations techniques exactes
La référence documente de façon exhaustive les paramètres, schémas et comportements. Elle est destinée à la consultation ponctuelle.
- Variables d’environnement (.env)
- Paramètres dynamiques (app_settings)
- Schéma de base de données
- Ports et services (4000 HTTP, 2222 SSH, 5432 PG)
- Conformité ANSSI PA-074
Explication — comprendre le pourquoi
Les explanations construisent les modèles mentaux nécessaires pour prendre de bonnes décisions d’exploitation. Lisez-les quand vous voulez comprendre les choix d’architecture, pas seulement les appliquer.
- Architecture globale de gitrust
- Modèle de déploiement : mono-machine vs HA
- CI Dagger : pourquoi et comment
- Stratégie de sauvegarde 3-2-1 adaptée gitrust
Tutoriels — Manuel d’administration
Les tutoriels vous guident pas à pas, du premier démarrage jusqu’à une instance gitrust en production. Chaque tutoriel part d’un état précis et vous mène à un résultat observable et vérifiable.
Avant de commencer : lisez l’encart de pré-requis en tête de chaque tutoriel. Le parcours est conçu pour être suivi dans l’ordre — chaque étape suppose la précédente réalisée.
Parcours d’apprentissage
graph LR
A[VM prête<br/>Linux + Docker<br/>ou Rust toolchain] --> B[01-installation-docker<br/>~30 min]
B --> C[02-installation-systemd<br/>~45 min]
C --> D[03-premier-demarrage<br/>~20 min]
D --> E[04-mise-en-production<br/>~90 min]
E --> F[Admin opérationnel<br/>Instance sécurisée]
style A fill:#f5f5f5
style F fill:#d4edda
Scaffolding dégressif — le niveau d’aide recule d’un cran à chaque tutoriel :
| Tutoriel | Niveau | Nature de l’aide |
|---|---|---|
| 01 — Installation Docker | 1 — Copier-coller | Toutes les commandes fournies intégralement |
| 02 — Installation systemd | 1-2 — Copier-coller + explication | Commandes fournies avec contexte |
| 03 — Premier démarrage | 2 — Compléter | Quelques valeurs à adapter (domaine, mots de passe) |
| 04 — Mise en production | 3 — Combiner | Combine les recettes des how-to en autonomie |
Checkpoint de parcours : avant de passer au tutoriel suivant, vous devez savoir faire :
- Après 01 ou 02 :
curl http://localhost:4000/retourne un code HTTP ;ssh-keyscan -p 2222 localhostretourne un fingerprint - Après 03 : connexion réussie à
/adminavec le compte bootstrap - Après 04 : accès HTTPS depuis l’extérieur, SMTP configuré, première sauvegarde réalisée
Liste des tutoriels
01 — Installation avec Docker Compose
Déployer gitrust + PostgreSQL en quelques minutes avec Docker Compose. Point d’entrée recommandé pour découvrir la plateforme.
État initial : VM Linux avec Docker installé
État final : gitrust accessible sur http://localhost:4000, SSH sur :2222
02 — Installation avec systemd (binaire natif)
Installer gitrust comme un binaire natif sur Debian/Ubuntu, créer l’utilisateur système gitrust, configurer PostgreSQL, écrire le fichier gitrust.service avec durcissement systemd.
État initial : VM Debian 12 / Ubuntu 22.04 LTS
État final : systemctl status gitrust affiche active (running), ports 4000 et 2222 répondent
03 — Premier démarrage
Bootstrap du compte administrateur, vérification de la clé SSH hôte générée automatiquement, confirmation que les trois services (HTTP, SSH, PostgreSQL) sont sains, premier accès à /admin.
État initial : tutoriel 01 ou 02 complété
État final : connecté à /admin, fingerprint SSH noté, migrations confirmées dans les logs
04 — Mise en production
Reverse-proxy Nginx avec TLS, configuration SMTP, première sauvegarde, monitoring de base. Généralisation à partir d’un exemple de déploiement réseau local.
État initial : tutoriel 03 complété, domaine/DNS prêt
État final : accès HTTPS depuis l’extérieur, e-mails fonctionnels, sauvegarde planifiée
Après les tutoriels
Une fois le parcours complété, vous pouvez utiliser les guides pratiques (how-to) de manière autonome pour des tâches spécifiques : configurer OAuth, régler le rate limiting, dépanner SSH, etc.
Installation Docker : démarrez gitrust en 30 minutes avec docker-compose
Objectifs
À la fin de ce tutoriel, vous saurez :
- O1. Déployer la stack gitrust minimale (application + PostgreSQL) avec docker-compose
- O2. Vérifier que les trois services démarrent correctement et sont en bonne santé
- O3. Créer le premier compte administrateur et accéder à l’interface web
Pré-requis
- Technique : une VM ou machine Linux (Debian 12 / Ubuntu 22.04 ou supérieur), Docker Engine ≥ 24 installé (
docker --versionrépond), Docker Compose plugin installé (docker compose versionrépond), ports 4000 et 2222 libres sur la machine - Pédagogique : aucun tutoriel gitrust préalable requis — c’est le point de départ du parcours administrateur
- Temps estimé : ~30 minutes
Vue d’ensemble
Avant de lancer la moindre commande, comprenons ce que docker-compose va orchestrer et pourquoi.
gitrust est composé de trois services principaux dans sa configuration minimale. Pensez-y comme trois pièces d’un appartement qui s’appellent entre elles via un réseau interne :
graph TB
subgraph internet ["Internet / votre navigateur"]
B[Navigateur :4000]
S[Client git/SSH :2222]
end
subgraph docker ["Réseau Docker gitrust_net"]
A["gitrust-app<br/>(Rust, port 4000 + 2222)<br/>Lit la config depuis .env"]
P["postgres<br/>(PostgreSQL 16, port 5432)<br/>Stocke comptes, dépôts, issues"]
R["redis (optionnel)<br/>(Redis 7, port 6379)<br/>Sessions web + cache"]
end
B -->|HTTP| A
S -->|SSH| A
A -->|SQL| P
A -.->|sessions| R
gitrust-app est le cœur : serveur HTTP (:4000) + serveur SSH (:2222) compilé en Rust. Il lit sa configuration depuis un fichier .env et écrit les dépôts Git bare sur un volume Docker. PostgreSQL stocke toutes les données relationnelles (comptes, organisations, dépôts, issues, PRs). Redis est optionnel dans la configuration minimale — il gère les sessions web et le cache quand il est présent.
Dans ce tutoriel, nous utilisons la stack minimale (app + PostgreSQL) suffisante pour évaluer gitrust et démarrer une petite instance. La stack production avec Redis et SMTP se trouve dans template/docker/docker-compose.production.yml.
Étape 1 : Créez le répertoire de travail et le fichier de configuration
Créez un répertoire dédié à votre instance gitrust et positionnez-vous dedans :
mkdir -p /opt/gitrust && cd /opt/gitrust
Créez le fichier de configuration .env avec les valeurs minimales. Changez impérativement les valeurs marquées CHANGE_ME :
cat > .env << 'EOF'
# === gitrust — configuration minimale ===
# Changez toutes les valeurs CHANGE_ME avant de démarrer
# Clé secrète de l'application (générez avec : openssl rand -hex 32)
SECRET_KEY=CHANGE_ME_openssl_rand_hex_32
# URL publique de l'instance (sans slash final)
APP_URL=http://localhost:4000
# Base de données PostgreSQL
DATABASE_URL=postgres://gitrust:CHANGE_ME_db_password@postgres:5432/gitrust
POSTGRES_USER=gitrust
POSTGRES_PASSWORD=CHANGE_ME_db_password
POSTGRES_DB=gitrust
# Serveur SSH (port exposé à l'extérieur)
SSH_PORT=2222
# Mode de l'application
RUST_LOG=info
APP_ENV=production
EOF
Générez maintenant une clé secrète robuste et remplacez la valeur dans .env :
SECRET=$(openssl rand -hex 32)
sed -i "s/CHANGE_ME_openssl_rand_hex_32/$SECRET/" .env
Choisissez également un mot de passe pour PostgreSQL et remplacez les deux occurrences :
DB_PASS=$(openssl rand -hex 16)
sed -i "s/CHANGE_ME_db_password/$DB_PASS/g" .env
Sortie attendue (vérification) :
grep SECRET_KEY .env
# → SECRET_KEY=4a7f2c9e1b8d3f6a0e5c2b9d7f4a1e8c3b6d9f2a5e8c1b4d7f0a3e6c9b2d5f8a
Checkpoint : exécutez grep CHANGE_ME .env — la commande doit ne rien retourner. Si elle retourne des lignes, ces valeurs n’ont pas été remplacées et le démarrage échouera.
Étape 2 : Créez le fichier docker-compose.yml
Créez le fichier docker-compose.yml suivant dans /opt/gitrust/. Ce contenu correspond à la stack minimale gitrust — il est reproduit ici intégralement pour que vous puissiez démarrer sans dépendance externe :
cat > docker-compose.yml << 'EOF'
# docker-compose.minimal.yml — Stack gitrust minimale (app + PostgreSQL)
# Cas d'usage : évaluation, développement, petite instance (<10 utilisateurs)
# Pour la production : utilisez docker-compose.production.yml (+ Redis + SMTP)
#
# Usage :
# docker compose up -d
# docker compose logs -f gitrust
# docker compose down
version: "3.9"
services:
# ── Base de données ───────────────────────────────────────────────────────
postgres:
image: postgres:16-alpine
container_name: gitrust_postgres
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
# Données persistantes — ne supprimez pas ce volume sans sauvegarde
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
networks:
- gitrust_net
# ── Application gitrust ───────────────────────────────────────────────────
gitrust:
image: ghcr.io/gitrust/gitrust:latest
container_name: gitrust_app
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
env_file:
# Lit toutes les variables depuis .env — ne commitez jamais ce fichier
- .env
ports:
# Interface web — exposée sur toutes les interfaces de la machine hôte
- "4000:4000"
# Serveur SSH Git — exposé sur le port configuré dans .env
- "${SSH_PORT:-2222}:2222"
volumes:
# Dépôts Git bare — données critiques, sauvegardez ce volume
- git_repos:/var/lib/gitrust/repositories
# Clés SSH du serveur — générées au premier démarrage, persistez-les
- ssh_host_keys:/etc/gitrust/ssh
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:4000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
networks:
- gitrust_net
volumes:
# postgres_data : données relationnelles (comptes, dépôts, issues, PRs)
postgres_data:
# git_repos : dépôts Git bare (le contenu réel des commits)
git_repos:
# ssh_host_keys : clés SSH hôte (fingerprint de votre serveur)
ssh_host_keys:
networks:
gitrust_net:
driver: bridge
EOF
Checkpoint : vérifiez que les deux fichiers sont présents dans /opt/gitrust/ :
ls -la /opt/gitrust/
Sortie attendue :
total 16
drwxr-xr-x 2 root root 4096 avr 17 10:00 .
drwxr-xr-x 8 root root 4096 avr 17 09:55 ..
-rw------- 1 root root 612 avr 17 10:00 .env
-rw-r--r-- 1 root root 2048 avr 17 10:00 docker-compose.yml
Étape 3 : Démarrez la stack et suivez les logs
Démarrez tous les services en arrière-plan :
docker compose up -d
Sortie attendue :
[+] Running 4/4
✔ Network gitrust_gitrust_net Created 0.1s
✔ Volume "gitrust_postgres_data" Created 0.0s
✔ Container gitrust_postgres Started 0.8s
✔ Container gitrust_app Started 1.2s
Suivez les logs de l’application pour observer le démarrage (les migrations s’exécutent automatiquement) :
docker compose logs -f gitrust
Sortie attendue (les 15-20 premières secondes) :
gitrust_app | 2026-04-17T10:00:15Z INFO gitrust > Starting gitrust v0.9.0
gitrust_app | 2026-04-17T10:00:15Z INFO gitrust > Connecting to database...
gitrust_app | 2026-04-17T10:00:16Z INFO gitrust > Running database migrations...
gitrust_app | 2026-04-17T10:00:17Z INFO gitrust > Applied 42 migrations successfully
gitrust_app | 2026-04-17T10:00:17Z INFO gitrust > Generating SSH host keys...
gitrust_app | 2026-04-17T10:00:17Z INFO gitrust > SSH host key fingerprint: SHA256:xxxxxxxxxxxxxxxxxxxx
gitrust_app | 2026-04-17T10:00:17Z INFO gitrust > HTTP server listening on 0.0.0.0:4000
gitrust_app | 2026-04-17T10:00:17Z INFO gitrust > SSH server listening on 0.0.0.0:2222
gitrust_app | 2026-04-17T10:00:17Z INFO gitrust > gitrust is ready
Appuyez sur Ctrl+C pour quitter le suivi des logs (les containers continuent de tourner).
Checkpoint : vérifiez l’état de santé des services :
docker compose ps
Sortie attendue :
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
gitrust_app ghcr.io/gitrust/gitrust:latest "/usr/local/bin/gitr…" gitrust 2 minutes ago Up 2 minutes (healthy) 0.0.0.0:4000->4000/tcp, 0.0.0.0:2222->2222/tcp
gitrust_postgres postgres:16-alpine "docker-entrypoint.s…" postgres 2 minutes ago Up 2 minutes (healthy) 5432/tcp
Les deux containers doivent afficher (healthy). Si l’un affiche (starting), attendez 30 secondes et relancez la commande. S’il affiche (unhealthy), consultez les logs avec docker compose logs gitrust ou docker compose logs postgres.
Étape 4 : Créez le premier compte administrateur
Ouvrez votre navigateur et rendez-vous sur http://localhost:4000.
Vous devriez voir la page d’accueil gitrust avec un formulaire d’inscription. Comme l’instance est vierge, le premier compte créé devient automatiquement administrateur.
Remplissez le formulaire :
- Pseudo : votre identifiant administrateur (ex.
admin) - E-mail : votre adresse e-mail
- Mot de passe : choisissez un mot de passe robuste (≥ 12 caractères)
Cliquez sur Créer mon compte.
Sortie attendue dans les logs (visible avec docker compose logs -f gitrust) :
gitrust_app | 2026-04-17T10:05:00Z INFO gitrust::auth > New user registered: admin (id=1)
gitrust_app | 2026-04-17T10:05:00Z INFO gitrust::admin > First user promoted to administrator: admin
Checkpoint final : connectez-vous avec vos identifiants. Après la connexion, vérifiez que le menu en haut à droite affiche un lien Administration (ou naviguez vers http://localhost:4000/admin). Cette page n’est accessible qu’aux administrateurs — sa présence confirme que votre compte a bien le rôle admin.
L’URL de votre instance est http://localhost:4000 et votre compte administrateur est opérationnel.
Récapitulatif
- ✓ O1 accompli : la stack docker-compose est déployée —
docker compose psaffiche deux containers(healthy) - ✓ O2 accompli : les trois services (app, PostgreSQL, réseau interne) démarrent correctement — les logs montrent « gitrust is ready » et les migrations sont appliquées
- ✓ O3 accompli : le premier compte administrateur est créé et la page
/adminest accessible
Et si ça ne marche pas
| Symptôme | Cause probable | Correction |
|---|---|---|
Error: port 4000 is already in use au démarrage | Un autre service occupe le port 4000 sur la machine hôte | Identifiez le service avec ss -tlnp | grep 4000 ou lsof -i :4000. Arrêtez-le ou changez le port dans docker-compose.yml : remplacez "4000:4000" par "4001:4000" et accédez sur :4001 |
Le container gitrust_app affiche (unhealthy) ou redémarre en boucle | La variable DATABASE_URL est incorrecte ou SECRET_KEY contient des caractères spéciaux non échappés | Vérifiez avec docker compose logs gitrust | grep ERROR. Assurez-vous que SECRET_KEY est bien une chaîne hexadécimale sans guillemets. Rechargez avec docker compose down && docker compose up -d |
La page http://localhost:4000 affiche « Connection refused » | Le container n’est pas encore en (healthy) ou le port n’est pas exposé | Attendez 30 secondes et rechargez. Si le problème persiste, vérifiez que votre pare-feu local (ufw status) autorise le port 4000 |
FATAL: password authentication failed for user "gitrust" dans les logs postgres | Le mot de passe dans DATABASE_URL ne correspond pas à POSTGRES_PASSWORD | Supprimez le volume PostgreSQL (docker compose down -v) et relancez — attention : supprime toutes les données |
Prochaine étape
→ 02 — Installation systemd : binaire natif + service : découvrez l’installation sans Docker pour un déploiement production avec hardening AppArmor (~45 min)
Ou si vous souhaitez rendre cette instance accessible depuis Internet :
→ 04 — Mise en production : reverse-proxy TLS, SMTP, sauvegarde automatique (~90 min)
Installer gitrust comme service systemd (binaire natif)
Objectifs
À la fin de ce tutoriel, vous saurez :
- O1. Créer un utilisateur système
gitrustdédié et structurer l’arborescence/opt/gitrust - O2. Configurer PostgreSQL pour gitrust et vérifier la connexion
- O3. Écrire et activer un fichier
gitrust.serviceavec durcissement systemd - O4. Démarrer gitrust et confirmer que le serveur HTTP et SSH répondent
Pré-requis
- Technique : Debian 12 ou Ubuntu 22.04 LTS (ou dérivé) ; accès root ou sudo ;
curl,rsync,psqlinstallés ; Docker non requis (c’est précisément l’alternative binaire natif) - Pédagogique : Tutoriel 01-installation-docker lu ou notions équivalentes d’administration Linux
- Temps estimé : ~45 minutes
Vue d’ensemble
L’installation binaire natif place gitrust comme un service systemd classique, au même titre que nginx ou PostgreSQL. Il n’y a aucun conteneur : le binaire Rust s’exécute directement sous un utilisateur système dédié, avec un durcissement systemd (PrivateTmp, ProtectSystem, NoNewPrivileges).
graph LR
A[Binaire gitrust<br/>/opt/gitrust/gitrust] -->|ExecStart| B[systemd unit<br/>gitrust.service]
B -->|User=gitrust| C[Utilisateur système<br/>gitrust]
C -->|R/W| D[/opt/gitrust/data<br/>repos + clé SSH]
A -->|DATABASE_URL| E[PostgreSQL :5432]
A -->|:4000| F[Navigateur / nginx]
A -->|:2222| G[Client Git SSH]
Modèle mental : pensez à gitrust comme à nginx — un processus unique, lancé par systemd, propriétaire de ses données, sans jamais tourner en root.
Étape 1 : Créer l’utilisateur et l’arborescence
Créez un utilisateur système sans shell de login ni répertoire home personnel :
sudo useradd \
--system \
--home-dir /opt/gitrust \
--shell /usr/sbin/nologin \
--comment "Gitrust service" \
gitrust
Créez l’arborescence de travail :
sudo mkdir -p /opt/gitrust/data/repos
sudo mkdir -p /opt/gitrust/static
sudo chown -R gitrust:gitrust /opt/gitrust
sudo chmod 750 /opt/gitrust
Sortie attendue (vérification) :
$ ls -la /opt/gitrust/
total 20
drwxr-x--- 4 gitrust gitrust 4096 ...
drwxr-xr-x 3 gitrust gitrust 4096 ... data/
drwxr-xr-x 2 gitrust gitrust 4096 ... static/
Checkpoint O1 : id gitrust doit retourner une ligne avec uid=NNN(gitrust) gid=NNN(gitrust). Si la commande échoue, relisez l’étape 1.
Étape 2 : Installer PostgreSQL et créer la base gitrust
Installez PostgreSQL si ce n’est pas fait :
sudo apt-get update
sudo apt-get install -y postgresql postgresql-contrib
sudo systemctl enable --now postgresql
Créez l’utilisateur et la base de données :
sudo -u postgres psql <<'SQL'
CREATE USER gitrust WITH PASSWORD 'CHOISIR_UN_MOT_DE_PASSE_FORT';
CREATE DATABASE gitrust OWNER gitrust;
GRANT ALL PRIVILEGES ON DATABASE gitrust TO gitrust;
SQL
Vérifiez la connexion :
psql "postgres://gitrust:CHOISIR_UN_MOT_DE_PASSE_FORT@localhost:5432/gitrust" \
-c "SELECT version();"
Sortie attendue :
version
------------------------------------------------------------------------------------------------------------
PostgreSQL 15.x on x86_64-pc-linux-gnu, ...
(1 row)
Checkpoint O2 : Si psql retourne version(), la base est accessible. Conservez le mot de passe PostgreSQL pour l’étape suivante.
Étape 3 : Déployer le binaire gitrust
Sur votre machine de développement (ou sur le serveur si vous compilez localement) :
# Depuis la racine du dépôt gitrust
cargo build --release
Puis transférez vers le serveur :
# Depuis la machine de développement
rsync -avz target/release/gitrust user@VOTRE_SERVEUR:/opt/gitrust/gitrust
rsync -avz static/ user@VOTRE_SERVEUR:/opt/gitrust/static/
Sur le serveur, rendez le binaire exécutable et attribuez-le à l’utilisateur gitrust :
sudo chown gitrust:gitrust /opt/gitrust/gitrust
sudo chmod 750 /opt/gitrust/gitrust
sudo chown -R gitrust:gitrust /opt/gitrust/static/
Alternative : si vous utilisez
deployment/deploy.sh(fourni dans le dépôt source), il effectue les étapes 1 à 5 automatiquement. Ce tutoriel décompose les étapes pour que vous compreniez ce que le script fait.
Étape 4 : Créer le fichier de configuration .env
Créez /opt/gitrust/.env avec les valeurs minimales de production :
sudo -u gitrust tee /opt/gitrust/.env <<'EOF'
# === BASE DE DONNÉES ===
DATABASE_URL=postgres://gitrust:CHOISIR_UN_MOT_DE_PASSE_FORT@localhost:5432/gitrust
# === SERVEUR HTTP ===
SERVER_HOST=127.0.0.1
SERVER_PORT=4000
RUST_LOG=info
# === SERVEUR SSH ===
SSH_PORT=2222
SSH_LISTEN_ADDR=0.0.0.0
SSH_HOST_KEY_PATH=/opt/gitrust/data/ssh_host_ed25519_key
SSH_PUBLIC_HOST=VOTRE_DOMAINE_OU_IP
# === DÉPÔTS GIT ===
GIT_REPOS_BASE_PATH=/opt/gitrust/data/repos
# === FICHIERS STATIQUES ===
STATIC_FILES_PATH=/opt/gitrust/static
# === JWT (OBLIGATOIRE — générer avec : openssl rand -hex 64) ===
JWT_SECRET=REMPLACER_PAR_OPENSSL_RAND_HEX_64
JWT_EXPIRATION_MINUTES=15
JWT_ISSUER=gitrust
# === SESSIONS ===
SESSION_TIMEOUT_MINUTES=30
SESSION_BACKEND=seaorm
# === COOKIES ===
COOKIE_SECURE=true
COOKIE_SAME_SITE=Lax
APP_DEBUG=false
# === ADMIN INITIAL ===
ADMIN_USERNAME=admin
ADMIN_EMAIL=admin@VOTRE_DOMAINE
ADMIN_PASSWORD=REMPLACER_PAR_MOT_DE_PASSE_FORT
# === EMAIL (optionnel au démarrage initial, configurer ensuite) ===
# SMTP_HOST=smtp.example.com
# SMTP_PORT=587
EMAIL_BASE_URL=https://VOTRE_DOMAINE
EOF
Sécurisez le fichier (il contient des secrets) :
sudo chmod 600 /opt/gitrust/.env
sudo chown gitrust:gitrust /opt/gitrust/.env
Générez le vrai secret JWT :
openssl rand -hex 64
Copiez la sortie et remplacez REMPLACER_PAR_OPENSSL_RAND_HEX_64 dans /opt/gitrust/.env.
Checkpoint : sudo -u gitrust stat /opt/gitrust/.env doit afficher Mode: 0600 et Uid: gitrust.
Étape 5 : Créer le fichier gitrust.service
Créez le fichier d’unité systemd :
sudo tee /etc/systemd/system/gitrust.service <<'EOF'
[Unit]
Description=Gitrust - Self-hosted Git platform
Documentation=https://gitrust.eu/fr/docs/
After=network.target postgresql.service
Requires=postgresql.service
[Service]
Type=simple
User=gitrust
Group=gitrust
WorkingDirectory=/opt/gitrust
EnvironmentFile=/opt/gitrust/.env
ExecStart=/opt/gitrust/gitrust
Restart=on-failure
RestartSec=5
TimeoutStartSec=30
TimeoutStopSec=30
# --- Durcissement systemd ---
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
PrivateDevices=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictRealtime=true
RestrictSUIDSGID=true
LockPersonality=true
# Répertoires en écriture autorisés
ReadWritePaths=/opt/gitrust/data
# Si gitrust écoute sur un port < 1024 (ex: 80) :
# AmbientCapabilities=CAP_NET_BIND_SERVICE
[Install]
WantedBy=multi-user.target
EOF
Rechargez systemd et activez le service :
sudo systemctl daemon-reload
sudo systemctl enable gitrust
Sortie attendue :
Created symlink /etc/systemd/system/multi-user.target.wants/gitrust.service → /etc/systemd/system/gitrust.service.
Checkpoint O3 : systemctl is-enabled gitrust doit afficher enabled.
Étape 6 : Premier démarrage et vérification
Démarrez le service :
sudo systemctl start gitrust
Attendez 3 secondes, puis vérifiez le statut :
sudo systemctl status gitrust
Sortie attendue :
● gitrust.service - Gitrust - Self-hosted Git platform
Loaded: loaded (/etc/systemd/system/gitrust.service; enabled; vendor preset: enabled)
Active: active (running) since ... ago
Main PID: XXXX (gitrust)
Tasks: N (limit: NNNN)
Memory: NNM
CPU: NNNms
CGroup: /system.slice/gitrust.service
└─XXXX /opt/gitrust/gitrust
Vérifiez que les ports répondent :
# HTTP
curl -s -o /dev/null -w "%{http_code}" http://localhost:4000/
# Attendu : 200 ou 302
# SSH (vérification du fingerprint)
ssh-keyscan -p 2222 -H localhost 2>/dev/null | head -1
# Attendu : [localhost]:2222 ssh-ed25519 AAAA...
Consultez les logs pour les migrations automatiques :
sudo journalctl -u gitrust -n 50 --no-pager
Cherchez les lignes :
INFO gitrust_core::migrations: Running migrations...
INFO gitrust_core::migrations: Applied migration m20260325_000001_create_repositories
...
INFO gitrust_core::migrations: All migrations applied successfully
INFO rustwarden_core::services::default_admin_service: Admin user 'admin' created
Checkpoint O4 : curl http://localhost:4000/ retourne du HTML (code 200 ou 302). Le service est opérationnel.
Récapitulatif
- ✓ O1 accompli en créant l’utilisateur système
gitrustet l’arborescence/opt/gitrust - ✓ O2 accompli en configurant PostgreSQL et en vérifiant la connexion avec
psql - ✓ O3 accompli en rédigeant
gitrust.serviceavec les directives de durcissement systemd - ✓ O4 accompli en démarrant le service et en confirmant la réponse HTTP et SSH
Et si ça ne marche pas
| Symptôme | Cause probable | Correction |
|---|---|---|
Active: failed immédiatement après start | .env manquant ou mal formé ; DATABASE_URL incorrecte | journalctl -u gitrust -n 30 --no-pager pour voir l’erreur exacte ; vérifier que /opt/gitrust/.env existe et que DATABASE_URL est correct |
Permission denied dans les logs | Répertoire data/ non accessible par l’utilisateur gitrust | sudo chown -R gitrust:gitrust /opt/gitrust/data |
Address already in use sur le port 4000 | Un autre processus utilise le port | `sudo ss -tlnp |
JWT_SECRET trop court / refus de démarrage | Le secret JWT est la valeur d’exemple ou fait moins de 32 bytes | Générer avec openssl rand -hex 64 et mettre à jour .env |
Les migrations échouent (migration error) | Base de données inaccessible ou droits insuffisants | Vérifier DATABASE_URL et que l’utilisateur PG a GRANT ALL ON DATABASE gitrust |
curl: (7) Failed to connect | Le service n’est pas encore démarré ou écoute sur 127.0.0.1 uniquement | Tester depuis le serveur (pas depuis l’extérieur) ; vérifier SERVER_HOST=127.0.0.1 |
Prochaine étape
→ 03 — Premier démarrage : bootstrap admin, clé SSH hôte, vérification santé
Premier démarrage : bootstrap admin, clé SSH hôte et vérification santé
Objectifs
À la fin de ce tutoriel, vous saurez :
- O1. Expliquer comment gitrust crée automatiquement le compte administrateur initial à partir des variables
ADMIN_* - O2. Identifier et vérifier la clé SSH hôte Ed25519 générée automatiquement au premier démarrage
- O3. Confirmer que les trois services (HTTP :4000, SSH :2222, PostgreSQL :5432) sont sains via des commandes de vérification
- O4. Vous connecter à l’interface d’administration
/adminpour la première fois
Pré-requis
- Technique : gitrust installé et démarré via 01-installation-docker ou 02-installation-systemd ;
curl,ssh-keyscan,psqldisponibles - Pédagogique : Tutoriel 01 ou 02 complété — gitrust doit être
active (running) - Temps estimé : ~20 minutes
Vue d’ensemble
Le premier démarrage de gitrust effectue automatiquement trois opérations critiques, dans cet ordre :
sequenceDiagram
participant S as systemd / Docker
participant G as gitrust (binaire)
participant PG as PostgreSQL
participant FS as Système de fichiers
S->>G: ExecStart / docker run
G->>PG: Connexion DATABASE_URL
G->>PG: AppMigrator::run_migrations()
PG-->>G: Migrations OK (18 tables créées)
G->>FS: SSH_HOST_KEY_PATH absent ?
FS-->>G: Absent
G->>FS: Génère Ed25519 host key
G->>PG: ensure_default_admin() — users vide ?
PG-->>G: Vide
G->>PG: INSERT admin (ADMIN_USERNAME, ADMIN_EMAIL, hash(ADMIN_PASSWORD))
G-->>S: Listening on :4000 (HTTP) et :2222 (SSH)
Modèle mental : c’est le même principe qu’un routeur domestique — à la première mise sous tension, il crée un mot de passe admin par défaut (ici fourni par vous via .env) et ne recommence jamais s’il existe déjà des données.
Étape 1 : Vérifier les migrations automatiques
Les migrations sont appliquées à chaque démarrage, mais seulement si elles n’ont pas encore été jouées. Elles sont idempotentes.
Consultez les logs :
# Installation systemd
sudo journalctl -u gitrust --since "10 minutes ago" --no-pager | grep -E "migrat|admin|listen"
# Installation Docker
docker compose logs gitrust 2>&1 | grep -E "migrat|admin|listen"
Sortie attendue (premier démarrage) :
INFO gitrust_core::migrations: Running migrations...
INFO gitrust_core::migrations: Applied migration m20260305_000001_initial_schema
INFO gitrust_core::migrations: Applied migration m20260306_000002_create_app_settings_table
...
INFO gitrust_core::migrations: Applied migration m20260327_000013_create_pr_comments
INFO gitrust_core::migrations: All migrations applied successfully (18 applied)
INFO rustwarden_core::services::default_admin_service: Admin user 'admin' created
INFO gitrust_web: HTTP server listening on 127.0.0.1:4000
INFO gitrust_ssh: SSH server listening on 0.0.0.0:2222
Sortie attendue (démarrage suivant) :
INFO gitrust_core::migrations: Running migrations...
INFO gitrust_core::migrations: No pending migrations
Checkpoint O1 : si vous voyez Admin user 'admin' created, le bootstrap admin a fonctionné. Si vous voyez Admin user already exists, skipping, un compte admin existait déjà — c’est normal si ce n’est pas le tout premier démarrage.
Étape 2 : Vérifier la clé SSH hôte
gitrust génère une clé Ed25519 à SSH_HOST_KEY_PATH si elle n’existe pas. Cette clé identifie votre instance auprès des clients Git.
Vérifiez que la clé existe :
# Systemd
sudo -u gitrust ls -la /opt/gitrust/data/ssh_host_ed25519_key*
# Docker (le volume est monté dans /data dans le conteneur)
docker compose exec gitrust ls -la /data/ssh_host_ed25519_key*
Sortie attendue :
-rw------- 1 gitrust gitrust 399 ... /opt/gitrust/data/ssh_host_ed25519_key
-rw-r--r-- 1 gitrust gitrust 93 ... /opt/gitrust/data/ssh_host_ed25519_key.pub
Affichez le fingerprint SHA256 (à conserver précieusement — vos utilisateurs vérifieront ce fingerprint lors du premier git clone) :
# Systemd
sudo -u gitrust ssh-keygen -l -f /opt/gitrust/data/ssh_host_ed25519_key.pub
# Docker
docker compose exec gitrust ssh-keygen -l -f /data/ssh_host_ed25519_key.pub
Sortie attendue :
256 SHA256:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX gitrust-host-key (ED25519)
Important : notez ce fingerprint SHA256 dans votre documentation interne. Vos utilisateurs devront le comparer lors du premier
ssh-keyscanou lors du messageThe authenticity of host ... can't be established. Si vous régénérez cette clé après que des utilisateurs ont cloné des dépôts, leur~/.ssh/known_hostssera en conflit et tous les push/pull échoueront.
Checkpoint O2 : les deux fichiers existent avec les bonnes permissions (clé privée 600, clé publique 644).
Étape 3 : Vérifier la santé des trois services
HTTP :4000
curl -v http://localhost:4000/ 2>&1 | grep -E "^< HTTP|Location"
Sortie attendue :
< HTTP/1.1 302 Found
< location: /login
Une redirection vers /login signifie que gitrust répond et que le routeur fonctionne.
SSH :2222
ssh-keyscan -p 2222 -H localhost 2>/dev/null
Sortie attendue :
[localhost]:2222 ssh-ed25519 AAAA... (votre clé hôte)
Si aucune sortie, le serveur SSH n’écoute pas encore — consultez les logs.
PostgreSQL :5432
# Depuis l'hôte
psql "postgres://gitrust:VOTRE_MOT_DE_PASSE@localhost:5432/gitrust" \
-c "SELECT COUNT(*) AS nb_tables FROM information_schema.tables WHERE table_schema='public';"
Sortie attendue :
nb_tables
-----------
18
(1 row)
18 tables = toutes les migrations ont été appliquées.
Checkpoint O3 : les trois commandes répondent comme attendu. Si l’une échoue, consultez la section « Et si ça ne marche pas ».
Étape 4 : Premier accès à l’interface d’administration
Ouvrez votre navigateur sur http://VOTRE_SERVEUR:4000/login (ou https:// si vous avez déjà configuré un reverse-proxy TLS).
Connectez-vous avec :
- Identifiant : valeur de
ADMIN_USERNAMEdans votre.env(par défaut :admin) - Mot de passe : valeur de
ADMIN_PASSWORDdans votre.env

Après connexion, accédez au tableau de bord d’administration :
http://VOTRE_SERVEUR:4000/admin
Vous devriez voir :
- Le nombre d’utilisateurs (1 — l’admin initial)
- Le nombre de dépôts (0)
- L’état des services CI

Sécurité : changez immédiatement le mot de passe admin via
/admin/usersou le profil utilisateur si vous avez utilisé un mot de passe faible dans.env. Ensuite, vous pouvez supprimer ou commenterADMIN_PASSWORDdans.env— gitrust ne recrée le compte admin que si la tableusersest vide.
Checkpoint O4 : vous êtes connecté à /admin et le tableau de bord s’affiche sans erreur.
Récapitulatif
- ✓ O1 accompli en lisant les logs de démarrage qui confirment les migrations et la création du compte admin
- ✓ O2 accompli en vérifiant l’existence et le fingerprint SHA256 de la clé SSH hôte Ed25519
- ✓ O3 accompli en testant HTTP (:4000 → 302), SSH (:2222 → fingerprint) et PostgreSQL (:5432 → 18 tables)
- ✓ O4 accompli en se connectant à
/adminavec les identifiantsADMIN_*du.env
Et si ça ne marche pas
| Symptôme | Cause probable | Correction |
|---|---|---|
HTTP/1.1 Connection refused sur le port 4000 | gitrust n’a pas démarré ou écoute sur un autre port | systemctl status gitrust ou docker compose ps ; vérifier SERVER_PORT dans .env |
Logs : JWT_SECRET trop court ou invalid JWT secret | Le secret JWT est la valeur exemple du template | Générer avec openssl rand -hex 64 et redémarrer |
ssh-keyscan ne retourne rien | Le port SSH est bloqué par un firewall ou gitrust n’a pas démarré le serveur SSH | sudo ufw allow 2222/tcp (si ufw actif) ; vérifier SSH_PORT dans .env |
Admin user already exists mais impossible de se connecter | Le mot de passe dans .env n’est pas celui du compte en base (déjà modifié) | Réinitialiser via psql : UPDATE users SET password_hash = '...' WHERE username = 'admin' — ou utiliser la procédure de reset |
| 18 tables manquantes (moins de 18 en base) | Migrations interrompues | journalctl -u gitrust -n 100 pour voir l’erreur ; souvent un problème de droits PG ou de connexion interrompue |
Prochaine étape
→ 04 — Mise en production : reverse-proxy TLS, SMTP, sauvegarde
Ou, si vous voulez configurer les fonctionnalités avancées maintenant :
Mise en production sur une machine Linux avec HTTPS Let’s Encrypt
Contexte
Procédure de référence pour déployer une instance gitrust sur une machine Linux à IP fixe, avec patte externe routée et HTTPS via Let’s Encrypt. Reproductible et testée en conditions réelles.
Adaptez les valeurs suivantes à votre environnement :
| Placeholder | Remplacez par |
|---|---|
<your-server-ip> | L’IP fixe de votre serveur (ex: 192.168.1.57 sur un LAN, ou une IP publique) |
<your-domain> | Votre FQDN principal (ex: gitrust.example.com) |
<acme-email> | L’email pour Let’s Encrypt (ex: contact@example.com) |
<admin-user> | L’utilisateur SSH admin sur le serveur cible |
Les trois services gitrust
| # | Service | Bind interne | Exposition publique | Variable |
|---|---|---|---|---|
| 1 | Web HTTP (UI SSR + API) | 127.0.0.1:4000 | :443 HTTPS via Nginx | SERVER_PORT |
| 2 | SSH Git (russh) | 127.0.0.1:2222 | :22 via Nginx stream | SSH_PORT |
| 3 | Worker CI/CD (Dagger) | tâche Tokio in-process | — (déclenché par push) | CI_ENABLED |
PostgreSQL = dépendance, conteneur Docker bind loopback 127.0.0.1:5432.
Topologie réseau
Internet
|
v
<your-server-ip> (patte externe)
|
+-- :22 Nginx stream --> 127.0.0.1:2222 (gitrust SSH)
+-- :80 Nginx HTTP --> 301 HTTPS (sauf /.well-known/acme-challenge)
+-- :443 Nginx HTTPS --> 127.0.0.1:4000 (gitrust HTTP)
+-- :2022 sshd système (accès admin, déplacé depuis :22)
Loopback uniquement :
+-- 127.0.0.1:4000 gitrust web
+-- 127.0.0.1:2222 gitrust SSH
+-- 127.0.0.1:5432 postgres docker
Risque critique — ordre d’exécution
Le déplacement de sshd système de :22 vers :2022 doit être fait avant la configuration nginx stream, et doit être validé depuis une deuxième session SSH avant fermeture de la première. Sinon : lockout total (besoin d’accès console physique).
Pré-requis
Poste de build (machine locale avec le code source)
rustup show && command -v rsync envsubst npx
# Installer si besoin : sudo apt install rsync gettext-base
Cible <your-server-ip>
- Debian/Ubuntu récent
- Accès SSH avec
sudo - Docker + plugin compose (
docker compose version) - Snap ou apt pour
certbot+ plugin nginx - DNS
<your-domain>→ IP publique de la patte externe (vérifierdig +short <your-domain>) - Module
streamNginx disponible (paquetlibnginx-mod-streamsur Debian)
Phase 0 — Générer .env.production (poste de build, une seule fois)
JWT=$(openssl rand -hex 64)
ADMIN_PWD=$(openssl rand -base64 24)
PG_PWD=$(openssl rand -base64 24)
cat > .env.production <<EOF
# ---- Base de données ----
DATABASE_URL=postgres://gitrust:${PG_PWD}@127.0.0.1:5432/gitrust
# ---- Serveur HTTP (derrière Nginx HTTPS) ----
SERVER_HOST=127.0.0.1
SERVER_PORT=4000
# ---- Serveur SSH (derrière nginx stream :22) ----
SSH_PORT=2222
SSH_LISTEN_ADDR=127.0.0.1
SSH_PUBLIC_HOST=<your-domain>
SSH_HOST_KEY_PATH=/opt/gitrust/data/ssh_host_ed25519_key
# ---- Chemins (réécrits par deploy.sh) ----
GIT_REPOS_BASE_PATH=/opt/gitrust/data/repos
STATIC_FILES_PATH=/opt/gitrust/static
# ---- Sécurité — HTTPS actif ----
JWT_SECRET=${JWT}
JWT_EXPIRATION_MINUTES=15
REFRESH_TOKEN_EXPIRATION_DAYS=7
COOKIE_SECURE=true # HTTPS via Nginx
COOKIE_SAME_SITE=Lax
APP_DEBUG=false
RUST_LOG=info
# ---- Admin initial ----
ADMIN_USERNAME=admin
ADMIN_EMAIL=<acme-email>
ADMIN_PASSWORD=${ADMIN_PWD}
# ---- Inscription & UI ----
ALLOW_REGISTRATION=false
APP_NAME=Gitrust
DEFAULT_LOCALE=fr
# ---- Email ----
EMAIL_VALIDATION_REQUIRED=false
EMAIL_BASE_URL=https://<your-domain>
# ---- CI/CD Dagger (worker 3) ----
CI_ENABLED=true
CI_MAX_CONCURRENT=4
CI_DEFAULT_TIMEOUT=3600
CI_WORKSPACE_PATH=/tmp/gitrust-ci
CI_REMOTE_HOST=localhost
# ---- Worker import ----
IMPORT_MAX_CONCURRENT=2
IMPORT_TIMEOUT_SECS=1800
DB_WORKER_POOL_SIZE=4
EOF
chmod 600 .env.production
echo "ADMIN_PASSWORD = ${ADMIN_PWD}"
echo "POSTGRES_PASSWORD = ${PG_PWD}"
Critique : noter
ADMIN_PASSWORDetPOSTGRES_PASSWORDdans un gestionnaire de mots de passe.
Phase 1 — Préparer la cible : déplacer sshd système sur :2022
À faire en gardant deux sessions SSH simultanées.
1.pre. Diagnostic de l’état initial
1.pre.1 — Trouver le port SSH ouvert depuis le poste de build
# Depuis votre poste (sans être connecté) :
for p in 22 2022 2222 2200 22022; do
timeout 3 bash -c "echo >/dev/tcp/<your-server-ip>/$p" 2>/dev/null \
&& echo "Port $p : OUVERT" \
|| echo "Port $p : fermé/filtré"
done
1.pre.2 — Se connecter sur le port détecté
# Cas le plus probable : sshd est sur :22 par défaut
ssh <admin-user>@<your-server-ip>
1.pre.3 — Une fois connecté, vérifier l’état sshd actuel
# Sur quel(s) port(s) sshd écoute-t-il ?
sudo ss -tlnp | grep sshd
# Contenu de la config
sudo grep -riE '^[[:space:]]*Port[[:space:]]+[0-9]+' /etc/ssh/sshd_config /etc/ssh/sshd_config.d/ 2>/dev/null
# Statut du service
sudo systemctl status ssh --no-pager
1a. Session 1 : modifier la config sshd
ssh <admin-user>@<your-server-ip>
# Éditer /etc/ssh/sshd_config
sudo tee /etc/ssh/sshd_config.d/99-gitrust.conf <<'EOF'
Port 2022
EOF
# Vérifier la syntaxe
sudo sshd -t
# Si OK, reload (ne déconnecte pas la session courante)
sudo systemctl reload ssh
sudo ss -tlnp | grep -E ':22\b|:2022\b'
# Attendu : sshd écoute sur 22 ET 2022
1b. Session 2 : valider le nouveau port
Depuis le poste de build, dans un autre terminal :
ssh -p 2022 <admin-user>@<your-server-ip> 'echo "OK port 2022"'
# Doit afficher : OK port 2022
1c. (Plus tard, après Phase 5) Fermer le port 22 du sshd système
Une fois nginx stream :22 actif et testé :
ssh -p 2022 <admin-user>@<your-server-ip>
sudo sed -i 's/^Port 22$/# Port 22 (déplacé vers 2022, :22 utilisé par nginx stream)/' /etc/ssh/sshd_config
sudo systemctl restart ssh
1d. Firewall
ssh -p 2022 <admin-user>@<your-server-ip> bash <<'EOF'
sudo ufw allow 80/tcp comment 'HTTP ACME + redirect'
sudo ufw allow 443/tcp comment 'HTTPS Gitrust'
sudo ufw allow 22/tcp comment 'SSH Git via nginx stream'
sudo ufw allow 2022/tcp comment 'SSH admin système'
sudo ufw status numbered
EOF
Mettre à jour
~/.ssh/configcôté poste dev :Host gitrust-host HostName <your-server-ip> User <admin-user> Port 2022Le reste du plan utilise
ssh gitrust-host.
Phase 2 — PostgreSQL (Docker, loopback)
scp -P 2022 database/docker-compose.yml <admin-user>@<your-server-ip>:/tmp/pg-compose.yml
ssh gitrust-host bash <<EOF
sudo mkdir -p /opt/gitrust/database
sudo mv /tmp/pg-compose.yml /opt/gitrust/database/docker-compose.yml
echo 'POSTGRES_PASSWORD=${PG_PWD}' | sudo tee /opt/gitrust/database/.env >/dev/null
sudo chmod 600 /opt/gitrust/database/.env
cd /opt/gitrust/database && sudo docker compose up -d
sudo docker compose ps
EOF
Remplacer ${PG_PWD} par la valeur réelle. Vérifier que le port n’est pas exposé publiquement :
ssh gitrust-host 'sudo ss -tlnp | grep 5432'
# Attendu : 127.0.0.1:5432 — PAS 0.0.0.0
Phase 3 — Build + déploiement gitrust via deploy.sh
3a. Préparer deployment/deploy.conf
cp deployment/deploy.conf.example deployment/deploy.conf
# Éditer DEPLOY_TARGET=gitrust-host, DEPLOY_REMOTE_PATH=/opt/gitrust, DEPLOY_FQDN_INITIAL=<your-domain>
$EDITOR deployment/deploy.conf
chmod 600 deployment/deploy.conf
3b. Validation du build seul (optionnel)
BUILD_ONLY=1 ./deployment/deploy.sh
3c. Déploiement
# Sans argument : utilise DEPLOY_TARGET du deploy.conf (gitrust-host via ~/.ssh/config, port 2022)
./deployment/deploy.sh
Ce que fait deploy.sh : cargo build --release, tailwindcss --minify, adapte gitrust.service + .env, rsync, crée user gitrust, installe systemd, restart.
À la fin : gitrust écoute sur 127.0.0.1:4000 (HTTP) et 127.0.0.1:2222 (SSH).
ssh gitrust-host 'sudo systemctl status gitrust --no-pager && sudo ss -tlnp | grep -E "4000|2222"'
# Attendu : 127.0.0.1:4000 et 127.0.0.1:2222 (PAS 0.0.0.0)
Phase 4 — Nginx HTTP + Let’s Encrypt
4a. Installer nginx + certbot
ssh gitrust-host bash <<'EOF'
sudo apt update
sudo apt install -y nginx libnginx-mod-stream certbot python3-certbot-nginx
sudo systemctl enable --now nginx
EOF
4b. Déployer la conf Nginx
scp -P 2022 deployment/nginx-gitrust.conf <admin-user>@<your-server-ip>:/tmp/gitrust-nginx.conf
ssh gitrust-host bash <<EOF
sudo sed -i 's/gitrust.nuage.ebii/<your-domain>/g' /tmp/gitrust-nginx.conf
sudo mv /tmp/gitrust-nginx.conf /etc/nginx/sites-available/gitrust
sudo ln -sf /etc/nginx/sites-available/gitrust /etc/nginx/sites-enabled/gitrust
sudo rm -f /etc/nginx/sites-enabled/default
sudo mkdir -p /var/www/html/.well-known/acme-challenge
sudo nginx -t && sudo systemctl reload nginx
EOF
4c. Émettre le certificat Let’s Encrypt
ssh gitrust-host 'sudo certbot --nginx \
-d <your-domain> \
--email <acme-email> \
--agree-tos \
--no-eff-email \
--redirect'
4d. Vérifier le renouvellement automatique
ssh gitrust-host 'sudo systemctl list-timers | grep certbot'
ssh gitrust-host 'sudo certbot renew --dry-run'
4e. Test HTTPS
curl -I https://<your-domain>/
# Attendu : HTTP/2 200 (ou 302 vers /login)
# Vérifier HSTS
curl -sI https://<your-domain>/ | grep -i strict-transport
Phase 5 — Exposer SSH Git sur :22 via Nginx stream
5a. Ajouter le bloc stream
ssh gitrust-host bash <<'EOF'
sudo nginx -V 2>&1 | grep -o with-stream
sudo tee -a /etc/nginx/nginx.conf <<'NGINX'
# --- Gitrust SSH (proxy :22 -> :2222) -----------------------------------------
stream {
upstream gitrust_ssh {
server 127.0.0.1:2222;
}
server {
listen 22;
listen [::]:22;
proxy_pass gitrust_ssh;
proxy_timeout 1h;
proxy_connect_timeout 30s;
error_log /var/log/nginx/gitrust-ssh.log;
}
}
NGINX
sudo nginx -t && sudo systemctl restart nginx
sudo ss -tlnp | grep -E ':22\b|:2222\b'
# Attendu : nginx :22, gitrust :2222 (loopback)
EOF
5b. Désactiver sshd système sur :22 (Phase 1c)
ssh -p 2022 <admin-user>@<your-server-ip> bash <<'EOF'
sudo sed -i 's/^Port 22$/# Port 22 déplacé/' /etc/ssh/sshd_config 2>/dev/null || true
sudo systemctl restart ssh
sudo ss -tlnp | grep -E ':22\b|:2022\b'
# Attendu : nginx :22, sshd :2022 — plus de sshd:22
EOF
5c. Test clone Git
# Dans l'UI gitrust : Settings -> SSH keys -> coller ~/.ssh/id_ed25519.pub
# Créer un repo test admin/test-deploy via UI
git clone git@<your-domain>:admin/test-deploy.git /tmp/test-deploy
cd /tmp/test-deploy
echo "# test" > README.md
git -c user.email=<acme-email> -c user.name=admin add . && \
git -c user.email=<acme-email> -c user.name=admin commit -m "test deploy" && \
git push origin main
Phase 6 — Vérifications globales
# Service systemd
ssh gitrust-host 'sudo systemctl status gitrust --no-pager'
# Logs (recherche d'erreurs)
ssh gitrust-host 'sudo journalctl -u gitrust -n 100 --no-pager | grep -iE "error|warn|fail"'
# Ports d'écoute (vue d'ensemble)
ssh gitrust-host 'sudo ss -tlnp'
# Attendu :
# :22 nginx
# :80 nginx
# :443 nginx
# :2022 sshd système
# 127.0.0.1:2222 gitrust
# 127.0.0.1:4000 gitrust
# 127.0.0.1:5432 docker-proxy
# Renouvellement TLS automatique
ssh gitrust-host 'sudo systemctl status certbot.timer --no-pager'
# Test E2E
curl -I https://<your-domain>/login
ssh -T git@<your-domain> || true # Banner SSH russh attendu
Phase 7 — Durcissement : Fail2ban
Voir administration_manual/how-to/durcir-avec-fail2ban.md pour la configuration complète.
7a. Installation
ssh gitrust-host bash <<'EOF'
sudo apt install -y fail2ban
sudo systemctl enable --now fail2ban
EOF
7b. Configuration minimale rapide
ssh gitrust-host sudo tee /etc/fail2ban/jail.local <<'EOF'
[DEFAULT]
backend = systemd
bantime = 1h
findtime = 10m
maxretry = 5
ignoreip = 127.0.0.1/8 ::1
banaction = ufw
banaction_allports = ufw
[sshd]
enabled = true
port = 2022
filter = sshd
backend = %(sshd_backend)s
logpath = %(sshd_log)s
maxretry = 3
[nginx-http-auth]
enabled = true
port = http,https
filter = nginx-http-auth
logpath = /var/log/nginx/gitrust.error.log
[gitrust-login]
enabled = true
port = http,https
filter = gitrust-login
logpath = /var/log/nginx/gitrust.access.log
maxretry = 5
[gitrust-ssh]
enabled = true
port = 22,2222
filter = gitrust-ssh
backend = systemd
journalmatch = _SYSTEMD_UNIT=gitrust.service
maxretry = 5
[recidive]
enabled = true
logpath = /var/log/fail2ban.log
banaction = %(banaction_allports)s
bantime = 1w
findtime = 1d
maxretry = 3
EOF
Créer les filtres gitrust-login et gitrust-ssh, puis :
ssh gitrust-host 'sudo systemctl restart fail2ban && sudo fail2ban-client status'
Phase 8 — Mise à jour vers un nouveau domaine (quand DNS prêt)
8a. Ajouter le SAN au certificat existant
ssh gitrust-host 'sudo certbot --nginx --expand \
-d <your-domain> \
-d <new-domain> \
--email <acme-email> \
--agree-tos --no-eff-email'
8b. Mettre à jour la conf Nginx
ssh gitrust-host bash <<EOF
sudo sed -i 's/server_name <your-domain>;/server_name <your-domain> <new-domain>;/g' /etc/nginx/sites-available/gitrust
sudo nginx -t && sudo systemctl reload nginx
EOF
8c. Mettre à jour .env.production et redéployer
sed -i 's|SSH_PUBLIC_HOST=.*|SSH_PUBLIC_HOST=<new-domain>|' .env.production
sed -i 's|EMAIL_BASE_URL=.*|EMAIL_BASE_URL=https://<new-domain>|' .env.production
./deployment/deploy.sh gitrust-host
Phase 9 — Mises à jour ultérieures
git pull
SKIP_ENV=1 SKIP_DB=1 ./deployment/deploy.sh
Dépannage
| Symptôme | Diagnostic | Fix |
|---|---|---|
| Lockout SSH après reload sshd | — | Console physique / KVM IPMI |
Cert Let’s Encrypt échoue (Connection refused) | dig +short <your-domain> ≠ IP publique | Corriger DNS, ou vérifier que :80 traverse bien le NAT |
Cert échoue (unauthorized) | Nginx :80 ne répond pas sur /.well-known/acme-challenge/ | curl http://<your-domain>/.well-known/acme-challenge/test doit ne pas rediriger en 301 |
Login boucle sur /login malgré HTTPS | grep COOKIE_SECURE /opt/gitrust/.env | Doit être true ; vérifier que Nginx forward bien X-Forwarded-Proto https |
Push SSH Connection closed | journalctl -u gitrust | grep ssh | Vérifier que gitrust écoute bien 127.0.0.1:2222 et que nginx stream forward |
nginx -t : unknown directive "stream" | Module non chargé | sudo apt install libnginx-mod-stream puis systemctl restart nginx |
| Mixed content HTTPS | Templates qui hardcodent http:// | Vérifier EMAIL_BASE_URL=https://... et headers X-Forwarded-Proto |
| Push gros repo timeout | Logs nginx client intended to send too large body | Augmenter client_max_body_size 2G (déjà fait pour .git/) |
Récapitulatif des ports
| Port | Process | Exposition | Rôle |
|---|---|---|---|
| 22 | nginx (stream) | Public | SSH Git → forward vers :2222 |
| 80 | nginx | Public | ACME challenge + redirect HTTPS |
| 443 | nginx | Public | HTTPS → forward vers :4000 |
| 2022 | sshd système | Public | Admin SSH (à restreindre par firewall si possible) |
| 2222 | gitrust | Loopback | Backend SSH russh |
| 4000 | gitrust | Loopback | Backend HTTP axum |
| 5432 | docker-proxy | Loopback | PostgreSQL |
Guides pratiques — Manuel d’administration
Les guides pratiques répondent à une question précise : « Comment faire X ? » Chaque guide est une recette orientée tâche, utilisable indépendamment, sans contexte pédagogique. Vous êtes supposé avoir déjà une instance gitrust opérationnelle.
Prérequis : parcours tutoriel complété (tutoriels 01-03 minimum). Si vous démarrez, commencez par les tutoriels.
Exploitation quotidienne
| Guide | Ce qu’il vous permet de faire |
|---|---|
| Gérer les utilisateurs | Créer, désactiver, supprimer des comptes ; forcer reset mot de passe ; gérer les droits admin |
| Auditer l’instance | Consulter /admin/audit, filtrer par utilisateur/action/période, exporter en CSV, corréler avec journalctl |
Configuration des services
| Guide | Ce qu’il vous permet de faire |
|---|---|
| Configurer SMTP | Activer l’envoi d’e-mails ; recettes pour Gmail, OVH, Postfix local, Mailpit |
| Configurer OAuth | Activer Google, GitHub, Discord ou Microsoft comme fournisseur de connexion |
| Régler le rate limiting | Ajuster les limites par profil (équipe 5 pers., 50 pers., instance publique) ; surveiller les 429 |
Maintenance et sécurité
| Guide | Ce qu’il vous permet de faire |
|---|---|
| Sauvegarder et restaurer | Script backup.sh complet ; restauration pas-à-pas de PostgreSQL, des dépôts bare et de la clé SSH hôte |
| Mettre à jour gitrust | Checklist upgrade : sauvegarde → arrêt → déploiement → migrations auto → smoke test → rollback si besoin |
| Durcir avec Fail2ban | Bannir les IPs après tentatives de brute force sur SSH et HTTP |
| Forcer le 2FA globalement | Activer totp_force_enabled pour obliger tous les utilisateurs à configurer le TOTP |
| Intégrer Dependency-Track | Connecter gitrust à une instance Dependency-Track pour le suivi CVE via SBOM |
Dépannage
| Guide | Ce qu’il vous permet de faire |
|---|---|
| Dépanner SSH | Diagnostiquer les erreurs Permission denied, Connection refused, REMOTE HOST IDENTIFICATION HAS CHANGED, timeouts |
| Dépanner la CI | Débloquer un pipeline bloqué, diagnostiquer Dagger, résoudre les erreurs SBOM et Dependency-Track |
Runners CI distants
| Guide | Ce qu’il vous permet de faire |
|---|---|
| Configurer un runner CI distant | Déporter l’exécution CI sur une machine dédiée via SSH + rsync |
Trouver rapidement la bonne page
- Vous cherchez la valeur d’une variable d’environnement → Référence des variables
- Vous voulez comprendre pourquoi quelque chose fonctionne ainsi → Explanations
- Vous commencez depuis zéro → Tutoriels
Sauvegarder et restaurer une instance gitrust
À qui s’adresse cette page
Administrateurs qui souhaitent mettre en place une sauvegarde fiable et tester la restauration. Pour comprendre la stratégie 3-2-1 adaptée à gitrust, consultez Stratégie de sauvegarde.
Ce qu’il faut sauvegarder
Gitrust repose sur trois sources de vérité distinctes :
| Source | Chemin / objet | Critique | Fréquence recommandée |
|---|---|---|---|
| Base de données PostgreSQL | database gitrust | Oui | Quotidien minimum |
| Dépôts Git bare | GIT_REPOS_BASE_PATH (ex. /opt/gitrust/data/repos) | Oui | Quotidien minimum |
| Clé SSH hôte | SSH_HOST_KEY_PATH (ex. /opt/gitrust/data/ssh_host_ed25519_key) | Oui | Une seule fois, puis conserver |
Fichier .env | /opt/gitrust/.env | Oui | À chaque modification |
La clé SSH hôte est immuable après le premier démarrage. Si vous la perdez, tous vos utilisateurs devront supprimer leur entrée
known_hostset re-vérifier le fingerprint.
Sauvegarder
Sauvegarde PostgreSQL (pg_dump)
BACKUP_DIR="/var/backups/gitrust"
DATE=$(date +%Y%m%d-%H%M%S)
mkdir -p "$BACKUP_DIR"
pg_dump \
-h localhost \
-U gitrust \
--format=custom \
--file="${BACKUP_DIR}/pg-${DATE}.dump" \
gitrust
echo "Dump : ${BACKUP_DIR}/pg-${DATE}.dump"
# Purger les sauvegardes de plus de 30 jours
find "$BACKUP_DIR" -name "pg-*.dump" -mtime +30 -delete
Le format --format=custom est compressé et restaurable avec pg_restore. Le fichier contient la structure ET les données.
Sauvegarde des dépôts bare (rsync)
REPOS_SRC="/opt/gitrust/data/repos"
REPOS_DST="/var/backups/gitrust/repos"
rsync \
--archive \
--delete \
--hard-links \
--checksum \
"${REPOS_SRC}/" "${REPOS_DST}/"
Les dépôts bare Git sont consistants en lecture même pendant un git push actif : Git garantit l’atomicité au niveau du pack.
Sauvegarde de la clé SSH hôte et du .env
BACKUP_DIR="/var/backups/gitrust"
DATE=$(date +%Y%m%d-%H%M%S)
tar czf "${BACKUP_DIR}/config-${DATE}.tar.gz" \
/opt/gitrust/data/ssh_host_ed25519_key \
/opt/gitrust/data/ssh_host_ed25519_key.pub \
/opt/gitrust/.env
chmod 600 "${BACKUP_DIR}/config-${DATE}.tar.gz"
Script complet backup.sh
#!/usr/bin/env bash
# /opt/gitrust/scripts/backup.sh
set -euo pipefail
BACKUP_DIR="${BACKUP_DIR:-/var/backups/gitrust}"
DATE=$(date +%Y%m%d-%H%M%S)
PG_USER="gitrust"
PG_DB="gitrust"
REPOS_SRC="/opt/gitrust/data/repos"
SSH_KEY="/opt/gitrust/data/ssh_host_ed25519_key"
ENV_FILE="/opt/gitrust/.env"
mkdir -p "$BACKUP_DIR"
echo "[1/3] Dump PostgreSQL..."
pg_dump -U "$PG_USER" --format=custom \
--file="${BACKUP_DIR}/pg-${DATE}.dump" "$PG_DB"
echo "[2/3] Rsync dépôts bare..."
mkdir -p "${BACKUP_DIR}/repos"
rsync --archive --delete --checksum \
"${REPOS_SRC}/" "${BACKUP_DIR}/repos/"
echo "[3/3] Archive config sensible..."
tar czf "${BACKUP_DIR}/config-${DATE}.tar.gz" \
"$SSH_KEY" "${SSH_KEY}.pub" "$ENV_FILE"
chmod 600 "${BACKUP_DIR}/config-${DATE}.tar.gz"
echo "Sauvegarde terminée dans $BACKUP_DIR"
find "$BACKUP_DIR" -name "pg-*.dump" -mtime +30 -delete
find "$BACKUP_DIR" -name "config-*.tar.gz" -mtime +90 -delete
Planifier via cron :
# Sauvegarder chaque nuit à 2h00
0 2 * * * /opt/gitrust/scripts/backup.sh >> /var/log/gitrust-backup.log 2>&1
Restaurer
Avant toute restauration
# 1. Arrêter gitrust
sudo systemctl stop gitrust # ou : docker compose stop gitrust
# 2. Vérifier les fichiers de sauvegarde disponibles
ls -lh /var/backups/gitrust/pg-*.dump | tail -5
ls -lh /var/backups/gitrust/config-*.tar.gz | tail -3
du -sh /var/backups/gitrust/repos/
Restaurer la base PostgreSQL
DUMP="/var/backups/gitrust/pg-20260417-020000.dump"
sudo -u postgres psql -c "DROP DATABASE IF EXISTS gitrust;"
sudo -u postgres psql -c "CREATE DATABASE gitrust OWNER gitrust;"
pg_restore \
-h localhost \
-U gitrust \
--dbname=gitrust \
--verbose \
"$DUMP"
Vérification :
psql "postgres://gitrust:MOT_DE_PASSE@localhost:5432/gitrust" \
-c "SELECT COUNT(*) FROM users; SELECT COUNT(*) FROM repositories;"
Restaurer les dépôts bare
REPOS_SRC="/var/backups/gitrust/repos"
REPOS_DST="/opt/gitrust/data/repos"
rsync --archive --delete "${REPOS_SRC}/" "${REPOS_DST}/"
sudo chown -R gitrust:gitrust "${REPOS_DST}"
Restaurer la clé SSH hôte et le .env
ARCHIVE="/var/backups/gitrust/config-20260417-020000.tar.gz"
sudo tar xzf "$ARCHIVE" -C /
sudo chown gitrust:gitrust /opt/gitrust/data/ssh_host_ed25519_key*
sudo chmod 600 /opt/gitrust/data/ssh_host_ed25519_key
sudo chmod 644 /opt/gitrust/data/ssh_host_ed25519_key.pub
sudo chmod 600 /opt/gitrust/.env
Redémarrer et vérifier
sudo systemctl start gitrust
sleep 3
curl -s -o /dev/null -w "%{http_code}" http://localhost:4000/
# Attendu : 302
Tester la restauration (drill trimestriel)
Une sauvegarde non testée n’est pas une sauvegarde. Testez sur une VM distincte :
# Sur une VM de test vierge — restaurer, puis vérifier :
git clone ssh://git@VM_TEST:2222/admin/un-depot-connu.git /tmp/drill-test
ls /tmp/drill-test/
# Les fichiers doivent correspondre à l'état au moment de la sauvegarde
Pour aller plus loin
- Stratégie de sauvegarde — comprendre le 3-2-1 adapté à gitrust
- Variables d’environnement :
GIT_REPOS_BASE_PATH,SSH_HOST_KEY_PATH
Mettre à jour gitrust
À qui s’adresse cette page
Administrateurs qui souhaitent appliquer une nouvelle version de gitrust sur une instance en production, en limitant les risques.
Principe général
Les migrations de base de données sont appliquées automatiquement au démarrage par AppMigrator. Vous n’avez jamais à exécuter de commande SQL manuellement. Le binaire contient toujours les migrations cumulatives.
flowchart LR
A[Sauvegarde] --> B[Arrêt service]
B --> C[Déploiement<br/>nouveau binaire]
C --> D[Démarrage]
D --> E{Migrations<br/>auto ?}
E -->|OK| F[Smoke test]
E -->|Erreur| G[Rollback]
F -->|OK| H[Terminé]
F -->|Erreur| G
Checklist de mise à jour
1. Sauvegarder avant tout
# Sauvegarde complète avant toute modification
/opt/gitrust/scripts/backup.sh
Vérifiez que le dump PostgreSQL a été créé :
ls -lh /var/backups/gitrust/pg-$(date +%Y%m%d)*.dump
Ne passez à l’étape 2 que si la sauvegarde est confirmée.
2. Arrêter le service
# Systemd
sudo systemctl stop gitrust
# Docker Compose
docker compose stop gitrust
Attendez la confirmation :
sudo systemctl is-active gitrust
# Attendu : inactive
3. Déployer le nouveau binaire
Option A — via deploy.sh (recommandé si vous compilez localement) :
# Sur la machine de développement, depuis la racine du dépôt gitrust
./deployment/deploy.sh user@VOTRE_SERVEUR /opt/gitrust
Le script compile en release, synchronise le binaire, les assets statiques et le service systemd, puis redémarre automatiquement.
Option B — manuellement :
# Compiler le nouveau binaire
cargo build --release
# Compiler le CSS si les templates ont changé
npx tailwindcss -i static/css/input.css -o static/css/style.css --minify
# Transférer
rsync -avz target/release/gitrust user@VOTRE_SERVEUR:/opt/gitrust/gitrust
rsync -avz static/ user@VOTRE_SERVEUR:/opt/gitrust/static/
# Sur le serveur
sudo chown gitrust:gitrust /opt/gitrust/gitrust
sudo chmod 750 /opt/gitrust/gitrust
4. Vérifier les éventuels changements de .env
Consultez le CHANGELOG de la nouvelle version pour identifier les nouvelles variables d’environnement. Ajoutez-les à /opt/gitrust/.env si nécessaire, avec des valeurs appropriées.
# Comparer votre .env avec .env.example de la nouvelle version
diff /opt/gitrust/.env .env.example | grep "^>"
5. Démarrer le service
# Systemd
sudo systemctl start gitrust
# Docker Compose
docker compose up -d gitrust
6. Vérifier les migrations automatiques
sudo journalctl -u gitrust --since "2 minutes ago" --no-pager | grep -E "migrat|error|panic"
Sortie attendue si de nouvelles migrations ont été appliquées :
INFO gitrust_core::migrations: Applied migration m20260501_000014_add_webhooks
INFO gitrust_core::migrations: All migrations applied successfully
Sortie attendue si aucune nouvelle migration :
INFO gitrust_core::migrations: No pending migrations
Si vous voyez ERROR ou panic, consultez la section Rollback.
7. Smoke test
# HTTP
curl -s -o /dev/null -w "%{http_code}" http://localhost:4000/
# Attendu : 302
# SSH (fingerprint inchangé — comparer avec la valeur notée)
ssh-keyscan -p 2222 -H localhost 2>/dev/null | ssh-keygen -l -f -
Connectez-vous à l’interface /admin et vérifiez :
- La liste des utilisateurs est correcte
- Un dépôt existant est accessible
- La page
/admin/settingss’affiche sans erreur
Rollback
Si le démarrage échoue ou si le smoke test révèle une régression :
1. Arrêter le service
sudo systemctl stop gitrust
2. Restaurer le binaire précédent
Si vous avez conservé l’ancien binaire (bonne pratique : le renommer avant la mise à jour) :
sudo cp /opt/gitrust/gitrust.backup /opt/gitrust/gitrust
sudo chown gitrust:gitrust /opt/gitrust/gitrust
3. Restaurer la base de données si des migrations ont été appliquées
# Identifier la dernière sauvegarde pré-mise-à-jour
DUMP=$(ls -t /var/backups/gitrust/pg-*.dump | head -1)
echo "Restauration depuis : $DUMP"
sudo -u postgres psql -c "DROP DATABASE IF EXISTS gitrust;"
sudo -u postgres psql -c "CREATE DATABASE gitrust OWNER gitrust;"
pg_restore -h localhost -U gitrust --dbname=gitrust "$DUMP"
4. Redémarrer avec l’ancienne version
sudo systemctl start gitrust
Attention : si des nouvelles migrations ont créé ou modifié des tables, le rollback de la base est indispensable avant de démarrer l’ancienne version. Un binaire ancien sur une base migratée peut produire des erreurs imprévisibles.
Conseils
- Conservez toujours le binaire précédent pendant 24 h avant de le supprimer (
mv gitrust gitrust.backup). - Planifiez les mises à jour en dehors des heures de pointe (le service est arrêté pendant l’opération).
- Les migrations sont idempotentes : redémarrer deux fois de suite avec la même version est sans risque.
Pour aller plus loin
Configurer l’envoi d’e-mails (SMTP)
À qui s’adresse cette page
Administrateurs qui souhaitent activer l’envoi d’e-mails (validation de compte, réinitialisation de mot de passe, notifications). Sans configuration SMTP, gitrust démarre normalement mais toutes les fonctionnalités e-mail sont désactivées.
Variables concernées
Toutes ces variables se placent dans /opt/gitrust/.env (ou dans l’EnvironmentFile de votre service systemd / docker-compose.yml). Un redémarrage du service est nécessaire après modification.
| Variable | Obligatoire | Défaut | Description |
|---|---|---|---|
SMTP_HOST | Oui | — | Nom d’hôte ou IP du serveur SMTP |
SMTP_PORT | Non | 587 | Port SMTP (25, 465, 587, 1025) |
SMTP_USER | Non | — | Identifiant SMTP (souvent = adresse e-mail) |
SMTP_PASSWORD | Non | — | Mot de passe applicatif SMTP |
SMTP_FROM | Non | — | Adresse expéditeur visible (doit être valide) |
SMTP_FROM_NAME | Non | — | Nom affiché dans le champ « De : » |
SMTP_USE_TLS | Non | true | Activer TLS natif (port 465) |
SMTP_USE_STARTTLS | Non | true | Activer STARTTLS (port 587) |
SMTP_ACCEPT_INVALID_CERTS | Non | false | Accepter les certificats invalides (dev seulement) |
SMTP_CA_CERT_PATH | Non | — | Chemin vers un CA personnalisé (PEM) |
EMAIL_BASE_URL | Oui (prod) | http://localhost:3000 | URL de base pour les liens dans les e-mails |
EMAIL_VALIDATION_REQUIRED | Non | true | Exiger la validation e-mail à l’inscription |
EMAIL_QUEUE_BATCH_SIZE | Non | 10 | Nombre d’e-mails traités par tick |
EMAIL_QUEUE_RETRY_ATTEMPTS | Non | 5 | Nombre de tentatives avant abandon |
EMAIL_QUEUE_RETRY_DELAY_SECONDS | Non | 300 | Délai entre deux tentatives (secondes) |
Important : Ne jamais activer
SMTP_ACCEPT_INVALID_CERTS=trueen production — cela ouvre la voie à des attaques MITM sur tous les e-mails sortants.
Recettes par prestataire
Gmail (compte personnel ou Google Workspace)
Google exige un mot de passe applicatif (pas votre mot de passe Google). Générez-le sur myaccount.google.com/apppasswords en activant au préalable la validation en deux étapes.
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=votre.adresse@gmail.com
SMTP_PASSWORD=xxxx_xxxx_xxxx_xxxx # mot de passe applicatif (16 caractères)
SMTP_FROM=votre.adresse@gmail.com
SMTP_FROM_NAME=Gitrust
SMTP_USE_TLS=false
SMTP_USE_STARTTLS=true
EMAIL_BASE_URL=https://VOTRE_DOMAINE
Limite : Gmail impose un quota de 500 e-mails/jour pour les comptes personnels. Pour les volumes importants, utilisez un service dédié (SendGrid, Brevo, etc.).
OVH / Infomaniak / Gandi
Ces hébergeurs utilisent STARTTLS sur le port 587 ou SMTPS sur le port 465.
# OVH (SSL/TLS natif sur 465)
SMTP_HOST=ssl0.ovh.net
SMTP_PORT=465
SMTP_USER=noreply@VOTRE_DOMAINE.com
SMTP_PASSWORD=MOT_DE_PASSE_EMAIL
SMTP_FROM=noreply@VOTRE_DOMAINE.com
SMTP_FROM_NAME=Gitrust
SMTP_USE_TLS=true
SMTP_USE_STARTTLS=false
EMAIL_BASE_URL=https://VOTRE_DOMAINE
# Gandi (STARTTLS sur 587)
SMTP_HOST=mail.gandi.net
SMTP_PORT=587
SMTP_USER=noreply@VOTRE_DOMAINE.com
SMTP_PASSWORD=MOT_DE_PASSE_EMAIL
SMTP_FROM=noreply@VOTRE_DOMAINE.com
SMTP_FROM_NAME=Gitrust
SMTP_USE_TLS=false
SMTP_USE_STARTTLS=true
EMAIL_BASE_URL=https://VOTRE_DOMAINE
Postfix local (instance interne sans authentification)
Pour les déploiements sur réseau interne où Postfix est installé sur le même serveur :
SMTP_HOST=127.0.0.1
SMTP_PORT=25
# Pas de SMTP_USER ni SMTP_PASSWORD si Postfix accepte sans auth sur loopback
SMTP_FROM=noreply@VOTRE_DOMAINE.com
SMTP_FROM_NAME=Gitrust
SMTP_USE_TLS=false
SMTP_USE_STARTTLS=false
EMAIL_BASE_URL=https://VOTRE_DOMAINE
Vérifiez que Postfix accepte les connexions depuis 127.0.0.1 :
# Dans /etc/postfix/main.cf
mynetworks = 127.0.0.0/8
Mailpit / Mailhog (développement local)
SMTP_HOST=localhost
SMTP_PORT=1025
SMTP_FROM=dev@localhost
SMTP_FROM_NAME=Gitrust-Dev
SMTP_USE_TLS=false
SMTP_USE_STARTTLS=false
SMTP_ACCEPT_INVALID_CERTS=false
EMAIL_BASE_URL=http://localhost:4000
EMAIL_VALIDATION_REQUIRED=false
Démarrez Mailpit : docker run -p 1025:1025 -p 8025:8025 axllent/mailpit
Interface web : http://localhost:8025
Tester la configuration
Après avoir modifié le .env et redémarré gitrust, testez via le panel admin :
- Connectez-vous à
/admin - Accédez à
/admin/email - Saisissez une adresse de test et cliquez Envoyer un e-mail de test

En cas d’erreur, consultez les logs :
sudo journalctl -u gitrust -n 50 --no-pager | grep -i "smtp\|email\|mail"
Erreurs courantes :
| Message dans les logs | Cause | Correction |
|---|---|---|
Connection refused sur le port SMTP | SMTP_HOST ou SMTP_PORT incorrect | Vérifier les paramètres du prestataire |
Authentication failed | SMTP_USER ou SMTP_PASSWORD incorrect | Pour Gmail : utiliser un mot de passe applicatif, pas le mot de passe du compte |
Certificate verification failed | Certificat TLS du serveur SMTP invalide ou CA non reconnu | Spécifier SMTP_CA_CERT_PATH ou contacter le prestataire |
EMAIL_BASE_URL pointe vers localhost | Liens dans les e-mails non cliquables pour les destinataires externes | Définir EMAIL_BASE_URL=https://VOTRE_DOMAINE |
Pour aller plus loin
- Variables d’environnement — section Email
- Auditer l’instance — vérifier les bounces et la file d’envoi
Configurer l’authentification OAuth (Google, GitHub, Discord, Microsoft)
À qui s’adresse cette page
Administrateurs qui souhaitent permettre à leurs utilisateurs de se connecter via un fournisseur OAuth externe plutôt que (ou en plus de) le couple identifiant/mot de passe local.
Principe
OAuth est le seul sous-système hybride de gitrust : la configuration peut venir soit de la base de données (via /admin/settings), soit du fichier .env en fallback. La priorité est toujours à la base de données.
flowchart LR
A[OAuthConfig::load au démarrage] --> B{Valeur en DB ?}
B -->|Oui| C[Utiliser valeur DB]
B -->|Non| D[Fallback .env]
D --> E[Valeur codée en dur si absent]
C --> F[OAuthConfig en mémoire]
E --> F
F --> G[Arc — non rechargé à chaud]
Conséquence : toute modification de la configuration OAuth (via l’UI ou via
.env) nécessite un redémarrage pour être prise en compte.
Méthode recommandée : via l’interface /admin/settings
C’est la méthode la plus simple. Les secrets sont chiffrés en base (AES-256-GCM).
- Connectez-vous à
/admin/settings - Activez le toggle OAuth / SSO activé
- Renseignez l’URL de base des callbacks (ex.
https://gitrust.votre-domaine.com) - Pour chaque fournisseur souhaité, activez le toggle et renseignez
Client IDetClient Secret - Cliquez Enregistrer
- Redémarrez le service :
sudo systemctl restart gitrust

Méthode alternative : via les variables .env
Utile pour le provisionnement automatisé (Ansible, cloud-init) ou pour définir les valeurs au premier démarrage.
# Activation globale
OAUTH_ENABLED=true
OAUTH_REDIRECT_BASE_URL=https://gitrust.votre-domaine.com
# Clé de chiffrement des secrets OAuth en base (AES-256-GCM)
# Générer avec : openssl rand -hex 32
OAUTH_ENCRYPTION_KEY=REMPLACER_PAR_32_BYTES_HEX
Ces variables ne s’appliquent qu’en l’absence de valeur en base de données.
Configurer Google OAuth
1. Créer les identifiants dans Google Cloud Console
- Ouvrez console.cloud.google.com → APIs & Services → Credentials
- Cliquez Create Credentials → OAuth 2.0 Client ID
- Type : Web application
- Ajoutez l’URI de redirection autorisée :
https://gitrust.votre-domaine.com/api/v1/auth/oauth/google/callback - Notez le Client ID et le Client Secret
2. Configurer dans gitrust
Via .env :
OAUTH_GOOGLE_ENABLED=true
OAUTH_GOOGLE_CLIENT_ID=123456789-xxxxxxxxxxxx.apps.googleusercontent.com
OAUTH_GOOGLE_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxxxxxxxxxx
Via /admin/settings : activez « Google » et renseignez les mêmes valeurs.
Scopes utilisés : openid, email, profile (read-only, pas d’accès au compte Google).
Configurer GitHub OAuth
1. Créer l’application OAuth sur GitHub
- Ouvrez github.com/settings/developers → OAuth Apps → New OAuth App
- Authorization callback URL :
https://gitrust.votre-domaine.com/api/v1/auth/oauth/github/callback - Notez le Client ID et générez un Client Secret
2. Configurer dans gitrust
OAUTH_GITHUB_ENABLED=true
OAUTH_GITHUB_CLIENT_ID=Iv1.xxxxxxxxxxxxxxxx
OAUTH_GITHUB_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Scopes utilisés : read:user, user:email.
Configurer Discord OAuth
1. Créer l’application sur Discord Developer Portal
- Ouvrez discord.com/developers/applications → New Application
- Onglet OAuth2 → Redirects → ajoutez :
https://gitrust.votre-domaine.com/api/v1/auth/oauth/discord/callback - Notez le Client ID et le Client Secret
2. Configurer dans gitrust
OAUTH_DISCORD_ENABLED=true
OAUTH_DISCORD_CLIENT_ID=123456789012345678
OAUTH_DISCORD_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Configurer Microsoft / Azure AD
1. Créer l’application dans Azure
- Ouvrez portal.azure.com → Azure Active Directory → App registrations → New registration
- Redirect URI (Web) :
https://gitrust.votre-domaine.com/api/v1/auth/oauth/microsoft/callback - Notez le Application (client) ID et créez un Client Secret dans Certificates & Secrets
2. Configurer dans gitrust
OAUTH_MICROSOFT_ENABLED=true
OAUTH_MICROSOFT_CLIENT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
OAUTH_MICROSOFT_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# "common" = comptes personnels et professionnels
# Remplacer par le GUID du tenant pour restreindre à votre organisation
OAUTH_MICROSOFT_TENANT=common
Comportement lors de la connexion OAuth
| Scénario | Comportement par défaut |
|---|---|
| Premier login OAuth, adresse e-mail inconnue | Compte créé automatiquement (oauth_auto_register=true) |
| Premier login OAuth, adresse e-mail déjà en base | Compte lié au compte existant (oauth_link_existing_account=true) |
| Login OAuth, compte désactivé par l’admin | Connexion refusée |
Ces comportements sont configurables dans /admin/settings (paramètres oauth_auto_register et oauth_link_existing_account).
Vérification après activation
# Redémarrer le service
sudo systemctl restart gitrust
# Vérifier les logs de démarrage
sudo journalctl -u gitrust -n 30 --no-pager | grep -i oauth
Ouvrez une fenêtre de navigation privée et accédez à /login : les boutons OAuth activés doivent apparaître sous le formulaire classique.
Pour aller plus loin
Configurer le runner CI distant
Guide d’usage du script qui provisionne une machine distante pour exécuter les pipelines CI/CD de gitrust via SSH + rsync + Dagger.
1. Rôle du script
deployment/setup-remote-ci.sh (disponible dans le dépôt source gitrust) prépare un serveur de build distant utilisé par le worker CI de gitrust. Il automatise 6 étapes :
- Vérifie la connectivité SSH vers le runner
- Installe Docker (
curl https://get.docker.com | sh) s’il est absent - Installe Dagger CLI (
curl https://dl.dagger.io/dagger/install.sh | sh) s’il est absent - Crée le répertoire de travail distant (
CI_REMOTE_PATH) - Synchronise le module
deployment/ci-engine/(mode CI Easy) viarsync -az --delete - Smoke test : affiche
docker --versionetdagger version
Le script est idempotent : rejoue sans dommage, skip ce qui existe déjà.
2. Quand l’utiliser
| Scénario | Besoin de setup-remote-ci.sh ? |
|---|---|
Dev local, CI sur la même machine que gitrust (CI_REMOTE_HOST=localhost) | Non — Docker et Dagger déjà installés localement, le ci-engine/ est lu depuis deployment/ci-engine/ directement |
| Prod on-premise, un seul serveur (gitrust + CI colocalisés) | Non — idem, un simple curl get.docker.com | sh suffit sur le serveur gitrust |
| Prod avec runner CI dédié (machine séparée) | OUI — c’est le cas cible de ce script |
| Prod avec plusieurs instances gitrust partageant un runner | OUI — le runner est provisionné une fois, chaque instance pointe dessus |
| CI dans Kubernetes / Nomad / runner managé cloud | Non — ce script est pensé pour une VM Debian/Ubuntu classique |
3. Architecture cible
+---------------------+ SSH + rsync +-----------------------+
| Gitrust (web+SSH) |---------------------------| CI Runner (distant) |
| <your-server-ip> | CI_REMOTE_HOST/USER/KEY | <ci-runner-ip> |
| | | |
| Worker CI Tokio | push .gitrust-ci.yml | /opt/gitrust-ci/ |
| (dans gitrust.bin) | + code source checkout | ├── ci-engine/ |
| | | └── workspaces/ |
| | dagger call ... | |
| | | Docker + Dagger CLI |
+---------------------+ +-----------------------+
Le worker CI de gitrust (tâche Tokio dans le binaire principal) ne fait pas tourner Docker localement — il délègue au runner via SSH. Cela permet d’isoler les workloads CPU/RAM gourmands (builds Rust, Node, Docker) du process web de gitrust.
4. Pré-requis
Sur la machine d’exécution (là où on lance le script)
| Outil | Rôle |
|---|---|
bash ≥ 4 | Interpréteur |
ssh, rsync | Transport et synchro |
ssh-agent chargé ou CI_REMOTE_SSH_KEY défini | Auth SSH sans mot de passe interactif (BatchMode=yes) |
Fichier .env avec les variables CI_REMOTE_* | Config (voir section 5) |
Sur le runner distant
| Pré-requis | Pourquoi |
|---|---|
| OS Linux récent (Debian 12+, Ubuntu 22.04+) | Docker install script compatible |
User avec sudo passwordless ou droits docker | get.docker.com fait sudo en interne |
Clé SSH publique du user local dans ~/.ssh/authorized_keys | Auth SSH non-interactive |
Réseau sortant autorisé vers get.docker.com et dl.dagger.io | Installation des binaires |
Au minimum ~5 Go libres dans CI_REMOTE_PATH | Images Docker + workspaces |
5. Variables d’environnement attendues (le .env)
Le script source un fichier .env dont le chemin est passé en argument, ou par défaut ../.env (relatif au script, donc <racine_projet>/.env).
Tableau des variables
| Variable | Obligatoire | Défaut | Description |
|---|---|---|---|
CI_REMOTE_HOST | OUI | — | Hostname ou IP du runner (ex: ci-runner.internal) |
CI_REMOTE_USER | non | $(whoami) (user courant) | Compte SSH sur le runner |
CI_REMOTE_SSH_PORT | non | 22 | Port SSH du runner |
CI_REMOTE_PATH | non | /opt/gitrust-ci | Répertoire de travail distant (créé par le script) |
CI_REMOTE_SSH_KEY | non | — (ssh-agent) | Chemin d’une clé privée SSH spécifique |
Ces variables sont les mêmes que celles lues par gitrust au runtime. Centraliser dans .env évite les divergences entre le provisionnement et le runtime.
Exemple minimal de .env (runner dédié)
# --- CI runner distant ---
CI_REMOTE_HOST=<ci-runner-ip>
CI_REMOTE_USER=ci-runner
CI_REMOTE_SSH_PORT=22
CI_REMOTE_PATH=/opt/gitrust-ci
CI_REMOTE_SSH_KEY=/home/gitrust/.ssh/ci_runner_ed25519
Exemple .env complet coexistant avec config gitrust
DATABASE_URL=postgres://...
JWT_SECRET=...
# ... (voir .env.example pour le reste)
# --- CI/CD (lu par gitrust ET par setup-remote-ci.sh) ---
CI_ENABLED=true
CI_MAX_CONCURRENT=4
CI_REMOTE_HOST=ci-runner.internal
CI_REMOTE_USER=ci-runner
CI_REMOTE_PATH=/opt/gitrust-ci
CI_REMOTE_SSH_KEY=/opt/gitrust/data/ci_runner_key
Cas « runner local » (pas besoin du script)
CI_REMOTE_HOST=localhost
# Les autres CI_REMOTE_* sont ignorés
Dans ce cas, installer Docker et Dagger directement sur la machine gitrust, copier deployment/ci-engine/ vers CI_ENGINE_PATH (défaut /opt/gitrust/ci-engine) et passer à la suite. Ne pas exécuter setup-remote-ci.sh avec CI_REMOTE_HOST=localhost.
6. Usage
Depuis la racine du projet (par défaut)
cd /chemin/vers/gitrust
# .env a la racine (défaut) :
./deployment/setup-remote-ci.sh
Avec un .env explicite
./deployment/setup-remote-ci.sh /chemin/vers/mon.env
# Utile si vous avez .env.production, .env.staging, etc.
./deployment/setup-remote-ci.sh .env.production
Sortie attendue
==> Configuration :
Serveur : ci-runner@<ci-runner-ip>
Port SSH : 22
Chemin : /opt/gitrust-ci
==> [1/6] Vérification de la connectivité SSH...
SSH OK
OK
==> [2/6] Vérification de Docker...
Docker déjà installé
==> [3/6] Vérification de Dagger CLI...
Installation de Dagger CLI...
Dagger installé
==> [4/6] Création du répertoire distant...
/opt/gitrust-ci créé
==> [5/6] Synchronisation du ci-engine...
sending incremental file list
ci-engine/
ci-engine/profiles/
...
ci-engine synchronisé vers /opt/gitrust-ci/ci-engine/
==> [6/6] Smoke test...
Versions distantes :
Docker version 28.0.1, build abcd123
dagger v0.19.2 (linux/amd64)
==> Setup terminé. Le serveur ci-runner@<ci-runner-ip> est prêt pour l'exécution CI.
Pensez à configurer CI_EXECUTION_MODE=remote dans votre .env
7. Ce qui se retrouve sur le runner
Après exécution réussie :
/opt/gitrust-ci/ <- CI_REMOTE_PATH
└── ci-engine/ <- synchronisé depuis deployment/ci-engine/
├── README.md
└── profiles/ <- templates par stack (Python, Node, Rust, ...)
Plus, installés globalement :
/usr/bin/docker(ou équivalent) + socket/var/run/docker.sock/usr/local/bin/dagger
Les workspaces de pipelines (checkouts temporaires) sont créés par le worker CI de gitrust au moment de l’exécution sous CI_WORKSPACE_PATH (défaut /tmp/gitrust-ci) — pas par ce script.
8. Vérifications post-setup
Depuis la machine gitrust
# Source le .env
set -a; source .env.production; set +a
# 1. SSH direct (doit passer sans prompt)
ssh -p ${CI_REMOTE_SSH_PORT:-22} \
${CI_REMOTE_SSH_KEY:+-i $CI_REMOTE_SSH_KEY} \
$CI_REMOTE_USER@$CI_REMOTE_HOST 'docker info && dagger version'
# 2. Rsync round-trip (lecture/écriture sur CI_REMOTE_PATH)
echo "test" | ssh $CI_REMOTE_USER@$CI_REMOTE_HOST \
"cat > $CI_REMOTE_PATH/.gitrust-test && cat $CI_REMOTE_PATH/.gitrust-test && rm $CI_REMOTE_PATH/.gitrust-test"
# Attendu : "test"
Pipeline de test via gitrust
Pousser un dépôt avec un .gitrust-ci.yml minimal :
# .gitrust-ci.yml
version: 1
pipeline:
- name: smoke
image: alpine:3
run: echo "CI runner OK"
Puis dans l’UI gitrust : onglet Pipelines → la pipeline doit passer au statut success.
9. Mise à jour après modification du ci-engine/
Le ci-engine/ synchronisé n’est pas « live-linké » : rejouer le script après modification côté source.
# Modifier deployment/ci-engine/profiles/*.py ou similaire
./deployment/setup-remote-ci.sh
# Les étapes 2-3 sont skip (déjà installés), seule l'étape 5 refait le rsync
rsync -az --delete supprime les fichiers côté runner qui n’existent plus localement — garantit la cohérence.
10. Dépannage
| Symptôme | Cause probable | Fix |
|---|---|---|
ERREUR: fichier .env introuvable | .env absent ou chemin incorrect | cp .env.example .env && $EDITOR .env ou passer le chemin : ./setup-remote-ci.sh /path/to/.env |
ERREUR: CI_REMOTE_HOST non défini | Variable commentée ou absente | Décommenter CI_REMOTE_HOST=... dans le .env |
ERREUR: impossible de se connecter | SSH bloqué, mauvais user, clé non autorisée | Tester manuellement : ssh -v -p X user@host ; vérifier ~/.ssh/authorized_keys sur le runner |
Permission denied (publickey) | BatchMode=yes interdit les prompts, clé non chargée | ssh-add ~/.ssh/ci_runner_ed25519 ou définir CI_REMOTE_SSH_KEY=/path/to/key |
sudo: a password is required pendant l’install Docker | User sans NOPASSWD sudo | Ajouter le user au sudoers : ci-runner ALL=(ALL) NOPASSWD:ALL (runner uniquement) |
curl: (7) Failed to connect to get.docker.com | Réseau sortant du runner bloqué | Whitelist get.docker.com et dl.dagger.io, ou pré-installer Docker + Dagger manuellement |
ATTENTION: ... ci-engine introuvable | Lancé hors du repo gitrust | cd dans la racine du projet avant de lancer |
Pipeline reste queued indéfiniment | Worker CI ne trouve pas le runner | Vérifier CI_EXECUTION_MODE=remote dans .env gitrust + logs : journalctl -u gitrust | grep -i 'ci|dagger' |
dagger: command not found au smoke test | $PATH du user SSH ne contient pas /usr/local/bin | echo 'export PATH=$PATH:/usr/local/bin' >> ~/.bashrc sur le runner, ou CI_DAGGER_BIN=/usr/local/bin/dagger côté gitrust |
11. Sécurité
- Le runner CI exécute du code arbitraire venant des dépôts hébergés. Ne jamais le colocaliser avec des secrets sensibles (PG prod, clés de prod).
- Isoler réseau : bloquer l’accès sortant du runner vers le LAN privé (seul l’accès Internet pour
docker pullest nécessaire). - Limiter
sudo NOPASSWDau strict minimum sur le runner (idéalement : juste pour les commandesdockeretapt). - Rotation régulière de la clé SSH
CI_REMOTE_SSH_KEY. La révoquer dans~/.ssh/authorized_keyscôté runner en cas de suspicion. - Le runner ne doit pas pouvoir se connecter en SSH à la machine gitrust (unidirectionnel).
Intégrer la CI et Dependency-Track
Ce document explique comment activer et configurer les deux fonctionnalités de qualité/sécurité de gitrust :
- La CI intégrée (Dagger) qui exécute les builds/tests/lints à chaque push.
- Le dependency tracker (Syft + Dependency-Track) qui scanne les composants du code à chaque push et détecte les vulnérabilités connues.
Ces deux systèmes sont indépendants : on peut activer l’un sans l’autre.
1. Architecture générale
flowchart TB
subgraph Client
Dev[Développeur]
end
subgraph Gitrust[Instance Gitrust]
HTTP[:4000 HTTP/Git]
SSH[:2222 SSH/Git]
Worker[CiWorker<br/>tokio task]
Sbom[SbomService<br/>tokio spawn]
DB[(PostgreSQL)]
end
subgraph Builder[Serveur de build]
Docker[Docker/Podman]
Dagger[Dagger CLI]
CiEngine[/opt/gitrust-ci/ci-engine/]
end
subgraph Security[Stack sécurité optionnelle]
Syft[syft]
Dtrack[Dependency-Track]
end
Dev -->|git push| HTTP
Dev -->|git push| SSH
HTTP --> Worker
SSH --> Worker
HTTP --> Sbom
SSH --> Sbom
Worker -->|SSH + rsync| Builder
Builder -->|logs streamés| Worker
Worker --> DB
Sbom -->|scan workspace| Syft
Sbom -->|PUT BOM| Dtrack
Dtrack -->|findings| Sbom
Sbom --> DB
Trois composants fonctionnent en parallèle :
- CiWorker : tâche Tokio qui consomme des
CiTaskdepuis un channelmpsc, limitée par unSemaphoreàCI_MAX_CONCURRENTpipelines simultanés. - SbomService : exécuté dans un
tokio::spawnaprès chaque push, totalement découplé du CI. - Serveur de build : machine distante (ou
localhost) qui exécute Docker + Dagger. Gitrust y envoie le workspace par rsync et lance Dagger par SSH.
2. Mettre en place la CI
2.1 Pré-requis serveur de build
Le serveur de build peut être la même machine que Gitrust (CI_REMOTE_HOST=localhost) ou une machine dédiée. Il doit disposer de :
| Outil | Rôle |
|---|---|
| Docker ou Podman | Containers Dagger |
| Dagger CLI | Moteur d’exécution |
| SSH (accès sortant depuis Gitrust) | Transport |
| rsync | Copie du workspace |
| git, tar | Extraction du tree du commit |
Installation automatique recommandée :
# Depuis la machine Gitrust (l'utilisateur doit pouvoir SSH vers le builder)
./deployment/setup-remote-ci.sh .env
Le script exécute :
- Vérification de la connectivité SSH
- Installation de Docker si absent
- Installation de Dagger CLI si absent
- Création du répertoire de travail distant
- Synchronisation du
ci-engine - Smoke test (versions)
2.2 Configuration Gitrust (fichier .env)
Ajouter au .env de Gitrust :
# Activer globalement le CI
CI_ENABLED=true
# Chemin du ci-engine sur le serveur de build
CI_ENGINE_PATH=/opt/gitrust-ci/ci-engine
# Limite de parallélisme
CI_MAX_CONCURRENT=4
CI_DEFAULT_TIMEOUT=3600
CI_WORKSPACE_PATH=/tmp/gitrust-ci
# Serveur de build (mettre localhost pour "même machine")
CI_REMOTE_HOST=builder.example.com
CI_REMOTE_USER=ci-runner
CI_REMOTE_PATH=/opt/gitrust-ci
CI_REMOTE_SSH_PORT=22
# CI_REMOTE_SSH_KEY=/home/gitrust/.ssh/id_ed25519 # si pas ssh-agent
Redémarrer Gitrust : systemctl restart gitrust (ou cargo run --release).
2.3 Activer la CI sur un dépôt
Dans l’interface web : aller sur le dépôt → Settings → CI.
- Cocher CI enabled (requis — par défaut
false) - Cocher Trigger on push (active l’exécution à chaque push)
- Optionnel : Auto-cancel (annule les pipelines en cours quand un nouveau démarre)
- Optionnel : Allowed branches (ex.
main,develop— vide = toutes)
2.4 Choisir le mode : Easy ou Power
flowchart LR
Push[Commit<br/>poussé] --> Tree{Arbre du<br/>commit contient ?}
Tree -->|.dagger/| Power[Mode Power<br/>dagger call -m .dagger/ ci]
Tree -->|.gitrust-ci.yml| Easy[Mode Easy<br/>dagger call -m ci-engine test-pr]
Tree -->|rien| None[Pas de pipeline]
Power --> Run[Exécution<br/>distante SSH]
Easy --> Run
Mode Easy (recommandé pour commencer)
Créer .gitrust-ci.yml à la racine du dépôt :
# Raccourci : charge un profil pré-configuré (rust | python | node)
language: rust
build:
command: "cargo build --release"
checks:
lint: "cargo clippy -- -D warnings"
format: "cargo fmt -- --check"
tests:
command: "cargo test --release"
Les profils disponibles se trouvent dans deployment/ci-engine/profiles/ (rust.yaml, python.yaml, node.yaml) dans le dépôt source.
Mode Power
Pour les utilisateurs avancés, créer un module Dagger complet dans .dagger/ :
.dagger/
├── dagger.json
├── src/
│ └── main.py # ou Go/TypeScript
La fonction ci du module est appelée directement :
dagger call -m .dagger/ ci
Avantage : accès au Daggerverse, composition, tests de pipeline. Voir la documentation Dagger.
2.5 Suivi d’un pipeline
Après un git push, une entrée apparaît dans l’onglet CI du dépôt :
stateDiagram-v2 [*] --> Pending: create_pipeline Pending --> Running: worker picks CiTask Running --> Success: exit 0 Running --> Failure: exit != 0 Running --> Cancelled: timeout / auto_cancel / manuel Success --> [*] Failure --> [*] Cancelled --> [*]
Les logs stdout/stderr sont streamés ligne par ligne dans la table ci_logs et visibles en direct dans l’UI. En cas d’échec, une notification est envoyée au propriétaire du dépôt.
3. Mettre en place le Dependency Tracker
Cette partie est totalement indépendante de la CI. Elle scanne le code poussé et produit un SBOM CycloneDX, puis (optionnellement) l’envoie à Dependency-Track pour une analyse de vulnérabilités.
3.1 Installer Syft
Sur la machine Gitrust (le scan se fait localement, pas sur le builder) :
# Installation officielle
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh \
| sh -s -- -b /usr/local/bin
# Vérification
syft --version
Dans .env :
CI_SBOM_ENABLED=true
CI_SYFT_BIN=/usr/local/bin/syft # optionnel, défaut: syft dans PATH
À ce stade : à chaque push, un SBOM CycloneDX est généré et stocké (sans upload externe). Visible dans l’onglet Security du dépôt : nombre de composants, sha256 du BOM.
3.2 Déployer Dependency-Track
Dependency-Track est une application Java qui stocke les SBOM et corrèle avec les bases CVE/OSV/NVD. Déploiement Docker recommandé :
mkdir -p /opt/dtrack && cd /opt/dtrack
curl -L -o docker-compose.yml \
https://dependencytrack.org/docker-compose.yml
docker compose up -d
L’API est disponible sur http://localhost:8081 et l’UI sur http://localhost:8080.
Login initial : admin / admin (à changer immédiatement).
3.3 Créer une clé API
Dans Dependency-Track → Administration → Access Management → Teams :
- Créer (ou réutiliser) une équipe
gitrust. - Lui attribuer les permissions :
BOM_UPLOADPROJECT_CREATION_UPLOADVIEW_PORTFOLIOVIEW_VULNERABILITY
- Générer une API key et la copier.
3.4 Configurer Gitrust
Ajouter au .env :
CI_DTRACK_ENABLED=true
CI_DTRACK_URL=http://localhost:8081
CI_DTRACK_API_KEY=odt_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Redémarrer Gitrust. À chaque push :
sequenceDiagram
participant Push as post-receive
participant Sbom as SbomService
participant Git as git archive
participant Syft
participant Dtrack as Dependency-Track
participant DB as sbom_reports
Push->>Sbom: process_push(sha, ref)
Sbom->>DB: upsert status=pending
Sbom->>Git: archive sha | tar -x tmpdir
Sbom->>Syft: scan dir:tmpdir -o cyclonedx-json
Syft-->>Sbom: BOM bytes
Sbom->>DB: update sha256
Sbom->>Dtrack: PUT /api/v1/bom (base64)
Dtrack-->>Sbom: token
loop max 30s
Sbom->>Dtrack: GET /api/v1/bom/token/{token}
Dtrack-->>Sbom: processing ?
end
Sbom->>Dtrack: GET /api/v1/finding/project/{uuid}
Dtrack-->>Sbom: findings[]
Sbom->>DB: update status=success<br/>+ critical/high/medium/low
3.5 Lire les résultats
Onglet Security du dépôt → encart SBOM :
- Nombre de composants détectés
- Compteurs par sévérité : Critical / High / Medium / Low
- Lien direct vers le projet dans Dependency-Track (si l’UUID est résolu)
- Hash sha256 du BOM (traçabilité)
Si Dependency-Track met plus de 30s à analyser, le status reste processing — un sweeper ultérieur ira rechercher les findings.
4. Débogage
Problèmes CI courants
| Symptôme | Cause probable | Vérifier |
|---|---|---|
Pipeline reste pending | Worker non démarré, ou channel saturé | Logs CI worker started, CI_MAX_CONCURRENT |
Échec rsync vers le serveur de build | SSH bloqué, clé absente | ssh -p ${CI_REMOTE_SSH_PORT} ${CI_REMOTE_USER}@${CI_REMOTE_HOST} manuel |
dagger: command not found | Dagger non installé sur le builder | Relancer setup-remote-ci.sh |
Status Cancelled inattendu | Timeout (CI_DEFAULT_TIMEOUT) ou auto-cancel | Augmenter le timeout, vérifier pipelines concurrents |
Problèmes SBOM courants
| Symptôme | Cause probable | Vérifier |
|---|---|---|
SBOM generation disabled | CI_SBOM_ENABLED=false | .env |
syft spawn failed | Binaire introuvable | which syft, CI_SYFT_BIN |
invalid commit sha | SHA non hex 40 chars | Rare, signale un bug |
Dtrack upload 401 | API key invalide ou permissions manquantes | Régénérer, vérifier les 4 permissions |
Status processing indéfiniment | Dtrack surchargé, analyse lente | Attendre, ou relancer un push |
Logs utiles
# Logs Gitrust (systemd)
journalctl -u gitrust -f | grep -E "CI|SBOM|sbom|pipeline"
# Vérifier un pipeline en DB
psql $DATABASE_URL -c "SELECT id, status, commit_sha, created_at FROM ci_pipelines ORDER BY created_at DESC LIMIT 10;"
# Vérifier un SBOM
psql $DATABASE_URL -c "SELECT commit_sha, status, components_count, critical_count, high_count FROM sbom_reports ORDER BY created_at DESC LIMIT 10;"
5. Checklist récapitulative
CI :
- Docker + Dagger installés sur le serveur de build (
setup-remote-ci.sh) -
CI_ENABLED=trueetCI_REMOTE_*dans.env - Gitrust redémarré
- CI activée dans Settings → CI pour chaque dépôt
- Fichier
.gitrust-ci.yml(Easy) ou.dagger/(Power) commité - Push → pipeline visible dans l’onglet CI
Dependency Tracker :
- Syft installé sur la machine Gitrust
-
CI_SBOM_ENABLED=truedans.env - (Optionnel) Dependency-Track déployé + API key créée avec les 4 permissions
- (Optionnel)
CI_DTRACK_ENABLED=true,CI_DTRACK_URL,CI_DTRACK_API_KEYdans.env - Gitrust redémarré
- Push → SBOM visible dans l’onglet Security
Configurer ssh-guard (durcissement SSH)
À qui s’adresse cette page
Administrateurs qui déploient ou ajustent la couche ssh-guard de gitrust selon leur topologie réseau (Internet direct, derrière nginx stream, derrière HAProxy, ou réseau privé).
Avant de commencer : si vous voulez d’abord comprendre ce que fait ssh-guard et ses compromis, lisez ssh-guard : détection d’attaques SSH. La référence des variables est dans Variables d’environnement — SSH_GUARD_*.
1. Choisir le profil de déploiement
Le profil pré-configure les défauts cohérents avec votre topologie. Une variable unique pilote tout le reste : SSH_GUARD_PROFILE.
| Profil | Topologie | Quand l’utiliser |
|---|---|---|
direct | gitrust écoute en :22 ou :2222 exposé Internet, sans proxy devant | VPS minimal, démo publique |
nginx | nginx stream sur :22 avec proxy_protocol on; → gitrust sur 127.0.0.1:2222 | Production avec reverse-proxy nginx (cas le plus courant) |
haproxy | HAProxy en frontal SSH avec send-proxy ou send-proxy-v2 | Topologies multi-services |
private | Instance interne (VPN, réseau d’entreprise), pas d’attaque externe attendue | Intranet, lab |
custom | Aucun preset, vous fixez chaque variable individuellement | Cas exotiques |
Le défaut au démarrage si SSH_GUARD_PROFILE n’est pas défini est custom, qui équivaut au profil direct côté détecteurs mais avec PROXY protocol désactivé.
2. Recette : profil direct (Internet direct)
Cas typique : VPS, pas de reverse-proxy SSH devant gitrust. Le port :2222 est ouvert sur Internet et peer_addr() est l’IP réelle du client.
2.1 .env minimal
Copiez template/env/ssh-guard-direct.env dans votre /opt/gitrust/.env ou ajoutez :
SSH_GUARD_ENABLED=true
SSH_GUARD_DRY_RUN=false
SSH_GUARD_PROFILE=direct
# Stockage hybride (RAM chaude + write-through PostgreSQL)
SSH_GUARD_STORE_BACKEND=hybrid
# Logs JSON dans journald + fichier dédié pour fail2ban
SSH_GUARD_LOG_FORMAT=json
SSH_GUARD_LOG_TARGET=both
SSH_GUARD_LOG_FILE=/var/log/gitrust-ssh-guard.json
Les seuils par défaut s’appliquent : brute-force 5/5min, énumération 10/5min, scan de clés 10/5min, flood 10/s burst 20, ban 1 h.
2.2 Redémarrage et vérification
sudo systemctl restart gitrust
# Le module annonce sa configuration au démarrage
sudo journalctl -u gitrust --since "1 min ago" | grep "SSH guard runtime assembly"
# Attendu : profile=direct enabled=true dry_run=false store=Hybrid
# Logrotate sur le fichier dédié
sudo tee /etc/logrotate.d/gitrust-ssh-guard <<'EOF'
/var/log/gitrust-ssh-guard.json {
daily
rotate 14
missingok
notifempty
compress
delaycompress
copytruncate
}
EOF
3. Recette : profil nginx (derrière nginx stream)
Cas typique : nginx termine TLS sur :443, fait du SSH stream sur :22 → 127.0.0.1:2222 avec PROXY protocol v2.
3.1 nginx (rappel — voir aussi Durcir avec Fail2ban)
# /etc/nginx/nginx.conf (bloc stream)
stream {
upstream gitrust_ssh {
server 127.0.0.1:2222;
}
server {
listen 22;
proxy_pass gitrust_ssh;
proxy_protocol on; # IMPORTANT — sinon ssh-guard rejettera
proxy_timeout 30m; # git push de gros pack peut être long
}
}
3.2 .env côté gitrust
Copiez template/env/ssh-guard-nginx.env ou :
SSH_GUARD_ENABLED=true
SSH_GUARD_PROFILE=nginx
# Le profil nginx pose déjà :
# SSH_GUARD_PROXY_PROTOCOL=v2
# SSH_GUARD_PROXY_PROTOCOL_STRICT=true
# SSH_GUARD_TRUSTED_PROXIES=127.0.0.1/32,::1/128
# (override individuel possible)
SSH_GUARD_LOG_FORMAT=json
SSH_GUARD_LOG_TARGET=both
SSH_GUARD_LOG_FILE=/var/log/gitrust-ssh-guard.json
Côté binding gitrust :
SSH_LISTEN_ADDR=127.0.0.1
SSH_PORT=2222
3.3 Vérification que la vraie IP est bien extraite
Faites un git ls-remote ssh://git@VOTRE_FQDN/owner/repo.git depuis une IP externe connue, puis :
sudo journalctl -u gitrust --since "1 min ago" | grep '"event":"connection_accepted"' | tail -1
# Attendu : "ip":"<VOTRE IP EXTERNE>" et NON "ip":"127.0.0.1"
Si vous voyez 127.0.0.1, c’est que le PROXY protocol n’est pas correctement transmis : vérifiez proxy_protocol on; dans nginx.
4. Recette : profil haproxy
Le profil pose SSH_GUARD_PROXY_PROTOCOL=any (auto-détection v1 ou v2) avec strict=true, mais n’a pas de trusted_proxies par défaut (HAProxy est presque toujours sur une IP distincte). À fournir explicitement :
SSH_GUARD_ENABLED=true
SSH_GUARD_PROFILE=haproxy
SSH_GUARD_TRUSTED_PROXIES=10.0.0.5/32 # IP de l'haproxy
SSH_GUARD_LOG_FORMAT=json
SSH_GUARD_LOG_TARGET=both
SSH_GUARD_LOG_FILE=/var/log/gitrust-ssh-guard.json
Si SSH_GUARD_TRUSTED_PROXIES est vide, gitrust refuse de démarrer avec l’erreur :
SSH_GUARD_TRUSTED_PROXIES required when SSH_GUARD_PROXY_PROTOCOL is enabled
C’est volontaire : sans cette protection, n’importe qui pourrait forger un en-tête PROXY pour usurper une IP cliente.
5. Recette : profil private (réseau interne)
Détecteurs désactivés (seuils u32::MAX), événements toujours émis pour audit, store en mémoire (rapide, perdu au restart).
SSH_GUARD_ENABLED=true
SSH_GUARD_PROFILE=private
# Tous les détecteurs sont désactivés par le preset.
# Les événements connection_accepted / auth_failed / auth_succeeded
# restent émis pour audit.
SSH_GUARD_LOG_FORMAT=json
SSH_GUARD_LOG_TARGET=stderr # journald uniquement, pas de fichier
6. Mode dry-run : observer avant de bloquer
Idéal pour valider un nouveau seuil ou laisser fail2ban faire le ban réel :
SSH_GUARD_DRY_RUN=true
Effets :
- Les détecteurs continuent à corréler.
- Un événement
ip_bannedest émis pour chaque déclenchement (signal pour fail2ban / Loki). - Aucun ban n’est persisté côté ssh-guard. Le
BanManagerreste no-op.
C’est le mode recommandé pendant les premiers jours après un changement de seuil agressif. Surveillez :
sudo journalctl -u gitrust --since "24 hours ago" --no-pager \
| grep '"event":"ip_banned"' \
| jq -r '.ip + " " + .reason' \
| sort | uniq -c | sort -rn
7. Ajuster les seuils
7.1 Variables disponibles
# Brute-force : nombre d'échecs auth depuis une IP dans la fenêtre
SSH_GUARD_BRUTE_FORCE_THRESHOLD=5 # défaut 5
SSH_GUARD_BRUTE_FORCE_WINDOW_SECS=300 # défaut 300 (5 min)
# Énumération : nombre d'usernames distincts essayés depuis une IP
SSH_GUARD_USER_ENUM_THRESHOLD=10
SSH_GUARD_USER_ENUM_WINDOW_SECS=300
# Scan de clés : nombre de fingerprints distincts essayés depuis une IP
SSH_GUARD_KEY_SCAN_THRESHOLD=10
SSH_GUARD_KEY_SCAN_WINDOW_SECS=300
# Flood TCP : cap dur de connexions par seconde par IP
SSH_GUARD_CONN_FLOOD_PER_SEC=10
SSH_GUARD_CONN_FLOOD_BURST=20
# Durée d'un ban auto. 0 = permanent.
SSH_GUARD_AUTO_BAN_DURATION_SECS=3600 # défaut 3600 (1 h)
7.2 Profils suggérés
| Posture | Brute-force | Conn flood | TTL ban |
|---|---|---|---|
| Stricte (instance publique très exposée) | 3 / 5 min | 5/s burst 10 | 6 h |
| Standard (défaut) | 5 / 5 min | 10/s burst 20 | 1 h |
| Tolérante (équipe interne, partenaires CI) | 10 / 5 min | 20/s burst 50 | 30 min |
Désactiver complètement un détecteur : mettre son seuil à 4294967295 (u32::MAX). Le préfixe private le fait déjà pour les quatre.
8. Allowlist d’un partenaire CI
Une IP allowlistée bypasse les détecteurs et le flood-limit, mais reste auditée. Les ACL se gèrent via la table ssh_guard_acl (admin UI ou SQL direct).
-- Allow le runner CI de l'équipe build (CIDR /32)
INSERT INTO ssh_guard_acl (id, kind, ip_cidr, reason, created_by, created_at, updated_at)
VALUES (
gen_random_uuid(),
'allow',
'198.51.100.42/32',
'Runner CI équipe build (ticket OPS-1234)',
'<UUID admin>',
NOW(), NOW()
);
Vérifier : depuis cette IP, déclencher 50 tentatives ratées en 1 minute, puis vérifier qu’aucun ip_banned n’a été émis :
sudo journalctl -u gitrust --since "5 min ago" --no-pager \
| grep '"ip":"198.51.100.42"' \
| jq -r '.event' | sort | uniq -c
# Attendu : auth_failed: 50, brute_force_detected: 0, ip_banned: 0
9. Bannir manuellement un CIDR abusif
Pour un AS qui scanne en masse, posez un ban permanent :
-- Denylist permanente d'un CIDR
INSERT INTO ssh_guard_bans (id, ip_cidr, reason, banned_at, expires_at, auto_banned)
VALUES (
gen_random_uuid(),
'198.51.100.0/24',
'admin_deny_list',
NOW(),
NULL, -- NULL = permanent
false
);
expires_at = NULL rend le ban permanent. Pour un TTL fini : NOW() + INTERVAL '7 days'.
10. Troubleshooting
10.1 Tous les ip valent 127.0.0.1
Vous êtes en profil direct mais nginx fait du stream devant gitrust. Passez en nginx et activez proxy_protocol on; dans nginx.
10.2 « SSH_GUARD_TRUSTED_PROXIES required »
Vous avez activé le profil haproxy (ou SSH_GUARD_PROXY_PROTOCOL=v1|v2|any) sans déclarer de proxies de confiance. Ajoutez SSH_GUARD_TRUSTED_PROXIES=<CIDR> et redémarrez.
10.3 Faux positifs sur un développeur avec 8 clés SSH
L’agent SSH du dev présente toutes ses clés successivement. Si la 9e est la bonne, le détecteur de scan de clés pourrait s’activer (seuil 10 par défaut). Solutions, par ordre de préférence :
- Côté dev : forcer une clé spécifique avec
IdentitiesOnly yesdans~/.ssh/config. - Côté admin : monter le seuil à 15 (
SSH_GUARD_KEY_SCAN_THRESHOLD=15).
10.4 Lever un ban auto sans attendre l’expiration
UPDATE ssh_guard_bans
SET unbanned_at = NOW(), unbanned_by = '<UUID admin>'
WHERE id = '<UUID du ban>';
ssh-guard émet un ip_unbanned au prochain cycle d’effective_status.
10.5 Désactiver temporairement ssh-guard
# .env
SSH_GUARD_ENABLED=false
sudo systemctl restart gitrust
ssh-guard devient un pass-through complet : aucune détection, aucun ban, le listener délègue directement à russh. À éviter sauf urgence.
11. Pour aller plus loin
- Variables d’environnement — SSH_GUARD_*
- Événements ssh-guard (JSON)
- ssh-guard : détection d’attaques SSH
- Durcir avec Fail2ban — chaîner ssh-guard et fail2ban
- Dépanner SSH — diagnostic général SSH
- Régler le rate limiting — distinction rate-limit HTTP vs TCP
Durcir l’instance avec Fail2ban
Évolution importante : depuis l’introduction de la crate
gitrust-ssh-guard, le jail[gitrust-ssh]ne lit plus les logsrusshviajournalctlmais consomme directement le flux JSON stable émis par ssh-guard dans/var/log/gitrust-ssh-guard.json. Cela rend le jail bien plus fiable (format garanti stable, pas de regex fragile sur les messages derussh). ActivezSSH_GUARD_LOG_TARGET=bothdans.env— voir Configurer ssh-guard.
Configuration complète pour protéger un déploiement gitrust exposant :
- sshd système sur
:2022(admin) - Nginx en reverse proxy HTTPS
:443+ redirect:80+stream :22 → :2222 - Gitrust (web
127.0.0.1:4000, SSH russh127.0.0.1:2222) - Dagger CI runner (local, pas d’exposition réseau directe)
- Dependency-Track API
:8081+ UI:8080(si exposés) - PostgreSQL
127.0.0.1:5432(défensif au cas où le bind fuit)
1. Installation
sudo apt install -y fail2ban
sudo systemctl enable --now fail2ban
fail2ban-client --version
Vérifier que ufw est actif (fournit banaction = ufw dans la config) :
sudo ufw status
2. Fichier principal /etc/fail2ban/jail.local
À placer via :
sudo tee /etc/fail2ban/jail.local <<'EOF'
[Le contenu ci-dessous]
EOF
# =============================================================================
# Défauts globaux
# =============================================================================
[DEFAULT]
backend = systemd
bantime = 1h
findtime = 10m
maxretry = 5
# LAN privé et loopback jamais bannis
ignoreip = 127.0.0.1/8 ::1 192.168.1.0/24
# UFW
banaction = ufw
banaction_allports = ufw
# Notifications (optionnel — nécessite un relay SMTP local)
# destemail = contact@gitrust.eu
# sender = fail2ban@votre-serveur.example.com
# action = %(action_mwl)s
# =============================================================================
# 1) sshd admin système — port 2022
# =============================================================================
[sshd]
enabled = true
port = 2022
filter = sshd
backend = %(sshd_backend)s
logpath = %(sshd_log)s
maxretry = 3
findtime = 10m
bantime = 1h
# =============================================================================
# 2) Nginx — 401/403 auth basic (si un jour activé sur /admin, /metrics, etc.)
# =============================================================================
[nginx-http-auth]
enabled = true
port = http,https
filter = nginx-http-auth
logpath = /var/log/nginx/gitrust.error.log
maxretry = 5
# =============================================================================
# 3) Nginx — scanners d'URL (wp-admin, .env, phpmyadmin, config.php, etc.)
# =============================================================================
[nginx-botsearch]
enabled = true
port = http,https
filter = nginx-botsearch
logpath = /var/log/nginx/gitrust.access.log
maxretry = 2
findtime = 10m
bantime = 24h
# =============================================================================
# 4) Nginx — bad user-agents (scanners agressifs, masscan, nikto, etc.)
# =============================================================================
[nginx-badbots]
enabled = true
port = http,https
filter = nginx-badbots
logpath = /var/log/nginx/gitrust.access.log
maxretry = 2
bantime = 24h
# =============================================================================
# 5) Nginx — dépassement limit_req (voir section 4 — zones HTTPS)
# =============================================================================
[nginx-limit-req]
enabled = true
port = http,https
filter = nginx-limit-req
logpath = /var/log/nginx/gitrust.error.log
maxretry = 10
findtime = 5m
bantime = 1h
# =============================================================================
# 6) Gitrust — brute force login (formulaire + API JWT)
# =============================================================================
[gitrust-login]
enabled = true
port = http,https
filter = gitrust-login
logpath = /var/log/nginx/gitrust.access.log
maxretry = 5
findtime = 10m
bantime = 1h
# =============================================================================
# 7) Gitrust — abus API (tokens personnels leaked, scrapers)
# =============================================================================
[gitrust-api-abuse]
enabled = true
port = http,https
filter = gitrust-api-abuse
logpath = /var/log/nginx/gitrust.access.log
maxretry = 30
findtime = 1m
bantime = 2h
# =============================================================================
# 8) Gitrust SSH — consomme les événements JSON stables de ssh-guard
# =============================================================================
# IMPORTANT : ce jail consomme le flux JSON émis par la crate gitrust-ssh-guard
# (champs stables documentés dans ../reference/ssh-guard-evenements.md).
# Le ban est déclenché soit sur le "signal fort" ip_banned (ssh-guard a déjà
# détecté la brute-force, fail2ban relaye au firewall) — un seul ip_banned
# suffit (maxretry=1) — soit sur le brut auth_failed avec le seuil habituel.
#
# Prérequis dans /opt/gitrust/.env :
# SSH_GUARD_LOG_TARGET=both
# SSH_GUARD_LOG_FILE=/var/log/gitrust-ssh-guard.json
[gitrust-ssh]
enabled = true
port = 22,2222
filter = gitrust-ssh
logpath = /var/log/gitrust-ssh-guard.json
maxretry = 1 # ssh-guard a déjà corrélé : 1 ip_banned = 1 ban firewall
findtime = 10m
bantime = 1h
# =============================================================================
# 9) Gitrust import worker — tokens invalides sur clone de dépôts externes
# =============================================================================
[gitrust-import]
enabled = true
port = http,https
filter = gitrust-import
backend = systemd
journalmatch = _SYSTEMD_UNIT=gitrust.service
maxretry = 3
findtime = 5m
bantime = 30m
# =============================================================================
# 10) Dependency-Track UI — brute force login (port 8080)
# =============================================================================
# Activer UNIQUEMENT si DTrack est exposé publiquement.
# Si DTrack est sur réseau privé/loopback, laisser enabled=false.
[dtrack-login]
enabled = false
port = 8080,http,https
filter = dtrack-login
logpath = /var/log/nginx/dtrack.access.log
maxretry = 5
findtime = 10m
bantime = 2h
# =============================================================================
# 11) Dependency-Track API — abus clé API (port 8081)
# =============================================================================
[dtrack-api]
enabled = false
port = 8081,http,https
filter = dtrack-api
logpath = /var/log/nginx/dtrack.access.log
maxretry = 10
findtime = 5m
bantime = 1h
# =============================================================================
# 12) PostgreSQL — tentatives de connexion invalides (défensif)
# =============================================================================
# PG doit binder 127.0.0.1 uniquement. Ce jail protège si la config fuit.
[postgresql]
enabled = true
port = 5432
filter = postgresql
backend = systemd
journalmatch = _SYSTEMD_UNIT=postgresql.service + _SYSTEMD_UNIT=docker.service
maxretry = 5
findtime = 10m
bantime = 1h
# =============================================================================
# 13) Récidive — ban long pour IPs bannies 3x en 24h, tous jails confondus
# =============================================================================
[recidive]
enabled = true
logpath = /var/log/fail2ban.log
banaction = %(banaction_allports)s
bantime = 1w
findtime = 1d
maxretry = 3
3. Filtres personnalisés
À placer dans /etc/fail2ban/filter.d/ (un fichier par jail custom).
3.1 gitrust-login.conf
sudo tee /etc/fail2ban/filter.d/gitrust-login.conf <<'EOF'
[Definition]
# POST sur /login ou endpoints JWT avec code 4xx (401/403/422/429)
failregex = ^<HOST> .* "POST /(login|api/v1/auth/login|api/v1/auth/refresh|api/v1/auth/2fa)[^"]*" (401|403|422|429) .*$
ignoreregex =
EOF
3.2 gitrust-api-abuse.conf
sudo tee /etc/fail2ban/filter.d/gitrust-api-abuse.conf <<'EOF'
[Definition]
# Abus API : 401/403 répétés sur /api/v1/* (hors auth déjà couvert par gitrust-login)
failregex = ^<HOST> .* "(GET|POST|PUT|DELETE|PATCH) /api/v1/(?!auth/)[^"]*" (401|403) .*$
ignoreregex =
EOF
3.3 gitrust-ssh.conf (consomme le JSON ssh-guard)
Le filtre matche les événements JSON stables produits par gitrust-ssh-guard. Voir Événements ssh-guard (JSON) pour le schéma complet.
Le « signal fort » ip_banned est privilégié : ssh-guard a déjà corrélé brute-force / énumération / scan de clés, et fail2ban n’a plus qu’à appliquer le ban au niveau firewall (UFW/iptables) pour les autres ports si désiré.
sudo tee /etc/fail2ban/filter.d/gitrust-ssh.conf <<'EOF'
[Definition]
# Filtre les événements stables émis par gitrust-ssh-guard dans
# /var/log/gitrust-ssh-guard.json. Format : une ligne JSON par événement.
#
# Capture <HOST> depuis le champ "ip" du JSON pour les variants pertinents.
#
# Tester :
# sudo fail2ban-regex /var/log/gitrust-ssh-guard.json \
# /etc/fail2ban/filter.d/gitrust-ssh.conf
failregex = ^.*"event":"ip_banned".*"ip":"<HOST>".*$
^.*"event":"brute_force_detected".*"ip":"<HOST>".*$
^.*"event":"user_enumeration_detected".*"ip":"<HOST>".*$
^.*"event":"key_scanning_detected".*"ip":"<HOST>".*$
^.*"event":"connection_dropped".*"ip":"<HOST>".*"reason":"untrusted_proxy".*$
^.*"event":"connection_dropped".*"ip":"<HOST>".*"reason":"proxy_header_invalid".*$
ignoreregex =
# Date au format ISO 8601 UTC produit par ssh-guard
datepattern = "ts":"%%Y-%%m-%%dT%%H:%%M:%%S
EOF
Variante « brute uniquement » — si vous préférez que fail2ban corrèle lui-même à partir des
auth_failedsans dépendre du verdict ssh-guard, remplacez le bloc ci-dessus parfailregex = ^.*"event":"auth_failed".*"ip":"<HOST>".*$et passezmaxretry = 5dans le jail. Les deux approches sont valides ; la première est plus rapide à réagir, la seconde est plus indépendante.
3.4 gitrust-import.conf
sudo tee /etc/fail2ban/filter.d/gitrust-import.conf <<'EOF'
[Definition]
# Import worker : tokens OAuth/GitHub invalides sur clone de dépôts externes
failregex = ^.*import.*authentication failed.*from <HOST>.*$
^.*import.*invalid (token|credentials).*from <HOST>.*$
^.*import.*clone failed.*401.*from <HOST>.*$
ignoreregex =
EOF
3.5 dtrack-login.conf (si DTrack exposé)
sudo tee /etc/fail2ban/filter.d/dtrack-login.conf <<'EOF'
[Definition]
# Dependency-Track : brute force du POST /api/v1/user/login
failregex = ^<HOST> .* "POST /api/v1/user/login[^"]*" (401|403) .*$
^<HOST> .* "POST /api/v1/user/forceChangePassword[^"]*" (401|403) .*$
ignoreregex =
EOF
3.6 dtrack-api.conf (si DTrack exposé)
sudo tee /etc/fail2ban/filter.d/dtrack-api.conf <<'EOF'
[Definition]
# Dependency-Track API : abus de clé API ou requêtes non authentifiées
failregex = ^<HOST> .* "[^"]+ /api/v1/[^"]*" 401 .*$
^<HOST> .* "[^"]+ /api/v1/[^"]*" 403 .*$
ignoreregex =
EOF
3.7 postgresql.conf (créer si absent)
if [ ! -f /etc/fail2ban/filter.d/postgresql.conf ]; then
sudo tee /etc/fail2ban/filter.d/postgresql.conf <<'EOF'
[Definition]
failregex = ^.*authentication failed for user.*host=<HOST>.*$
^.*FATAL:.*password authentication failed.*<HOST>.*$
^.*no pg_hba\.conf entry for host "<HOST>".*$
ignoreregex =
EOF
fi
3.8 nginx-limit-req.conf (créer si absent sur votre distro)
if [ ! -f /etc/fail2ban/filter.d/nginx-limit-req.conf ]; then
sudo tee /etc/fail2ban/filter.d/nginx-limit-req.conf <<'EOF'
[Definition]
failregex = ^.*limiting requests, excess: .* by zone .*, client: <HOST>.*$
ignoreregex =
EOF
fi
3.9 nginx-badbots.conf — OBLIGATOIRE (pas fourni par Debian/Ubuntu)
Le paquet fail2ban Debian/Ubuntu fournit apache-badbots.conf mais PAS nginx-badbots.conf. Sans ce filter, fail2ban-client reload affiche :
Found no accessible config files for 'filter.d/nginx-badbots' under /etc/fail2ban
Le filter ci-dessous réutilise la liste de bad user-agents de apache-badbots avec un failregex adapté au format combined de nginx :
sudo tee /etc/fail2ban/filter.d/nginx-badbots.conf <<'EOF'
[Definition]
badbotscustom = EmailCollector|WebEMailExtrac|TrackBack/1\.02|sogou music spider
badbots = Atomic_Email_Hunter/4\.0|atSpider/1\.0|autoemailspider|bwh3_user_agent|China Local Browse 2\.6|ContactBot/0\.2|ContentSmartz|DataCha0s/2\.0|DBrowse 1\.4b|DBrowse 1\.4d|Demo Bot DOT 16b|Demo Bot Z 16b|DSurf15a 01|DSurf15a 71|DSurf15a 81|DSurf15a VA|EBrowse 1\.4b|Educate Search VxB|EmailSiphon|EmailSpider|EmailWolf 1\.00|ExtractorPro|Franklin Locator 1\.8|Full Web Bot 0416B|Guestbook Auto Submitter|ISC Systems iRc Search 2\.1|LMQueueBot/0\.2|LWP\:\:Simple/5\.803|Microsoft URL Control - 6\.00\.8xxx|Missigua Locator 1\.9|Mozilla/4\.0 efp@gmx\.net|Nsauditor/1\.x|PBrowse 1\.4b|PEval 1\.4b|Poirot|psycheclone|sogou spider|sohu agent|VadixBot|WebVulnCrawl\.unknown/1\.0 libwww-perl/5\.803|Wells Search II|WEP Search 00
failregex = ^<HOST> -.*"(GET|POST|HEAD).*HTTP.*"(?:%(badbots)s|%(badbotscustom)s)"$
ignoreregex =
datepattern = ^[^\[]*\[({DATE})
{^LN-BEG}
EOF
Alternative : si vous ne voulez pas maintenir la liste, simplement désactiver le jail (enabled = false dans le [nginx-badbots]) — la combinaison nginx-botsearch + nginx-limit-req + gitrust-api-abuse couvre déjà l’essentiel.
4. Rate limiting côté Nginx (requis par nginx-limit-req)
4.1 Déclarer les zones globales
sudo tee /etc/nginx/conf.d/gitrust-limit.conf <<'EOF'
# Zones de rate limiting gitrust
limit_req_zone $binary_remote_addr zone=gitrust_login:10m rate=1r/s;
limit_req_zone $binary_remote_addr zone=gitrust_api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=gitrust_git:10m rate=30r/s;
EOF
4.2 Ajouter les location dans /etc/nginx/sites-available/gitrust
Dans le bloc server { listen 443 ssl; ... }, avant location / finale :
# Protection brute force login (1 req/s par IP, burst 5)
location ~ ^/(login|api/v1/auth/) {
limit_req zone=gitrust_login burst=5 nodelay;
proxy_pass http://gitrust_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Protection API (10 req/s par IP, burst 20)
location ^~ /api/v1/ {
limit_req zone=gitrust_api burst=20 nodelay;
proxy_pass http://gitrust_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Protection Git smart HTTP (30 req/s — clone/push légitime peut être verbeux)
location ~ ^/[^/]+/[^/]+\.git/ {
limit_req zone=gitrust_git burst=50 nodelay;
proxy_pass http://gitrust_backend;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_request_buffering off;
client_max_body_size 2G;
}
4.3 Reload
sudo nginx -t && sudo systemctl reload nginx
5. Démarrage et validation
sudo systemctl restart fail2ban
# Liste des jails actifs
sudo fail2ban-client status
# Attendu :
# sshd, nginx-http-auth, nginx-botsearch, nginx-badbots, nginx-limit-req,
# gitrust-login, gitrust-api-abuse, gitrust-ssh, gitrust-import,
# postgresql, recidive
# (+ dtrack-login, dtrack-api si activées)
# Stats par jail
sudo fail2ban-client status gitrust-login
sudo fail2ban-client status sshd
Valider chaque filtre custom
# 1. gitrust-login contre l'access log nginx
sudo fail2ban-regex /var/log/nginx/gitrust.access.log \
/etc/fail2ban/filter.d/gitrust-login.conf
# 2. gitrust-api-abuse
sudo fail2ban-regex /var/log/nginx/gitrust.access.log \
/etc/fail2ban/filter.d/gitrust-api-abuse.conf
# 3. gitrust-ssh contre le flux JSON ssh-guard
sudo fail2ban-regex /var/log/gitrust-ssh-guard.json \
/etc/fail2ban/filter.d/gitrust-ssh.conf
# Si /var/log/gitrust-ssh-guard.json n'existe pas, vérifiez SSH_GUARD_LOG_TARGET
# et SSH_GUARD_LOG_FILE dans /opt/gitrust/.env (voir how-to/configurer-ssh-guard.md).
# 4. postgresql
journalctl -u docker --no-pager -n 5000 | grep -i postgres > /tmp/pg.log
sudo fail2ban-regex /tmp/pg.log \
/etc/fail2ban/filter.d/postgresql.conf
0 failregex found = regex à ajuster après observation des logs réels de chaque service.
Test de bannissement
# Depuis un host HORS LAN (sinon ignoreip s'applique)
for i in $(seq 1 10); do
curl -sk -o /dev/null -w "%{http_code}\n" \
-X POST https://<votre-domaine>/login \
-d 'username=admin&password=wrong'
done
# Vérifier le ban
ssh gitrust-host 'sudo fail2ban-client status gitrust-login'
# Currently banned : 1 — Banned IP list : <IP test>
# Débannir manuellement
ssh gitrust-host 'sudo fail2ban-client unban <IP>'
6. Monitoring
# Logs fail2ban temps réel
sudo tail -f /var/log/fail2ban.log
# Toutes les IPs bannies, tous jails confondus
sudo fail2ban-client banned
# Stats détaillées d'un jail
sudo fail2ban-client status gitrust-ssh
# Currently failed : 2
# Total failed : 47
# Currently banned : 1
# Total banned : 12
# Banned IP list : 203.0.113.42
sudo grep "Ban " /var/log/fail2ban.log | tail -20
7. Matrice récapitulative
| # | Jail | Port(s) | Source logs | Max retry | Ban time | Surface protégée |
|---|---|---|---|---|---|---|
| 1 | sshd | 2022 | journald sshd | 3 | 1h | SSH admin |
| 2 | nginx-http-auth | 80,443 | nginx error | 5 | 1h | Basic auth (admin futur) |
| 3 | nginx-botsearch | 80,443 | nginx access | 2 | 24h | Scanners WP/PHP |
| 4 | nginx-badbots | 80,443 | nginx access | 2 | 24h | User-agents malveillants |
| 5 | nginx-limit-req | 80,443 | nginx error | 10 | 1h | Flood global |
| 6 | gitrust-login | 80,443 | nginx access | 5 | 1h | Brute force login UI + API |
| 7 | gitrust-api-abuse | 80,443 | nginx access | 30 | 2h | Scrapers API (tokens fuités) |
| 8 | gitrust-ssh | 22,2222 | /var/log/gitrust-ssh-guard.json (JSON stable ssh-guard) | 1 | 1h | Brute force / scan clés / énumération SSH Git |
| 9 | gitrust-import | 80,443 | journald gitrust | 3 | 30m | Brute force PAT/OAuth import |
| 10 | dtrack-login | 8080 | nginx access | 5 | 2h | Brute force UI Dep-Track |
| 11 | dtrack-api | 8081 | nginx access | 10 | 1h | Abus clé API Dep-Track |
| 12 | postgresql | 5432 | journald | 5 | 1h | Défense en profondeur PG |
| 13 | recidive | all | fail2ban.log | 3 | 1w | Méta-ban 3×/24h |
Ordre de déclenchement typique : brute force → jail spécifique (ban 1h) → si récidive 3x → recidive (ban 1 semaine, tous ports).
8. Notes spécifiques à la stack gitrust
8.1 Dagger (CI)
Dagger s’exécute localement sur la machine gitrust (ou un runner distant — voir administration_manual/how-to/configurer-ci-runner-remote.md). Aucun port réseau ouvert → pas de jail dédié.
8.2 Dependency-Track
Si DTrack est déployé sur le même serveur et exposé, activer les jails 10 et 11 en mettant enabled = true et en pointant logpath vers les logs nginx du vhost DTrack.
Si DTrack est interne (VPN, réseau privé), laisser enabled = false — le firewall suffit.
Config DTrack recommandée :
- Bind sur
127.0.0.1viadtrack.url.base=http://127.0.0.1:8080
8.3 PostgreSQL
Le jail postgresql est défensif. PG doit rester bindé sur 127.0.0.1:5432 via Docker. Vérifier :
sudo ss -tlnp | grep 5432
# Attendu : 127.0.0.1:5432 — PAS 0.0.0.0:5432
8.4 Notifications
Pour recevoir un mail à chaque ban :
- Installer un relay SMTP local :
sudo apt install msmtp-mta - Configurer
/etc/msmtprcavec un compte SMTP - Décommenter dans
[DEFAULT]:destemail = contact@gitrust.eu sender = fail2ban@votre-serveur.example.com action = %(action_mwl)s sudo systemctl restart fail2ban
9. Limites et évolutions
- IPv6 : toutes les regex utilisent
<HOST>qui matche IPv4 ET IPv6. Vérifier que UFW est configuré pour v6 également. - CDN/Cloudflare devant : si un CDN est ajouté,
$remote_addrcôté Nginx sera l’IP du CDN — il faut récupérer la vraie IP viaX-Forwarded-Foret propager au logging Nginx. Changerfailregexen conséquence. Sinon les jails banniront le CDN. - Docker/Podman : si gitrust passe en conteneur, les logs de
gitrust.servicedeviennentdocker.serviceoupodman.service→ mettre à jourjournalmatchdans le jail 9 (gitrust-import). Le jail 8 (gitrust-ssh) n’est pas affecté car il consomme le fichier JSON ssh-guard, à condition que ce fichier soit monté côté hôte. - GeoIP : pour bloquer des pays entiers en amont, ajouter
geo $blocked_countrydans Nginx (modulengx_http_geoip2_module) — complémentaire à fail2ban.
Régler le rate limiting
À qui s’adresse cette page
Administrateurs qui souhaitent ajuster les limites de débit pour protéger l’instance contre les attaques par force brute et les abus d’API, tout en évitant de bloquer les utilisateurs légitimes.
Variables disponibles
Ces variables se définissent dans /opt/gitrust/.env. Un redémarrage est nécessaire après modification. 0 désactive la limite (déconseillé en production).
| Variable | Défaut | Description |
|---|---|---|
RATE_LIMIT_LOGIN_PER_MINUTE | 5 | Tentatives de connexion par IP par minute |
RATE_LIMIT_REFRESH_PER_MINUTE | 10 | Rafraîchissements de token par IP par minute |
RATE_LIMIT_GENERAL_PER_MINUTE | 100 | Requêtes générales par IP par minute (tous les autres endpoints) |
Les limites s’appliquent par adresse IP. Si gitrust est derrière un reverse-proxy, assurez-vous que le vrai IP du client est transmis (en-tête X-Forwarded-For ou X-Real-IP).
Profils de configuration recommandés
Petite équipe (≤ 5 personnes, réseau de confiance)
Toutes les personnes travaillent depuis un réseau connu. On peut se permettre des limites légèrement plus souples pour éviter les faux positifs lors des scripts d’intégration.
RATE_LIMIT_LOGIN_PER_MINUTE=10
RATE_LIMIT_REFRESH_PER_MINUTE=20
RATE_LIMIT_GENERAL_PER_MINUTE=200
Équipe moyenne (6-20 personnes, instance interne)
Configuration par défaut, adaptée à la cible principale de gitrust.
RATE_LIMIT_LOGIN_PER_MINUTE=5
RATE_LIMIT_REFRESH_PER_MINUTE=10
RATE_LIMIT_GENERAL_PER_MINUTE=100
Instance publique ou exposée sur Internet (50+ utilisateurs)
Limites plus strictes sur le login pour contrer les attaques par dictionnaire. La limite générale peut être augmentée si vos utilisateurs utilisent intensivement l’API.
RATE_LIMIT_LOGIN_PER_MINUTE=3
RATE_LIMIT_REFRESH_PER_MINUTE=10
RATE_LIMIT_GENERAL_PER_MINUTE=150
CI/CD avec beaucoup d’appels API
Si vos pipelines CI font de nombreuses requêtes API (création de statuts, commentaires, webhooks), augmentez uniquement RATE_LIMIT_GENERAL_PER_MINUTE depuis les IPs des runners :
RATE_LIMIT_GENERAL_PER_MINUTE=300
Ou configurez Nginx pour whitelister les IPs des runners CI avant le rate limiting de gitrust.
Surveiller les réponses 429
Quand une limite est dépassée, gitrust retourne HTTP 429 Too Many Requests. Ces réponses apparaissent dans les logs avec le niveau WARN.
# Surveiller les 429 en temps réel
sudo journalctl -u gitrust -f | grep "429\|rate_limit\|too many"
# Compter les 429 sur les 24 dernières heures
sudo journalctl -u gitrust --since "24 hours ago" --no-pager \
| grep -c "429\|rate_limit"
Si vous utilisez Nginx comme reverse-proxy, vous pouvez aussi consulter les logs Nginx :
grep " 429 " /var/log/nginx/gitrust-access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head -20
Cela vous donne les IPs les plus souvent bloquées, utile pour distinguer une vraie attaque d’un utilisateur légitime avec un script trop agressif.
Gitrust derrière un reverse-proxy
Si gitrust écoute sur 127.0.0.1:4000 derrière Nginx, le rate limiting de gitrust voit l’IP 127.0.0.1 pour toutes les requêtes, ce qui rend la protection inefficace.
Configurez Nginx pour transmettre le vrai IP :
# Dans le bloc location de gitrust
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
Et dans /opt/gitrust/.env, indiquez que gitrust est derrière un proxy de confiance :
# Ajouter si le reverse-proxy est sur 127.0.0.1
SERVER_HOST=127.0.0.1
Gitrust utilise alors X-Real-IP pour identifier l’IP cliente réelle dans ses mécanismes de rate limiting.
Rate-limit TCP par IP (ssh-guard)
Les variables RATE_LIMIT_* ci-dessus s’appliquent à la couche HTTP (login, API, refresh JWT). Pour le port SSH (:2222 par défaut), un mécanisme distinct est fourni par la crate gitrust-ssh-guard : un cap dur de connexions TCP/seconde par IP, indépendant de tout traitement applicatif.
| Variable | Défaut | Description |
|---|---|---|
SSH_GUARD_CONN_FLOOD_PER_SEC | 10 | Cap soutenu de connexions/sec par IP. 0 ou u32::MAX = désactivé. |
SSH_GUARD_CONN_FLOOD_BURST | 20 | Burst momentané autorisé. |
Différences avec le rate-limit HTTP :
- Pas de réponse
429: la connexion TCP est droppée immédiatement au listener (avant tout handshake SSH). - Pas de ban persistant en DB : un scan de port n’engendre aucune écriture.
- Budgets indépendants par IP via token bucket GCRA (algorithme leaky bucket sans biais).
- Une IP allowlistée dans
ssh_guard_aclbypasse complètement ce cap.
Pour la recette complète (profils par topologie, dry-run, allowlist CI), voir Configurer ssh-guard. Pour le détail des événements émis (connection_flood_detected, connection_dropped avec reason=flood_limit), voir Événements ssh-guard (JSON).
Fail2ban comme couche complémentaire
Le rate limiting de gitrust agit requête par requête. Fail2ban analyse les logs et peut bannir les IPs pour une durée configurable après plusieurs 429.
Voir Durcir avec Fail2ban pour la configuration complète.
Pour aller plus loin
- Variables d’environnement — section Rate Limiting
- Variables d’environnement — section SSH_GUARD_*
- Configurer ssh-guard
- ssh-guard : détection d’attaques SSH
- Durcir avec Fail2ban
- Conformité ANSSI PA-074 — protection contre les attaques par force brute
Forcer le 2FA sur toute l’instance
À qui s’adresse cette page
Administrateurs qui souhaitent obliger tous les utilisateurs à configurer l’authentification à deux facteurs (TOTP) avant d’accéder à l’instance.
Activer le 2FA obligatoire
Le paramètre totp_force_enabled est un paramètre dynamique stocké en base de données. Il prend effet immédiatement, sans redémarrage du service.
- Connectez-vous à l’interface d’administration :
/admin - Accédez à
/admin/settings - Trouvez le paramètre
totp_force_enabled - Activez le toggle et cliquez Enregistrer

Comportement après activation
Dès que totp_force_enabled=true est enregistré :
- Tout utilisateur sans 2FA configuré est redirigé vers la page de configuration TOTP à sa prochaine connexion. Il ne peut pas accéder à l’application tant que la configuration n’est pas complète.
- Les utilisateurs qui ont déjà configuré le 2FA ne sont pas affectés.
- Les administrateurs sont soumis à la même règle — vérifiez que votre propre compte a le 2FA actif avant d’activer ce paramètre.
Conseil : prévenez vos utilisateurs avant d’activer ce paramètre. Un e-mail ou une annonce interne évite les appels au support pour des connexions bloquées.
Vérifier que le 2FA est actif sur votre compte
Avant d’activer totp_force_enabled, confirmez que votre compte admin a bien le 2FA configuré :
- Cliquez sur votre avatar → Paramètres du profil
- Section Sécurité → vérifiez que Authentification à deux facteurs affiche « Actif »
Si ce n’est pas le cas, configurez le 2FA sur votre compte avant d’activer le paramètre global.
Révoquer le 2FA d’un utilisateur spécifique
Si un utilisateur a perdu accès à son application TOTP (téléphone perdu, application réinstallée) :
- Accédez à
/admin/users - Ouvrez la fiche de l’utilisateur concerné
- Cliquez Révoquer le 2FA
- L’utilisateur sera invité à reconfigurer le TOTP à sa prochaine connexion
Désactiver le 2FA obligatoire
- Accédez à
/admin/settings - Désactivez le toggle
totp_force_enabled - Cliquez Enregistrer
Les utilisateurs peuvent à nouveau se connecter sans 2FA. Les configurations TOTP existantes sont conservées.
Pour aller plus loin
- Gérer les utilisateurs — révoquer le 2FA individuellement
- Paramètres dynamiques —
totp_force_enabled - Conformité ANSSI PA-074 — authentification multi-facteurs
Dépanner les problèmes SSH (clés, ports, fingerprints)
À qui s’adresse cette page
Administrateurs confrontés à des erreurs lors de l’authentification SSH ou des opérations git clone/git push via SSH sur gitrust.
Architecture SSH de gitrust
Gitrust intègre son propre serveur SSH via la bibliothèque Russh — ce n’est pas le démon sshd du système. Il écoute par défaut sur le port 2222. Depuis l’introduction de la crate gitrust-ssh-guard, toute connexion TCP passe d’abord par un sas (SecureListener) qui peut la dropper avant le handshake SSH (ban, flood, en-tête PROXY invalide). Voir Configurer ssh-guard et ssh-guard : détection d’attaques SSH.
sequenceDiagram
participant U as Client git (utilisateur)
participant G as ssh-guard (SecureListener)
participant R as Serveur gitrust (Russh :2222)
participant DB as PostgreSQL
U->>G: Connexion TCP (port 2222)
G->>G: ACL / ban / flood
alt Drop
G-->>U: Connexion fermée
else Accept
G->>R: TcpStream + ClientIdentity
R->>U: Présente clé hôte Ed25519
U->>U: Vérifie fingerprint (known_hosts)
U->>R: Envoie clé publique SSH
R->>DB: Cherche fingerprint dans ssh_keys
DB-->>R: Trouvé (user_id)
R->>G: AuthTracker.record_auth_attempt
R-->>U: Authentifié
U->>R: git-upload-pack / git-receive-pack
end
Erreur 1 : Connection refused sur le port 2222
ssh: connect to host SERVEUR port 2222: Connection refused
Diagnostic :
# Depuis le serveur : vérifier que gitrust écoute
ss -tlnp | grep 2222
# Attendu : LISTEN 0 ... *:2222 ... gitrust
# Depuis l'extérieur
nc -zv SERVEUR 2222
Causes et corrections :
| Cause | Correction |
|---|---|
| gitrust n’est pas démarré | sudo systemctl start gitrust |
SSH_PORT mal configuré dans .env | Vérifier SSH_PORT=2222 et redémarrer |
| Firewall bloque le port | sudo ufw allow 2222/tcp ou règle iptables équivalente |
SSH_LISTEN_ADDR=127.0.0.1 | Le serveur SSH n’est accessible que localement — changer en 0.0.0.0 si accès externe souhaité |
Erreur 2 : WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING A MITM ATTACK!
Cause : la clé SSH hôte du serveur a changé (migration vers un nouveau serveur, régénération de la clé, restauration depuis une sauvegarde différente).
Vérifier le nouveau fingerprint :
# Sur le serveur
sudo -u gitrust ssh-keygen -l -f /opt/gitrust/data/ssh_host_ed25519_key.pub
# Exemple : SHA256:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX gitrust-host-key (ED25519)
Si ce fingerprint correspond à un changement légitime (migration, etc.), demandez aux utilisateurs de supprimer l’ancienne entrée de leur known_hosts :
# Sur la machine du client git
ssh-keygen -R "[SERVEUR]:2222"
# Puis re-cloner ou accepter le nouveau fingerprint
Si vous ne reconnaissez pas ce changement, il peut indiquer une compromission. Investiguez avant de continuer.
Erreur 3 : Permission denied (publickey)
git@SERVEUR: Permission denied (publickey).
fatal: Could not read from remote repository.
Diagnostic pas à pas :
# 1. Tester la connexion avec verbosité maximale
ssh -vvv -p 2222 git@SERVEUR
# 2. Vérifier quelle clé est proposée par le client
# Dans la sortie -vvv, chercher :
# debug1: Offering public key: /home/user/.ssh/id_ed25519 ED25519 SHA256:...
# debug1: Authentications that can continue: publickey
# 3. Obtenir le fingerprint de votre clé locale
ssh-keygen -l -f ~/.ssh/id_ed25519.pub
# SHA256:YYYYYYYYYYYYYYYYYYYYYYYYYYYYYY (ED25519)
Comparez ce fingerprint avec celui enregistré dans gitrust pour votre compte :
- Connectez-vous à l’interface web gitrust
- Accédez à Paramètres → Clés SSH
- Vérifiez que le fingerprint correspond
Causes courantes :
| Cause | Correction |
|---|---|
| Aucune clé SSH enregistrée dans gitrust | Ajouter la clé publique dans les paramètres du compte |
| La clé proposée par le client n’est pas celle enregistrée | Vérifier ~/.ssh/config et l’agent SSH |
| L’utilisateur est désactivé dans gitrust | L’administrateur doit réactiver le compte via /admin/users |
| La clé enregistrée a été révoquée | Enregistrer une nouvelle clé publique |
Vérifier les permissions des fichiers de clés côté client :
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519
chmod 644 ~/.ssh/id_ed25519.pub
Erreur 4 : Could not resolve hostname
ssh: Could not resolve hostname SERVEUR: Name or service not known
Cause : le nom de domaine dans l’URL de clone ne correspond pas à SSH_PUBLIC_HOST ou n’est pas résolvable.
Vérifier la configuration :
# Dans .env
grep SSH_PUBLIC_HOST /opt/gitrust/.env
# Doit correspondre au FQDN ou à l'IP utilisé dans les URLs de clone
# Tester la résolution DNS
nslookup VOTRE_SSH_PUBLIC_HOST
Erreur 5 : Timeout ou connexion très lente
ssh: connect to host SERVEUR port 2222: Operation timed out
ou connexion qui s’établit mais met 30+ secondes.
Causes et corrections :
| Cause | Correction |
|---|---|
| Résolution DNS inverse lente | Ajouter UseDNS no dans la config SSH hôte (non applicable à Russh — configurable via SSH_LISTEN_ADDR côté gitrust) |
| Firewall qui filtre silencieusement (DROP vs REJECT) | nc -zv SERVEUR 2222 pour distinguer refused (REJECT) de timeout (DROP) |
| Trop de connexions simultanées | Vérifier les logs gitrust pour des erreurs de pool de threads |
Erreur 6 : la connexion est droppée par ssh-guard
Symptôme côté client : la connexion TCP est acceptée puis fermée immédiatement, sans aucun message d’authentification SSH. Ce comportement est typique d’un drop ssh-guard (ban actif, flood, en-tête PROXY refusé).
Diagnostic :
# Si SSH_GUARD_LOG_TARGET=both ou file
sudo tail -100 /var/log/gitrust-ssh-guard.json \
| jq 'select(.event=="connection_dropped")'
# Sinon (target=stderr) — passer par journald
sudo journalctl -u gitrust --since "5 min ago" --no-pager \
| grep '"event":"connection_dropped"' | tail -10
reason retourné | Cause | Correction |
|---|---|---|
banned | IP couverte par un ban actif (auto ou denylist admin) | Lever le ban via la table ssh_guard_bans ou attendre l’expiration. Voir Configurer ssh-guard §10.4. |
flood_limit | Trop de connexions/sec depuis cette IP | Allowlister l’IP si c’est un partenaire CI, ou monter SSH_GUARD_CONN_FLOOD_PER_SEC. |
untrusted_proxy | En-tête PROXY reçu d’une IP non listée | Vérifier SSH_GUARD_TRUSTED_PROXIES ; si nginx tourne sur le même hôte, le défaut 127.0.0.1/32,::1/128 du profil nginx doit suffire. |
proxy_header_invalid ou proxy_header_missing | nginx/HAProxy n’envoie pas le header attendu | Vérifier proxy_protocol on; dans nginx (ou send-proxy-v2 dans HAProxy). En transition, basculer temporairement SSH_GUARD_PROXY_PROTOCOL_STRICT=false. |
Diagnostic ssh-guard rapide
Lire le flux d’événements en direct
# Dernier événement par IP (ssh-guard log target = stderr/journald)
sudo journalctl -u gitrust -f | grep '"event":"' | jq -c '.'
# Idem si SSH_GUARD_LOG_TARGET=file ou both
sudo tail -f /var/log/gitrust-ssh-guard.json | jq -c '.'
Mode dry-run pour reproduire un blocage sans persister
Si vous suspectez qu’un seuil est trop agressif, passez SSH_GUARD_DRY_RUN=true dans .env puis redémarrez. Les détecteurs continuent à émettre les ip_banned (signal pour fail2ban / observabilité) sans que ssh-guard pose réellement le ban. Vous pouvez observer le motif sur 24-48 h avant de décider.
Vérifier que la vraie IP est bien extraite (profils nginx/haproxy)
# Faire un git ls-remote depuis une IP externe connue, puis :
sudo journalctl -u gitrust --since "1 min ago" --no-pager \
| grep '"event":"connection_accepted"' | tail -1 | jq .ip
# Doit retourner l'IP externe, PAS 127.0.0.1
Désactiver temporairement ssh-guard
# .env
SSH_GUARD_ENABLED=false
sudo systemctl restart gitrust
ssh-guard devient un pass-through complet (aucune détection, aucun ban). À utiliser seulement pour confirmer qu’un problème vient bien de la couche guard ; remettre true immédiatement après.
Pour le détail des événements, voir Événements ssh-guard (JSON).
Consulter les logs SSH
# Logs du service gitrust (contient les événements SSH)
sudo journalctl -u gitrust -f | grep -i "ssh\|auth\|key\|fingerprint"
# Filtrer les erreurs d'authentification
sudo journalctl -u gitrust --since "1 hour ago" --no-pager \
| grep -i "auth.*failed\|permission denied\|invalid key"
Pour aller plus loin
- Variables d’environnement — section SSH
- Variables d’environnement — section SSH_GUARD_*
- Configurer ssh-guard
- Événements ssh-guard (JSON)
- Ports et services
- Gérer les utilisateurs : révoquer une clé SSH
Dépanner la CI intégrée (Dagger)
À qui s’adresse cette page
Administrateurs confrontés à des pipelines bloqués, des workers CI qui ne répondent plus, ou des erreurs SBOM/Dependency-Track.
Architecture CI de gitrust
flowchart LR
A[git push] --> B[gitrust-hooks<br/>on_push]
B --> C[CiWorker async<br/>gitrust-core]
C --> D{CI_REMOTE_HOST}
D -->|localhost| E[Dagger Engine<br/>local]
D -->|runner distant| F[Runner SSH<br/>CI_REMOTE_HOST]
E --> G[Pipeline exécuté]
F --> G
G --> H[Logs stockés<br/>CI_WORKSPACE_PATH]
G --> I[Status → DB]
G -->|CI_SBOM_ENABLED=true| J[Syft → SBOM]
J -->|CI_DTRACK_ENABLED=true| K[Dependency-Track]
Vérifier l’état de la CI
Pipeline bloqué à l’état pending
Un pipeline reste en pending si le worker ne le prend pas en charge.
# Vérifier les logs du worker CI
sudo journalctl -u gitrust -n 100 --no-pager | grep -i "ci\|pipeline\|worker\|dagger"
# Vérifier que CI_ENABLED est bien à true
grep CI_ENABLED /opt/gitrust/.env
Si CI_ENABLED=false, aucun pipeline ne s’exécute. Pour désactiver temporairement la CI sans arrêter gitrust :
# Désactiver la CI (les pipelines s'accumulent en pending mais ne s'exécutent pas)
sed -i 's/^CI_ENABLED=.*/CI_ENABLED=false/' /opt/gitrust/.env
sudo systemctl restart gitrust
Nombre maximum de pipelines concurrentes atteint
grep CI_MAX_CONCURRENT /opt/gitrust/.env
# Défaut : 4 — augmenter si la machine a les ressources
Les pipelines supplémentaires attendent en file. Vérifiez qu’aucun pipeline n’est bloqué indéfiniment (timeout CI_DEFAULT_TIMEOUT, défaut : 3600 s).
Erreur : dagger: command not found
Le binaire dagger n’est pas dans le PATH de l’utilisateur gitrust.
# Vérifier depuis l'utilisateur gitrust
sudo -u gitrust which dagger
sudo -u gitrust dagger version
Installation de Dagger :
curl -fsSL https://dl.dagger.io/dagger/install.sh | sudo sh
# Le binaire est installé dans /usr/local/bin/dagger
Si Dagger est installé dans un chemin non standard, précisez-le dans .env :
CI_DAGGER_BIN=/usr/local/bin/dagger
Erreur : connexion au runner distant échoue
Si CI_REMOTE_HOST pointe vers une machine distante :
# Tester la connexion SSH depuis l'utilisateur gitrust
sudo -u gitrust ssh -i /opt/gitrust/data/ci_runner_key \
-p ${CI_REMOTE_SSH_PORT:-22} \
${CI_REMOTE_USER:-$USER}@${CI_REMOTE_HOST} \
"dagger version"
Causes courantes :
| Symptôme | Cause | Correction |
|---|---|---|
Permission denied (publickey) | Clé CI runner non autorisée sur le runner | Ajouter la clé publique dans ~/.ssh/authorized_keys du runner |
Connection refused | Le runner est éteint ou le port SSH est fermé | Vérifier l’état du runner et le firewall |
dagger: command not found sur le runner | Dagger non installé sur la machine distante | Installer Dagger sur le runner |
| Timeout rsync | CI_REMOTE_PATH inexistant sur le runner | ssh runner "mkdir -p /opt/gitrust-ci" |
Erreur SBOM : syft: command not found
# Vérifier depuis l'utilisateur gitrust
sudo -u gitrust which syft
# Installer Syft
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sudo sh -s -- -b /usr/local/bin
# Ou préciser le chemin dans .env
CI_SYFT_BIN=/usr/local/bin/syft
Si vous ne souhaitez pas utiliser SBOM :
CI_SBOM_ENABLED=false
CI_DTRACK_ENABLED=false
Erreur Dependency-Track : push SBOM échoue
# Vérifier la connectivité vers Dependency-Track
curl -s -H "X-Api-Key: ${CI_DTRACK_API_KEY}" \
"${CI_DTRACK_URL}/v1/project" | head -100
# Attendu : liste JSON des projets
# Erreur 401 : clé API invalide ou expirée
# Connection refused : CI_DTRACK_URL incorrect ou Dtrack non démarré
Vérifiez dans les logs gitrust :
sudo journalctl -u gitrust --since "1 hour ago" --no-pager \
| grep -i "dtrack\|sbom\|dependency"
Timeout de pipeline
Si un pipeline dépasse CI_DEFAULT_TIMEOUT (défaut : 3600 s = 1 h), il est annulé avec le statut timeout.
Pour les projets avec des builds très longs (compilation Rust complète, etc.) :
# Augmenter le timeout global
CI_DEFAULT_TIMEOUT=7200 # 2 heures
# Ou surcharger par pipeline dans .gitrust-ci.yml :
# pipeline:
# timeout: 7200
Nettoyer l’espace disque CI
Les artefacts et workspaces CI s’accumulent dans CI_WORKSPACE_PATH (défaut : /tmp/gitrust-ci) et les logs dans la base de données.
# Espace occupé par les workspaces CI
du -sh /tmp/gitrust-ci/
# Nettoyer manuellement (uniquement si aucun pipeline en cours)
sudo systemctl stop gitrust
sudo rm -rf /tmp/gitrust-ci/*
sudo systemctl start gitrust
Les logs en base sont purgés automatiquement selon CI_LOG_RETENTION_DAYS (défaut : 30 jours).
Désactiver la CI temporairement
Pour désactiver toute exécution CI sans arrêter gitrust (utile lors d’une maintenance) :
# Dans .env
CI_ENABLED=false
sudo systemctl restart gitrust
Les pipelines déclenchés pendant cette période restent en pending et peuvent être relancés une fois la CI réactivée.
Pour aller plus loin
Gérer les utilisateurs via le panel d’administration
À qui s’adresse cette page
Administrateurs qui gèrent les comptes utilisateurs de leur instance gitrust depuis l’interface /admin/users.
Accéder au panel utilisateurs
Connectez-vous avec un compte administrateur et accédez à :
https://VOTRE_INSTANCE/admin/users

La liste affiche pour chaque utilisateur : nom d’utilisateur, adresse e-mail, date de création, statut du compte (actif/désactivé), rôle (admin ou utilisateur standard), et si la vérification e-mail a été effectuée.
Créer un utilisateur
Si l’inscription publique est désactivée (allow_registration=false dans /admin/settings), seuls les administrateurs peuvent créer des comptes.
- Cliquez Nouvel utilisateur
- Renseignez : nom d’utilisateur, adresse e-mail, mot de passe temporaire
- Cochez Forcer le changement de mot de passe à la prochaine connexion si souhaité
- Cliquez Créer
L’utilisateur reçoit un e-mail de validation si EMAIL_VALIDATION_REQUIRED est activé.
Conseil : pour les instances privées (équipe interne), désactivez l’inscription publique dans
/admin/settingset créez les comptes manuellement pour garder le contrôle.
Désactiver un compte utilisateur
La désactivation empêche la connexion sans supprimer les données (dépôts, issues, commentaires restent intacts).
- Dans
/admin/users, cliquez sur l’utilisateur concerné - Cliquez Désactiver le compte
- Confirmez
Après désactivation :
- L’utilisateur ne peut plus se connecter (HTTP ou SSH)
- Ses dépôts restent accessibles aux autres membres des équipes
- La désactivation est réversible : cliquez Réactiver pour rétablir l’accès
Supprimer un compte utilisateur
La suppression est irréversible. Les dépôts dont l’utilisateur est propriétaire doivent être transférés ou supprimés au préalable.
- Désactivez d’abord le compte (étape ci-dessus)
- Cliquez Supprimer le compte
- Confirmez en saisissant le nom d’utilisateur
Les données supprimées : compte utilisateur, clés SSH, personal access tokens, sessions actives.
Les données conservées : dépôts transférés, issues/commentaires (auteur anonymisé).
Forcer la réinitialisation du mot de passe
Utile si un compte est suspecté d’être compromis ou si l’utilisateur a oublié son mot de passe.
- Ouvrez la fiche de l’utilisateur dans
/admin/users - Cliquez Forcer la réinitialisation du mot de passe
- L’utilisateur reçoit un e-mail avec un lien de reset (valable
PASSWORD_RESET_EXPIRATION_HOURSheures, défaut : 1 h)
Si le compte n’a pas d’adresse e-mail valide ou si le SMTP n’est pas configuré, définissez un nouveau mot de passe directement :
- Ouvrez la fiche utilisateur
- Cliquez Définir un nouveau mot de passe
- Saisissez le mot de passe temporaire et cochez Forcer le changement à la prochaine connexion
Forcer l’activation du 2FA
Pour exiger que tous les utilisateurs activent l’authentification à deux facteurs (TOTP), configurez le paramètre global dans /admin/settings :
- Accédez à
/admin/settings - Activez Forcer le 2FA pour tous les utilisateurs (
totp_force_enabled=true) - Enregistrez
Après activation, tout utilisateur sans 2FA configuré est redirigé vers la page de configuration TOTP à sa prochaine connexion. L’accès à l’application est bloqué jusqu’à la configuration.
Pour forcer le 2FA uniquement pour un utilisateur spécifique :
- Ouvrez la fiche de l’utilisateur dans
/admin/users - Cliquez Révoquer le 2FA (si actif mais compromis) pour forcer une reconfiguration
Gérer les permissions administrateur
Accorder les droits administrateur
- Ouvrez la fiche de l’utilisateur
- Cliquez Accorder le rôle administrateur
- Confirmez
Un administrateur a accès à /admin complet : gestion des utilisateurs, paramètres, équipes, logs d’audit.
Révoquer les droits administrateur
- Ouvrez la fiche de l’utilisateur
- Cliquez Révoquer le rôle administrateur
Vous ne pouvez pas révoquer vos propres droits administrateur. Un autre administrateur doit effectuer cette opération.
Droits sur les dépôts (niveau ressource)
Les droits sur les dépôts spécifiques (Reader, Developer, Maintainer) sont gérés au niveau de chaque dépôt ou via les équipes — pas depuis /admin/users. Voir le modèle de permissions pour les détails.
Pour aller plus loin
- Auditer l’instance — journal des actions admin
- Paramètres dynamiques —
allow_registration,totp_force_enabled - Schéma de base de données — table
users
Auditer une instance gitrust
À qui s’adresse cette page
Administrateurs qui souhaitent consulter le journal d’audit, filtrer les événements suspects, exporter les logs et les corréler avec les journaux système.
Le journal d’audit dans /admin/audit
Gitrust enregistre toutes les actions significatives dans la table audit_log. Chaque entrée contient : l’acteur (utilisateur), l’action effectuée, la ressource concernée, l’adresse IP source et l’horodatage.
Les actions auditées par défaut (audit_log_actions dans app_settings) :
create— création d’un dépôt, utilisateur, équipe, etc.update— modification de paramètres, de profildelete— suppression d’une ressourcereset_password— réinitialisation de mot de passe
Accédez au journal : https://VOTRE_INSTANCE/admin/audit

Filtrer les événements
L’interface /admin/audit propose des filtres combinables :
| Filtre | Description |
|---|---|
| Utilisateur | Filtrer par nom d’utilisateur de l’acteur |
| Action | Filtrer par type d’action (create, update, delete, reset_password) |
| Période | Sélectionner une plage de dates |
| Ressource | Filtrer par type de ressource (repository, user, team…) |
Exemple de requête typique : « toutes les suppressions de dépôts depuis 30 jours »
- Action :
delete - Période : 30 derniers jours
- Cliquez Filtrer
Exporter les logs en CSV
- Appliquez les filtres souhaités
- Cliquez Exporter CSV
- Le fichier téléchargé contient les colonnes :
timestamp,actor_username,action,resource_type,resource_id,ip_address,details
Ce fichier peut être importé dans un tableur ou un SIEM pour des analyses avancées.
Interroger la base de données directement
Pour des requêtes avancées non disponibles dans l’UI :
-- Connexions de l'administrateur sur les 7 derniers jours
SELECT
al.created_at,
u.username,
al.action,
al.resource_type,
al.ip_address
FROM audit_log al
JOIN users u ON al.actor_id = u.id
WHERE u.username = 'admin'
AND al.created_at > NOW() - INTERVAL '7 days'
ORDER BY al.created_at DESC;
-- Suppressions massives (plus de 5 suppressions par utilisateur sur 1 heure)
SELECT
u.username,
COUNT(*) AS nb_suppressions,
MIN(al.created_at) AS premiere,
MAX(al.created_at) AS derniere
FROM audit_log al
JOIN users u ON al.actor_id = u.id
WHERE al.action = 'delete'
AND al.created_at > NOW() - INTERVAL '1 hour'
GROUP BY u.username
HAVING COUNT(*) > 5
ORDER BY nb_suppressions DESC;
Configurer le niveau d’audit
Le niveau et les actions auditées sont des paramètres dynamiques modifiables via /admin/settings :
| Paramètre | Valeur | Description |
|---|---|---|
audit_log_level | INFO | Niveau de détail (INFO, WARN, ERROR) |
audit_log_actions | JSON array | Actions enregistrées |
Pour ajouter l’action login aux événements audités :
- Accédez à
/admin/settings - Trouvez
audit_log_actions - Ajoutez
"login"au tableau JSON :["create","update","delete","reset_password","login"] - Enregistrez (effet immédiat, pas de redémarrage nécessaire)
Corréler avec journalctl
Les logs systemd de gitrust complètent le journal d’audit en capturant les erreurs techniques et les événements bas niveau (connexions SSH refusées, erreurs de migration, etc.).
# Logs gitrust des 24 dernières heures
sudo journalctl -u gitrust --since "24 hours ago" --no-pager
# Filtrer les erreurs et avertissements uniquement
sudo journalctl -u gitrust -p err..warning --since "24 hours ago" --no-pager
# Corréler avec une adresse IP spécifique
sudo journalctl -u gitrust --since "24 hours ago" --no-pager \
| grep "192.168.1.100"
# Suivre les logs en temps réel
sudo journalctl -u gitrust -f
Signaux d’alerte à surveiller
# Tentatives d'authentification SSH échouées répétées
sudo journalctl -u gitrust --since "1 hour ago" --no-pager \
| grep -i "auth.*fail\|invalid.*key\|permission denied" \
| wc -l
# Réponses 429 (rate limiting déclenché)
sudo journalctl -u gitrust --since "1 hour ago" --no-pager \
| grep "429" | awk '{print $NF}' | sort | uniq -c | sort -rn | head
Conservation des logs
La rétention des logs d’audit en base de données n’a pas de limite automatique dans la version actuelle — les entrées s’accumulent indéfiniment. Pour une instance à long terme, planifiez une purge périodique :
-- Supprimer les entrées d'audit de plus de 1 an (hors suppressions et reset mdp)
DELETE FROM audit_log
WHERE created_at < NOW() - INTERVAL '1 year'
AND action NOT IN ('delete', 'reset_password');
Vérifiez les exigences de conservation de votre organisation avant de purger (RGPD, ANSSI PA-074 recommande 12 mois minimum pour les événements de sécurité).
Pour aller plus loin
- Conformité ANSSI PA-074 — journalisation
- Schéma de base de données — table
audit_log - Paramètres dynamiques —
audit_log_level,audit_log_actions
Référence — Manuel d’administration
Les pages de référence sont des tableaux factuels exhaustifs. Consultez-les quand vous cherchez une valeur précise, une définition, ou une liste complète. Elles ne contiennent pas d’explications pédagogiques.
Pages de référence
Variables d’environnement
Tableau exhaustif de toutes les variables du fichier .env, groupées par thème : base de données, serveur HTTP, SSH, JWT, sessions, rate limiting, SMTP, dépôts Git, CI/CD, SBOM, import, bootstrap admin, OAuth. Pour chaque variable : nom, valeur par défaut, caractère obligatoire, description et impact sécurité.
Utilisez cette page quand : vous ne connaissez pas le nom exact d’une variable, vous voulez vérifier la valeur par défaut, ou vous provisionnez une nouvelle instance.
Paramètres dynamiques (/admin/settings)
Référence des paramètres modifiables à chaud depuis l’interface /admin/settings (table app_settings). Contrairement aux variables .env, ces paramètres sont effectifs immédiatement sans redémarrage. Inclut les règles de précédence entre .env et base de données.
Utilisez cette page quand : vous cherchez à modifier un comportement fonctionnel (inscription ouverte, 2FA obligatoire, politique de mots de passe) sans redémarrer le service.
Schéma de la base de données
Diagramme ERD Mermaid + description de toutes les tables principales : users, repositories, ssh_keys, teams, personal_access_tokens, issues, pull_requests, labels, app_settings, audit_log. Colonnes, types, contraintes, index, clés étrangères, modèle de permissions.
Utilisez cette page quand : vous construisez une requête SQL directe sur la base, vous voulez comprendre les relations entre entités, ou vous évaluez l’impact d’une suppression de données.
Ports et services
Tableau de tous les ports réseau : 4000 (HTTP), 2222 (SSH Git), 5432 (PostgreSQL), 6379 (Redis), 25/465/587 (SMTP sortant). Binding recommandé par topologie, règles firewall, configuration nginx stream proxy pour SSH sur port 22.
Utilisez cette page quand : vous configurez un firewall, vous vérifiez qu’un port n’est pas exposé à tort, ou vous planifiez un déploiement derrière un reverse-proxy.
Conformité ANSSI PA-074
Checklist exhaustive des critères ANSSI PA-074 appliqués à gitrust : authentification, chiffrement, protection contre les injections, gestion des secrets, journalisation, gestion des accès, durcissement système. Pour chaque point : statut dans gitrust et action requise de l’administrateur.
Utilisez cette page quand : vous préparez un audit de sécurité, vous durcissez une instance pour une utilisation en milieu sensible, ou vous documentez votre conformité.
Navigation
- Vous cherchez à accomplir une tâche concrète → Guides pratiques (how-to)
- Vous voulez comprendre pourquoi → Explanations
- Vous démarrez depuis zéro → Tutoriels
Variables d’environnement
Référence exhaustive de toutes les variables d’environnement de gitrust. Ces variables se définissent dans le fichier .env (ou .env.production) et sont chargées une seule fois au démarrage. Modifier le fichier sans redémarrer le service n’a aucun effet.
Principe d’autonomie : ce tableau contient intégralement le contenu documenté de
.env.example. La documentation ne référence pas le fichier source par lien — tout est ici.
Les variables marquées SEC ont des contraintes de sécurité vérifiées au démarrage : gitrust refuse de démarrer si elles ne sont pas conformes.
1. Base de données
| Variable | Défaut | Obligatoire | Description |
|---|---|---|---|
DATABASE_URL | — | Oui | URL de connexion PostgreSQL. Format : postgres://USER:PASSWORD@HOST:PORT/DB. Le mot de passe doit être fort en production. PostgreSQL ne doit jamais binder sur 0.0.0.0 en production. |
Exemples :
# Développement local (Docker Compose)
DATABASE_URL=postgres://gitrust:gitrust@localhost:5432/gitrust
# Production (réseau privé)
DATABASE_URL=postgres://gitrust:MOT_DE_PASSE_FORT@db.internal:5432/gitrust
2. Serveur HTTP
| Variable | Défaut | Description |
|---|---|---|
SERVER_HOST | 0.0.0.0 | Adresse d’écoute HTTP. En production derrière un reverse-proxy : 127.0.0.1. En développement : 0.0.0.0. |
SERVER_PORT | 3000 | Port HTTP. Convention gitrust : 4000 en production. |
3. Logging
| Variable | Défaut | Description |
|---|---|---|
RUST_LOG | debug | Niveau de log (syntaxe tracing_subscriber::EnvFilter). Valeurs : error, warn, info, debug, trace. Syntaxe avancée : "info,gitrust_core=debug,sea_orm=warn". |
4. Serveur SSH Git
| Variable | Défaut | Description |
|---|---|---|
SSH_HOST_KEY_PATH | ./data/ssh_host_ed25519_key | Chemin de la clé hôte SSH Ed25519. Générée automatiquement au premier démarrage si absente. SEC : permissions 600, propriétaire = utilisateur systemd. En production : chemin absolu. |
SSH_PORT | 2222 | Port d’écoute du serveur SSH Russh (intégré, distinct de sshd). |
SSH_LISTEN_ADDR | 0.0.0.0 | Adresse d’écoute SSH. 127.0.0.1 si derrière un proxy nginx stream. |
SSH_PUBLIC_HOST | localhost | Nom d’hôte affiché dans les URLs de clone SSH dans l’UI. En production : FQDN public. |
MAX_SSH_KEY_SIZE | 16384 | Taille maximale d’une clé SSH publique acceptée (octets). |
5. JWT et authentification
| Variable | Défaut | Description |
|---|---|---|
JWT_SECRET | — | SEC OBLIGATOIRE — Secret de signature JWT. Minimum 32 bytes. Générer avec openssl rand -hex 64. Les valeurs exemples (change-me-in-production, etc.) sont explicitement rejetées au démarrage. Changer cette valeur invalide toutes les sessions actives. |
JWT_EXPIRATION_MINUTES | 15 | Durée de vie d’un JWT d’accès (minutes). |
JWT_ISSUER | mon-app | Valeur du claim iss dans le JWT. En production : FQDN public. |
REFRESH_TOKEN_EXPIRATION_DAYS | 7 | Durée du refresh token (jours). Rotation à chaque usage. |
REMEMBER_ME_EXPIRATION_DAYS | 30 | Durée étendue quand « Se souvenir de moi » est coché. SEC : doit être ≥ REFRESH_TOKEN_EXPIRATION_DAYS et ≤ 90 jours. Au-delà de 60 jours : warning dans les logs. |
6. Sessions
| Variable | Défaut | Description |
|---|---|---|
SESSION_TIMEOUT_MINUTES | 30 | Timeout d’inactivité d’une session (minutes). |
SESSION_BACKEND | seaorm | Backend de stockage : memory (volatil, dev uniquement), seaorm (PostgreSQL, recommandé), redis (multi-instances, nécessite REDIS_URL). |
REDIS_URL | — | URL Redis (obligatoire si SESSION_BACKEND=redis). Exemple : redis://localhost:6379. |
7. Rate limiting
| Variable | Défaut | Description |
|---|---|---|
RATE_LIMIT_LOGIN_PER_MINUTE | 5 | Tentatives de connexion par IP par minute. Protection contre le brute force. |
RATE_LIMIT_REFRESH_PER_MINUTE | 10 | Rafraîchissements de token par IP par minute. |
RATE_LIMIT_GENERAL_PER_MINUTE | 100 | Limite globale sur tous les autres endpoints. 0 = désactivé (déconseillé). |
8. Cookies et debug
| Variable | Défaut | Description |
|---|---|---|
APP_DEBUG | false | SEC : true = logs verbeux, cookies non sécurisés (HTTP local). false = production, enforce COOKIE_SECURE=true même si défini à false. Ne jamais mettre true en production. |
COOKIE_SECURE | false | Cookies envoyés uniquement via HTTPS. SEC : forcé à true si APP_DEBUG=false. |
COOKIE_SAME_SITE | Lax | Politique SameSite. Strict (le plus sûr, casse les redirections OAuth cross-site), Lax (compatible OAuth), None (nécessite Secure=true). |
9. Bootstrap admin initial
| Variable | Défaut | Description |
|---|---|---|
ADMIN_USERNAME | admin | Nom d’utilisateur du compte admin créé au premier démarrage (si la table users est vide). 3-64 caractères alphanumériques + _-. |
ADMIN_EMAIL | admin@example.com | Adresse e-mail du compte admin initial. Doit contenir @ + domaine avec .. |
ADMIN_PASSWORD | — | SEC OBLIGATOIRE — Mot de passe du compte admin initial. Minimum 8 caractères. À remplacer immédiatement après le premier démarrage. |
CREATE_DEFAULT_ADMIN | true | Mettre à false une fois d’autres admins créés pour désactiver la création automatique. |
10. Email SMTP
| Variable | Défaut | Description |
|---|---|---|
SMTP_HOST | — | Serveur SMTP. Obligatoire pour activer les e-mails. Sans cette variable, gitrust démarre mais les fonctionnalités e-mail sont désactivées. |
SMTP_PORT | 587 | Port SMTP : 25 (non chiffré, interdit en prod), 465 (TLS natif), 587 (STARTTLS, recommandé), 1025 (dev/Mailpit). |
SMTP_USER | — | Identifiant SMTP. Souvent = adresse e-mail expéditrice. |
SMTP_PASSWORD | — | Mot de passe applicatif SMTP (pas le mot de passe du compte e-mail). |
SMTP_FROM | — | Adresse expéditeur. Doit être valide (validée par is_valid_email). En prod : valider DKIM/SPF sur le domaine. |
SMTP_FROM_NAME | — | Nom affiché dans le champ « De : » des e-mails. |
SMTP_USE_TLS | true | Activer TLS natif (port 465). |
SMTP_USE_STARTTLS | true | Activer STARTTLS (port 587). SEC : ne jamais désactiver les deux en production. |
SMTP_ACCEPT_INVALID_CERTS | false | SEC : accepter les certificats SMTP invalides. Warning au démarrage si true. Uniquement pour les serveurs de mail internes en développement. |
SMTP_CA_CERT_PATH | — | Chemin vers un CA personnalisé (PEM) pour valider le certificat SMTP. |
EMAIL_BASE_URL | http://localhost:3000 | URL de base pour les liens dans les e-mails. Obligatoire en prod sinon les liens pointent vers localhost. Doit commencer par http:// ou https://. |
EMAIL_QUEUE_BATCH_SIZE | 10 | Nombre d’e-mails traités par tick du worker. |
EMAIL_QUEUE_RETRY_ATTEMPTS | 5 | Tentatives d’envoi avant abandon. |
EMAIL_QUEUE_RETRY_DELAY_SECONDS | 300 | Délai entre deux tentatives (secondes). |
EMAIL_QUEUE_INTERVAL_SECS | 30 | Intervalle du tick du worker e-mail (secondes). |
EMAIL_VALIDATION_REQUIRED | true | Exiger la validation e-mail à l’inscription. Note : cette variable est documentée dans .env.example mais la valeur effective est contrôlée par le paramètre dynamique validation_email_required en base de données. |
11. Inscription
| Variable | Défaut | Description |
|---|---|---|
ALLOW_REGISTRATION | false | Autoriser l’inscription publique. Note : valeur effective contrôlée par le paramètre dynamique allow_registration en base de données — voir Paramètres dynamiques. |
12. Application (branding)
| Variable | Défaut | Description |
|---|---|---|
APP_NAME | Mon Application | Nom affiché dans l’UI, les e-mails et la balise <title>. |
APP_THEME | light | Thème visuel. Valeurs : light, dark. |
DEFAULT_LOCALE | fr | Langue par défaut. Valeurs : fr, en, de, es, pt, it. |
13. IMAP — traitement des bounces (optionnel)
Si IMAP_HOST est vide, la fonctionnalité est désactivée sans erreur.
| Variable | Défaut | Description |
|---|---|---|
IMAP_HOST | — | Serveur IMAP. Obligatoire pour activer le traitement des bounces. |
IMAP_PORT | 993 | Port IMAP : 993 (TLS direct, recommandé), 143 (STARTTLS). |
IMAP_USER | = SMTP_USER | Identifiant IMAP. |
IMAP_PASSWORD | = SMTP_PASSWORD | Mot de passe IMAP. |
IMAP_USE_TLS | true | TLS direct (port 993). |
IMAP_USE_STARTTLS | false | STARTTLS après connexion (port 143). |
IMAP_ACCEPT_INVALID_CERTS | false | Accepter les certificats invalides (warning si true). |
IMAP_MAILBOX | INBOX | Boîte à scanner pour les bounces. |
IMAP_POLL_INTERVAL_SECONDS | 300 | Intervalle de poll (secondes). |
IMAP_SOCKET_TIMEOUT_SECS | 30 | Timeout socket read/write. |
14. Tâches de fond — intervalles et expirations
Ces variables ont des valeurs par défaut raisonnables. Ne les ajustez que si vous avez un besoin spécifique.
| Variable | Défaut | Description |
|---|---|---|
PASSWORD_CHANGE_RETRY_INTERVAL_SECS | 600 | Retry e-mail changement de mot de passe (10 min). |
PASSWORD_CHANGE_RETRY_GAP_MINUTES | 20 | Espacement minimum entre deux retries. |
PASSWORD_CHANGE_MAX_RETRIES | 3 | Abandon après N échecs. |
PASSWORD_CHANGE_CLEANUP_INTERVAL_SECS | 86400 | Nettoyage quotidien des demandes expirées. |
PASSWORD_CHANGE_EXPIRATION_HOURS | 24 | Validité d’une demande de changement de mot de passe. |
PASSWORD_CHANGE_RATE_LIMIT_MAX | 3 | Demandes maximum par fenêtre. |
PASSWORD_CHANGE_RATE_LIMIT_WINDOW_SECS | 3600 | Fenêtre de rate limit (1 heure). |
PASSWORD_RESET_EXPIRATION_HOURS | 1 | Validité du lien de réinitialisation de mot de passe. |
EMAIL_VALIDATION_EXPIRATION_HOURS | 24 | Validité du lien de validation e-mail. |
JWT_CLEANUP_INTERVAL_SECS | 3600 | Purge de la blacklist JWT expirés. |
COOKIE_PREFERENCE_MAX_AGE_SECS | 31536000 | Durée de vie du cookie de préférences (locale/thème) — 1 an. |
PAGINATION_DEFAULT_PER_PAGE | 50 | Taille par défaut des pages de liste. |
PAGINATION_MAX_PER_PAGE | 100 | Taille maximale des pages de liste. |
15. Dépôts Git
| Variable | Défaut | Description |
|---|---|---|
GIT_REPOS_BASE_PATH | ./data/repos | Chemin des dépôts bare. Structure : {base}/{owner}/{repo}.git. En production : chemin absolu (ex. /opt/gitrust/data/repos). SEC : path traversal bloqué au niveau service. |
16. Fichiers statiques
| Variable | Défaut | Description |
|---|---|---|
STATIC_FILES_PATH | ./static | Chemin du dossier des assets statiques (CSS, JS, images). En production avec systemd : chemin absolu (ex. /opt/gitrust/static). |
17. CI/CD Dagger
| Variable | Défaut | Description |
|---|---|---|
CI_ENABLED | true | Activation globale de la CI. false = plus aucune exécution de pipeline, mais l’UI reste accessible. |
CI_DAGGER_BIN | dagger | Chemin du binaire Dagger. En production : chemin absolu recommandé. |
CI_ENGINE_PATH | ./deployment/ci-engine | Chemin du module CI (Easy Mode — interprète .gitrust-ci.yml). |
CI_MAX_CONCURRENT | 4 | Pipelines exécutés simultanément. Au-delà : file d’attente. |
CI_DEFAULT_TIMEOUT | 3600 | Timeout par défaut d’un pipeline (secondes). Surchargeable par pipeline. |
CI_WORKSPACE_PATH | /tmp/gitrust-ci | Dossier de travail temporaire CI (checkouts, artefacts). En production : /var/lib/gitrust-ci. |
CI_LOG_RETENTION_DAYS | 30 | Rétention des logs de pipeline (jours). |
18. Runner CI distant (SSH + rsync)
| Variable | Défaut | Description |
|---|---|---|
CI_REMOTE_HOST | localhost | Host du runner CI. localhost = CI sur la même machine. |
CI_REMOTE_USER | $USER | Utilisateur SSH sur le runner. |
CI_REMOTE_PATH | /opt/gitrust-ci | Chemin de travail sur le runner. |
CI_REMOTE_SSH_KEY | — | Chemin de la clé privée SSH pour le runner. Optionnel si ssh-agent est chargé. |
CI_REMOTE_SSH_PORT | 22 | Port SSH du runner. |
19. Import de dépôts externes
| Variable | Défaut | Description |
|---|---|---|
IMPORT_MAX_CONCURRENT | 2 | Nombre maximum de clones concurrents. |
IMPORT_TIMEOUT_SECS | 1800 | Timeout par import (secondes). Augmenter pour les gros monorepos. |
DB_WORKER_POOL_SIZE | 4 | Pool de connexions DB dédié au worker d’import. Évite la saturation du pool HTTP principal. |
20. SBOM / Dependency-Track
| Variable | Défaut | Description |
|---|---|---|
CI_SBOM_ENABLED | false | Générer un SBOM (Software Bill of Materials) à chaque pipeline via Syft. Nécessite syft installé. |
CI_SYFT_BIN | syft | Chemin du binaire Syft. |
CI_DTRACK_ENABLED | false | Pousser le SBOM vers Dependency-Track (nécessite CI_SBOM_ENABLED=true). |
CI_DTRACK_URL | — | URL API de Dependency-Track (ex. https://dtrack.internal/api). |
CI_DTRACK_API_KEY | — | Clé API Dependency-Track (générer dans l’UI Dtrack : Administration > API keys). |
CI_DTRACK_FRONTEND_URL | — | URL UI Dependency-Track. Si absent : déduite de CI_DTRACK_URL. |
21. OAuth / SSO
| Variable | Défaut | Description |
|---|---|---|
OAUTH_ENCRYPTION_KEY | — | Clé de chiffrement AES-256-GCM des secrets OAuth en base. Générer avec openssl rand -hex 32. |
OAUTH_ENABLED | false | Activation globale OAuth. Fallback si pas de valeur en base. |
OAUTH_REDIRECT_BASE_URL | http://localhost:3000 | URL de base pour les callbacks OAuth : {URL}/api/v1/auth/oauth/{provider}/callback. |
OAUTH_GOOGLE_ENABLED | false | Activer Google OAuth. |
OAUTH_GOOGLE_CLIENT_ID | — | Client ID Google. |
OAUTH_GOOGLE_CLIENT_SECRET | — | Client Secret Google. |
OAUTH_GITHUB_ENABLED | false | Activer GitHub OAuth. |
OAUTH_GITHUB_CLIENT_ID | — | Client ID GitHub. |
OAUTH_GITHUB_CLIENT_SECRET | — | Client Secret GitHub. |
OAUTH_DISCORD_ENABLED | false | Activer Discord OAuth. |
OAUTH_DISCORD_CLIENT_ID | — | Client ID Discord. |
OAUTH_DISCORD_CLIENT_SECRET | — | Client Secret Discord. |
OAUTH_MICROSOFT_ENABLED | false | Activer Microsoft / Azure AD OAuth. |
OAUTH_MICROSOFT_CLIENT_ID | — | Client ID Azure (Application ID). |
OAUTH_MICROSOFT_CLIENT_SECRET | — | Client Secret Azure. |
OAUTH_MICROSOFT_TENANT | common | Tenant Azure AD : "common" (comptes perso + pro), "organizations" (pro uniquement), ou GUID du tenant. |
22. Durcissement SSH (SSH_GUARD_*)
Variables consommées par la crate gitrust-ssh-guard. Toutes sont préfixées SSH_GUARD_. Pour le détail des comportements, voir Configurer ssh-guard et ssh-guard : détection d’attaques SSH.
Modèle de configuration :
SSH_GUARD_PROFILEsélectionne un preset cohérent (direct/nginx/haproxy/private/custom). Chaque autre variable override individuellement le défaut du preset.
22.1 Kill switch et profil
| Variable | Défaut | Description |
|---|---|---|
SSH_GUARD_ENABLED | true | false = pass-through complet, ssh-guard ne fait rien. À éviter sauf urgence. |
SSH_GUARD_DRY_RUN | false | true = les détecteurs tournent et émettent les événements ip_banned, mais aucun ban n’est persisté. Idéal pour valider un nouveau seuil. |
SSH_GUARD_PROFILE | custom | Preset : direct (Internet direct), nginx (derrière nginx stream + PROXY v2), haproxy (derrière HAProxy), private (réseau interne, détecteurs OFF), custom (aucun preset). |
22.2 PROXY protocol
| Variable | Défaut | Description |
|---|---|---|
SSH_GUARD_PROXY_PROTOCOL | disabled | Mode parsing PROXY : disabled, v1 (texte HAProxy legacy), v2 (binaire nginx stream / HAProxy moderne), any (auto-détection v1/v2). |
SSH_GUARD_PROXY_PROTOCOL_STRICT | true | true = drop si en-tête PROXY absent ou invalide quand le mode est activé. false = fallback sur peer_addr avec warn (utile en transition). |
SSH_GUARD_PROXY_PROTOCOL_TIMEOUT_MS | 3000 | Timeout (ms) pour lire l’en-tête PROXY après accept TCP. |
SSH_GUARD_TRUSTED_PROXIES | — | Obligatoire si PROXY activé. Liste CIDR (séparée par virgules) des proxies autorisés à émettre un en-tête PROXY. Sans cette protection, n’importe qui pourrait forger une IP cliente. Exemple : 127.0.0.1/32,::1/128. |
22.3 Détecteurs (seuils + fenêtres)
| Variable | Défaut | Description |
|---|---|---|
SSH_GUARD_BRUTE_FORCE_THRESHOLD | 5 | Nombre d’échecs d’auth depuis une IP avant ban auto. 4294967295 (u32::MAX) = détecteur désactivé. |
SSH_GUARD_BRUTE_FORCE_WINDOW_SECS | 300 | Fenêtre glissante (secondes). |
SSH_GUARD_USER_ENUM_THRESHOLD | 10 | Nombre d’usernames distincts essayés depuis une IP. |
SSH_GUARD_USER_ENUM_WINDOW_SECS | 300 | |
SSH_GUARD_KEY_SCAN_THRESHOLD | 10 | Nombre de fingerprints de clé distincts essayés depuis une IP. |
SSH_GUARD_KEY_SCAN_WINDOW_SECS | 300 | |
SSH_GUARD_CONN_FLOOD_PER_SEC | 10 | Cap dur de nouvelles connexions TCP par IP par seconde. 0 ou u32::MAX = désactivé. |
SSH_GUARD_CONN_FLOOD_BURST | 20 | Burst autorisé au-dessus du cap soutenu. |
SSH_GUARD_MAX_CONCURRENT_PER_IP | 10 | Limite de sessions concurrentes par IP (placeholder, non encore appliqué). |
22.4 Ban
| Variable | Défaut | Description |
|---|---|---|
SSH_GUARD_AUTO_BAN_DURATION_SECS | 3600 | TTL des bans posés par les détecteurs. 0 = ban permanent. |
22.5 Stockage
| Variable | Défaut | Description |
|---|---|---|
SSH_GUARD_STORE_BACKEND | hybrid | memory (DashMap, perdu au restart), postgres (chaque write en DB), hybrid (RAM chaude + write-through DB + rehydrate au boot — défaut recommandé). |
SSH_GUARD_STORE_FLUSH_INTERVAL_MS | 1000 | Intervalle entre deux flushs RAM → Postgres pour le mode hybrid. |
SSH_GUARD_EVENTS_RETENTION_DAYS | 90 | Rétention de la table ssh_guard_events (utilisée par les détecteurs). Au-delà, les lignes sont purgées. |
22.6 Observabilité
| Variable | Défaut | Description |
|---|---|---|
SSH_GUARD_LOG_FORMAT | json | json (stable, fail2ban-friendly) ou text (debug uniquement). |
SSH_GUARD_LOG_TARGET | stderr | stderr (via tracing → journald), file (fichier dédié uniquement), both (les deux). |
SSH_GUARD_LOG_FILE | /var/log/gitrust-ssh-guard.json | Chemin du fichier dédié si LOG_TARGET=file ou both. Logrotate quasi obligatoire (sinon le fichier grossit sans limite). |
SSH_GUARD_METRICS_ENABLED | true | Expose les métriques Prometheus (placeholder, exposition future). |
22.7 Validations bloquantes au démarrage
gitrust refuse de démarrer si :
SSH_GUARD_PROXY_PROTOCOLest activé sansSSH_GUARD_TRUSTED_PROXIES(forge d’IP triviale sinon).SSH_GUARD_LOG_TARGET=fileoubothsansSSH_GUARD_LOG_FILEvalide.
Le message d’erreur identifie la variable fautive.
Récapitulatif local vs production
| Dimension | Local (dev) | Production |
|---|---|---|
DATABASE_URL host | localhost | Réseau privé |
SERVER_HOST | 0.0.0.0 | 127.0.0.1 (derrière nginx) |
SERVER_PORT | 3000 ou 4000 | 4000 (nginx devant) |
SSH_LISTEN_ADDR | 0.0.0.0 | 127.0.0.1 (nginx stream) |
SSH_PUBLIC_HOST | localhost | FQDN public |
COOKIE_SECURE | false (HTTP) | true (HTTPS, forcé) |
APP_DEBUG | true | false (force sécurité) |
EMAIL_BASE_URL | http://localhost:... | https://FQDN |
JWT_SECRET | Valeur courte OK | openssl rand -hex 64 |
ADMIN_PASSWORD | Faible accepté | Fort (≥ 16 caractères) |
ALLOW_REGISTRATION | true | false (instance fermée) |
RUST_LOG | debug | info ou warn |
GIT_REPOS_BASE_PATH | Relatif (./data) | Absolu (/opt/gitrust) |
Pour aller plus loin
- Paramètres dynamiques
/admin/settings— variables modifiables à chaud sans redémarrage - Tutoriel 02 — Installation systemd — exemple de
.envminimal de production - Configurer ssh-guard — recettes par profil de déploiement
- Événements ssh-guard (JSON) — schéma des événements émis
Événements ssh-guard (JSON)
Référence stable du flux d’événements émis par la couche ssh-guard du serveur SSH gitrust. Ce format est garanti stable pour les consommateurs externes (fail2ban, Loki, Vector, SIEM). Tout changement incompatible passe par un nouveau nom d’événement.
Où trouver ces événements : selon
SSH_GUARD_LOG_TARGET, dans le journald du service (stderr), dans/var/log/gitrust-ssh-guard.json(file), ou les deux (both). Voir Variables d’environnement — SSH_GUARD_*.
1. Forme générale
Tous les événements partagent trois champs :
| Champ | Type | Description |
|---|---|---|
event | string | Nom snake_case du type d’événement (clé de filtrage). |
ts | string ISO 8601 UTC | Horodatage UTC de l’événement (ex. 2026-04-19T14:32:11.482Z). |
ip | string | Adresse IPv4 ou IPv6 réelle du client (après extraction PROXY si applicable). |
Champs additionnels selon le type d’événement (voir tables ci-dessous).
2. Catalogue des événements
event | Catégorie | Émis par | Cas d’usage admin |
|---|---|---|---|
connection_accepted | Trafic | SecureListener | Audit volume |
connection_dropped | Décision | SecureListener ou ConnectionFloodDetector | Volumétrie des refus |
auth_failed | Authentification | AuthTracker | Source principale fail2ban |
auth_succeeded | Authentification | AuthTracker | Audit accès légitimes |
brute_force_detected | Détection | BruteForceDetector | Signal fort fail2ban |
user_enumeration_detected | Détection | UserEnumerationDetector | Signal fort fail2ban |
key_scanning_detected | Détection | KeyScanningDetector | Signal fort fail2ban |
connection_flood_detected | Détection | ConnectionFloodDetector | Signal fort fail2ban |
ip_banned | Action | BanManager | Ban à appliquer côté firewall (fail2ban) |
ip_unbanned | Action | BanManager | Levée de ban (manuelle ou TTL) |
3. Référence par événement
3.1 connection_accepted
Une connexion TCP a passé tous les contrôles ssh-guard. Le russh handshake va démarrer.
| Champ | Type | Description |
|---|---|---|
event | "connection_accepted" | |
ts | string | Horodatage UTC |
session_id | string UUID v4 | Identifiant unique de session, présent ensuite dans tous les événements liés (auth_failed, auth_succeeded) |
ip | string | IP cliente réelle |
{"event":"connection_accepted","ts":"2026-04-19T14:32:11.482Z","session_id":"a4c2b8e1-9f3d-4d7e-8c11-0a5d9b6f4e22","ip":"203.0.113.42"}
3.2 connection_dropped
ssh-guard a refusé la connexion avant le handshake SSH.
| Champ | Type | Description |
|---|---|---|
event | "connection_dropped" | |
ts | string | Horodatage UTC |
ip | string | IP source (réelle si PROXY parsé, sinon peer_addr) |
reason | enum string | Voir tableau des raisons ci-dessous |
Valeurs de reason :
reason | Signification |
|---|---|
banned | IP couverte par un ban actif (auto ou denylist admin) |
flood_limit | Cap de connexions/seconde par IP atteint |
proxy_header_missing | PROXY protocol obligatoire mais en-tête absent (timeout) |
untrusted_proxy | En-tête PROXY reçu d’un socket pas dans trusted_proxies |
proxy_header_invalid | En-tête PROXY malformé ou version non autorisée |
concurrent_limit | Limite de sessions concurrentes par IP atteinte (placeholder, non actif) |
{"event":"connection_dropped","ts":"2026-04-19T14:33:02.117Z","ip":"203.0.113.42","reason":"banned"}
3.3 auth_failed
Tentative d’authentification SSH refusée. Source principale pour fail2ban.
| Champ | Type | Description |
|---|---|---|
event | "auth_failed" | |
ts | string | Horodatage UTC |
session_id | string UUID | Lien avec le connection_accepted |
ip | string | IP cliente réelle |
user | string ou null | Nom d’utilisateur tenté (si fourni par le client) |
method | enum string | Méthode SSH : none, password, public_key, keyboard_interactive, host_based |
fingerprint | string ou null | Fingerprint SHA256 de la clé publique tentée (forme SHA256:...) ou null si non public_key |
{"event":"auth_failed","ts":"2026-04-19T14:32:13.221Z","session_id":"a4c2b8e1-9f3d-4d7e-8c11-0a5d9b6f4e22","ip":"203.0.113.42","user":"root","method":"public_key","fingerprint":"SHA256:k1Qp9xJ8r6Z3HfV2Bn7tT5Cw"}
3.4 auth_succeeded
Tentative d’authentification SSH validée. Audit des accès légitimes.
| Champ | Type | Description |
|---|---|---|
event | "auth_succeeded" | |
ts | string | Horodatage UTC |
session_id | string UUID | |
ip | string | |
user | string | Nom d’utilisateur authentifié |
fingerprint | string ou null | Fingerprint de la clé utilisée (peut être null si méthode sans clé) |
{"event":"auth_succeeded","ts":"2026-04-19T14:32:14.005Z","session_id":"a4c2b8e1-9f3d-4d7e-8c11-0a5d9b6f4e22","ip":"203.0.113.42","user":"alice","fingerprint":"SHA256:p3Lm7nB6xQz9fK1WyT8c"}
3.5 brute_force_detected
Le seuil SSH_GUARD_BRUTE_FORCE_THRESHOLD a été atteint. Un événement ip_banned suit immédiatement.
| Champ | Type | Description |
|---|---|---|
event | "brute_force_detected" | |
ts | string | Horodatage UTC |
ip | string | IP fautive |
count | number | Nombre d’auth_failed comptés dans la fenêtre |
window_secs | number | Largeur de la fenêtre (en secondes) |
{"event":"brute_force_detected","ts":"2026-04-19T14:36:42.998Z","ip":"203.0.113.42","count":5,"window_secs":300}
3.6 user_enumeration_detected
Le seuil SSH_GUARD_USER_ENUM_THRESHOLD (nombre d’usernames distincts essayés depuis la même IP) a été atteint.
| Champ | Type | Description |
|---|---|---|
event | "user_enumeration_detected" | |
ts | string | Horodatage UTC |
ip | string | |
distinct_users | number | Nombre d’usernames distincts dans la fenêtre |
window_secs | number |
{"event":"user_enumeration_detected","ts":"2026-04-19T14:38:15.402Z","ip":"203.0.113.42","distinct_users":10,"window_secs":300}
3.7 key_scanning_detected
Le seuil SSH_GUARD_KEY_SCAN_THRESHOLD (nombre de fingerprints distincts essayés depuis la même IP) a été atteint.
| Champ | Type | Description |
|---|---|---|
event | "key_scanning_detected" | |
ts | string | Horodatage UTC |
ip | string | |
distinct_keys | number | Nombre de fingerprints distincts dans la fenêtre |
window_secs | number |
{"event":"key_scanning_detected","ts":"2026-04-19T14:39:08.117Z","ip":"203.0.113.42","distinct_keys":10,"window_secs":300}
3.8 connection_flood_detected
Le cap SSH_GUARD_CONN_FLOOD_PER_SEC a été dépassé pour cette IP. Un connection_dropped avec reason="flood_limit" suit dans le même millième de seconde.
| Champ | Type | Description |
|---|---|---|
event | "connection_flood_detected" | |
ts | string | Horodatage UTC |
ip | string | |
rate_per_sec | number | Cap nominal (valeur de SSH_GUARD_CONN_FLOOD_PER_SEC) |
{"event":"connection_flood_detected","ts":"2026-04-19T14:40:01.555Z","ip":"203.0.113.42","rate_per_sec":10}
3.9 ip_banned
Un ban a été appliqué (auto par un détecteur, manuel par un admin, ou simulé en dry_run).
| Champ | Type | Description |
|---|---|---|
event | "ip_banned" | |
ts | string | Horodatage UTC |
ip | string | IP bannie (pour un CIDR > /32, l’adresse réseau) |
reason | enum string | Voir tableau ci-dessous |
expires_at | string ou null | Horodatage UTC d’expiration. null = ban permanent |
Valeurs de reason :
reason | Origine |
|---|---|
brute_force | BruteForceDetector |
user_enumeration | UserEnumerationDetector |
key_scanning | KeyScanningDetector |
connection_flood | ConnectionFloodDetector (rare : le flood drop ne pose pas de ban persistant par défaut) |
admin_deny_list | Ajout admin dans l’ACL deny |
manual | Ban manuel via UI/API admin |
{"event":"ip_banned","ts":"2026-04-19T14:36:42.999Z","ip":"203.0.113.42","reason":"brute_force","expires_at":"2026-04-19T15:36:42.999Z"}
Mode
dry_run: unip_bannedest émis pour signal externe (fail2ban) sans que ssh-guard inscrive le ban dans son store. Utile pour observer un nouveau seuil sans risque.
3.10 ip_unbanned
Un ban a été levé (TTL expiré ou action admin).
| Champ | Type | Description |
|---|---|---|
event | "ip_unbanned" | |
ts | string | Horodatage UTC |
ip | string | |
manual | boolean | true = action admin, false = expiration auto |
{"event":"ip_unbanned","ts":"2026-04-19T15:36:43.001Z","ip":"203.0.113.42","manual":false}
4. Filtrage côté outils externes
4.1 Fail2ban (filtre par regex sur le fichier JSON)
[Definition]
# Brute-force détecté → ban dur côté firewall
failregex = ^.*"event":"auth_failed".*"ip":"<HOST>".*$
ignoreregex =
Voir le template clé en main : template/security/fail2ban-gitrust-ssh-guard.conf et le how-to Durcir avec Fail2ban.
4.2 Loki (LogQL)
# Toutes les détections fortes (signal pour alerting Grafana)
{job="gitrust-ssh-guard"}
| json
| event=~"brute_force_detected|user_enumeration_detected|key_scanning_detected|connection_flood_detected"
4.3 jq (extraction shell)
# 10 IP les plus actives en échec d'auth sur la dernière heure
sudo journalctl -u gitrust --since "1 hour ago" --no-pager \
| jq -r 'select(.event=="auth_failed") | .ip' \
| sort | uniq -c | sort -rn | head -10
5. Pour aller plus loin
- Variables d’environnement — SSH_GUARD_*
- Configurer ssh-guard
- ssh-guard : détection d’attaques SSH
- Durcir avec Fail2ban
- Crate gitrust-ssh-guard (pour les contributeurs)
Paramètres dynamiques
Ce document explique comment fonctionne la configuration de la plateforme, les deux mécanismes en jeu, leur précédence, et les pièges à connaître.
Vue d’ensemble : deux systèmes distincts
Gitrust utilise deux systèmes de configuration indépendants :
| Système | Source | Modifiable à chaud | Visible dans l’UI admin |
|---|---|---|---|
| Configuration statique | Fichier .env + variables d’environnement | Non (restart requis) | Non |
| Configuration dynamique | Table app_settings (PostgreSQL) | Oui (effet immédiat) | Oui (/admin/settings) |
Il existe un cas hybride pour OAuth (DB prioritaire, .env en fallback).
1. Configuration statique (.env)
Principe
Les variables d’environnement sont chargées une seule fois au démarrage par dotenvy::dotenv() et stockées dans des structs Rust immuables. Elles ne sont jamais relues après le boot.
Fichiers de référence
| Fichier | Rôle |
|---|---|
.env.example | Template documenté avec toutes les variables |
.env | Configuration locale (gitignore) |
.env.production | Configuration de déploiement |
.env.test | Configuration de tests |
Structs de chargement
| Struct | Variables concernées |
|---|---|
GitrustConfig | GIT_REPOS_BASE_PATH, SSH_PORT, SSH_LISTEN_ADDR, SSH_HOST_KEY_PATH, CI_*, IMPORT_* |
AppConfig | APP_NAME, APP_THEME, APP_DEBUG |
AuthConfig | JWT_SECRET, JWT_EXPIRATION_MINUTES, SESSION_*, RATE_LIMIT_*, COOKIE_* |
EmailConfig | SMTP_*, EMAIL_*, IMAP_* |
Comportement
- Chargées dans
RustwardenApp::build()au démarrage - Stockées dans des
Arc<Config>partagés via l’état axum - Modifier le
.envsans redémarrer n’a aucun effet - Ces variables ne sont PAS affichées dans l’interface admin
Liste des variables statiques (non exhaustive)
DATABASE_URL, SERVER_HOST, SERVER_PORT, RUST_LOG,
SSH_HOST_KEY_PATH, SSH_PORT, SSH_LISTEN_ADDR, SSH_PUBLIC_HOST,
JWT_SECRET, JWT_EXPIRATION_MINUTES, JWT_ISSUER,
REFRESH_TOKEN_EXPIRATION_DAYS, REMEMBER_ME_EXPIRATION_DAYS,
SESSION_TIMEOUT_MINUTES, SESSION_BACKEND,
RATE_LIMIT_LOGIN_PER_MINUTE, RATE_LIMIT_REFRESH_PER_MINUTE,
RATE_LIMIT_GENERAL_PER_MINUTE,
APP_DEBUG, COOKIE_SECURE, COOKIE_SAME_SITE,
ADMIN_USERNAME, ADMIN_EMAIL, ADMIN_PASSWORD,
SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, SMTP_FROM,
EMAIL_BASE_URL, EMAIL_QUEUE_*,
APP_NAME, APP_THEME, DEFAULT_LOCALE,
GIT_REPOS_BASE_PATH,
CI_ENABLED, CI_MAX_CONCURRENT, CI_REMOTE_HOST, CI_WORKSPACE_PATH
2. Configuration dynamique (table app_settings)
Principe
Les réglages dynamiques sont stockés en base PostgreSQL dans la table app_settings et lus à chaque requête via AppSettingsService. Ils sont modifiables à chaud par un administrateur depuis l’interface /admin/settings.
Schéma de la table
CREATE TABLE app_settings (
id UUID PRIMARY KEY,
key VARCHAR(100) UNIQUE NOT NULL,
value TEXT NOT NULL,
description TEXT,
updated_by UUID REFERENCES users(id) ON DELETE SET NULL,
updated_at TIMESTAMPTZ NOT NULL
);
Initialisation au démarrage
Au boot, AppSettingsService::initialize_default_settings() crée les réglages par défaut uniquement s’ils n’existent pas encore en base :
#![allow(unused)]
fn main() {
let existing = Self::get_setting(db, key).await?;
if existing.is_none() {
// ... insert valeur par défaut
}
}
Conséquence critique : une fois qu’un réglage existe en base (créé au premier démarrage ou modifié via l’UI), il n’est jamais écrasé par un redémarrage ultérieur.
Liste des réglages dynamiques et leurs valeurs par défaut
| Clé | Défaut | Description |
|---|---|---|
app_domain | env::var("APP_DOMAIN") ou "localhost" | Domaine de l’application |
allow_registration | false | Autoriser l’inscription publique |
validation_email_required | true | Exiger la validation email |
audit_log_level | INFO | Niveau de log d’audit |
audit_log_actions | ["create","update","delete","reset_password"] | Actions auditées |
password_min_length | 8 | Longueur minimum mot de passe |
password_require_uppercase | false | Exiger des majuscules |
password_require_lowercase | false | Exiger des minuscules |
password_require_digits | false | Exiger des chiffres |
password_require_special | false | Exiger des caractères spéciaux |
password_change_require_email | true | Confirmation email pour changement mdp |
password_expiration_enabled | false | Activer l’expiration des mots de passe |
password_expiration_days | 0 | Durée d’expiration (jours) |
password_expiration_alert_enabled | false | Alerte email avant expiration |
password_expiration_alert_days_before | 7 | Jours avant expiration pour alerter |
oauth_enabled | false | Activer OAuth/SSO |
oauth_google_enabled | false | Activer Google OAuth |
oauth_github_enabled | false | Activer GitHub OAuth |
oauth_discord_enabled | false | Activer Discord OAuth |
oauth_microsoft_enabled | false | Activer Microsoft OAuth |
oauth_redirect_base_url | "" | URL de base pour les callbacks OAuth |
oauth_auto_register | true | Créer un compte auto au premier login OAuth |
oauth_link_existing_account | true | Lier un compte OAuth à un utilisateur existant |
oauth_{provider}_client_id | "" | Client ID du provider OAuth |
oauth_{provider}_client_secret | "" | Client secret (chiffré AES-256-GCM en base) |
oauth_microsoft_tenant | "" | Tenant Azure AD |
3. Cas hybride : OAuth
OAuth est le seul sous-système qui combine les deux sources. La logique dans OAuthConfig::load() :
Pour chaque réglage OAuth :
1. Chercher dans app_settings (DB)
→ si trouvé et non vide : utiliser cette valeur
2. Sinon : fallback sur la variable d'environnement
→ si absente : utiliser la valeur par défaut codée en dur
Important : OAuthConfig::load() est appelé au démarrage. Le résultat est stocké dans un Arc et n’est pas relu dynamiquement. Donc :
- Modifier un réglage OAuth via l’UI admin nécessite un redémarrage pour que
OAuthConfigsoit rechargé - Le
.envn’est qu’un fallback si la DB n’a pas de valeur
4. Précédence et comportement au redémarrage
Réglages statiques (.env uniquement)
| Action | Effet immédiat | Après restart |
|---|---|---|
Modifier .env | Non | Oui |
Réglages dynamiques (DB uniquement)
| Action | Effet immédiat | Après restart |
|---|---|---|
| Modifier via UI admin | Oui | Oui (valeur en DB persiste) |
Modifier le .env | Aucun effet | Aucun effet |
Réglages hybrides (OAuth)
| Action | Effet immédiat | Après restart |
|---|---|---|
| Modifier via UI admin | Non (OAuthConfig est en Arc) | Oui (DB prime sur .env) |
Modifier .env | Non | Seulement si aucune valeur en DB |
5. Piège principal : les variables .env fantômes
Certaines variables présentes dans .env.example donnent l’illusion de configurer des réglages qui sont en réalité gérés par la base de données :
Variable .env | Réglage DB correspondant | La variable .env est-elle lue ? |
|---|---|---|
ALLOW_REGISTRATION=true | allow_registration | Non — valeur par défaut codée en dur ("false") |
EMAIL_VALIDATION_REQUIRED=true | validation_email_required | Non — valeur par défaut codée en dur ("true") |
OAUTH_ENABLED=true | oauth_enabled | Oui, mais seulement en fallback si la DB n’a pas de valeur |
Pour ALLOW_REGISTRATION et EMAIL_VALIDATION_REQUIRED :
- La variable
.envest documentée dans.env.examplemais n’est jamais consultée parinitialize_default_settings() - La valeur initiale est codée en dur dans le service
- Seule la valeur en base de données (modifiable via
/admin/settings) fait foi
Pour app_domain, c’est le seul réglage dynamique qui lit le .env comme seed initial.
6. Diagramme de décision
┌─────────────────────────────┐
│ Quel type de réglage ? │
└──────────┬──────────────────-┘
│
┌────────────────┼────────────────┐
▼ ▼ ▼
Infrastructure Fonctionnel OAuth
(port, JWT, SMTP) (inscription, (providers,
mots de passe) secrets)
│ │ │
▼ ▼ ▼
.env seul DB seule DB + fallback .env
│ │ │
▼ ▼ ▼
Restart requis Effet immédiat Restart requis
via /admin/ (OAuthConfig en Arc)
settings
7. Guide pour les administrateurs
Modifier un réglage d’infrastructure
Éditer .env (ou .env.production) puis redémarrer le service :
# Éditer
vim .env
# Redémarrer
systemctl restart gitrust # production
# ou Ctrl+C + cargo run # dev
Modifier un réglage fonctionnel
Se connecter en tant qu’administrateur, aller dans /admin/settings, modifier la valeur, et cliquer “Save”. L’effet est immédiat, aucun redémarrage nécessaire.
Modifier un réglage OAuth
Via /admin/settings, modifier les valeurs OAuth puis redémarrer le service (la config OAuth est chargée en mémoire au boot et n’est pas relue dynamiquement).
Vérifier la valeur effective d’un réglage
Pour les réglages dynamiques, interroger la base directement :
SELECT key, value, updated_at, updated_by
FROM app_settings
WHERE key = 'allow_registration';
Pour les réglages statiques, vérifier les logs au démarrage (RUST_LOG=debug).
8. Guide pour les développeurs
Ajouter un nouveau réglage dynamique
-
Ajouter le tuple
(clé, valeur_défaut, description)dansAppSettingsService::initialize_default_settings():#![allow(unused)] fn main() { ("ma_nouvelle_cle", "valeur_par_defaut", Some("Description pour l'UI admin")), } -
Lire la valeur dans le code via le service :
#![allow(unused)] fn main() { // Boolean avec défaut let enabled = AppSettingsService::get_bool(&db, "ma_nouvelle_cle", false).await?; // String let val = AppSettingsService::get_setting(&db, "ma_nouvelle_cle").await?; } -
La valeur apparaîtra automatiquement dans
/admin/settings.
Ajouter un nouveau réglage statique
- Ajouter la variable dans
.env.exampleavec documentation complète - Lire la variable dans le struct
Configcorrespondant viaenv::var() - La valeur ne sera pas visible dans l’interface admin
Ajouter un réglage hybride (DB + fallback .env)
Suivre le pattern OAuth dans crates/rustwarden-core/src/config/oauth.rs :
#![allow(unused)]
fn main() {
let ma_valeur = match db_val("ma_cle").await {
Some(v) => v,
None => env::var("MA_CLE").unwrap_or_else(|_| "defaut".to_string()),
};
}
Schéma de la base de données
Référence des tables PostgreSQL de gitrust, avec colonnes, index, contraintes et relations. Cette page concerne les tables visibles par un administrateur — pour les détails internes du code (migrations SeaORM, patterns de contribution), voir le manuel développeur.
Diagramme entité-relation
erDiagram
users {
uuid id PK
string username UK
string email UK
string password_hash
boolean must_change_password
timestamptz last_login_at
boolean email_verified
timestamptz created_at
timestamptz updated_at
}
resources {
uuid id PK
string resource_type
uuid resource_id
uuid owner_id FK
boolean is_public
timestamptz created_at
}
resource_shares {
uuid id PK
uuid resource_id FK
uuid shared_with_user_id FK
string permission_level
uuid shared_by_user_id FK
timestamptz created_at
}
repositories {
uuid id PK
uuid owner_id FK
string slug
string description
string disk_path
string default_branch
boolean is_empty
timestamptz created_at
timestamptz updated_at
}
ssh_keys {
uuid id PK
uuid user_id FK
string title
string fingerprint UK
string key_type
text key_data
timestamptz last_used_at
timestamptz created_at
}
teams {
uuid id PK
uuid owner_id FK
string slug
string description
timestamptz created_at
timestamptz updated_at
}
team_members {
uuid id PK
uuid team_id FK
uuid user_id FK
string role
timestamptz created_at
}
team_repository_access {
uuid id PK
uuid team_id FK
uuid repository_id FK
string permission
timestamptz created_at
}
personal_access_tokens {
uuid id PK
uuid user_id FK
string name
string token_hash UK
string scopes
timestamptz expires_at
timestamptz last_used_at
timestamptz created_at
}
issues {
uuid id PK
uuid repository_id FK
integer number
uuid author_id FK
string title
text body
string state
uuid closed_by FK
timestamptz created_at
timestamptz updated_at
}
pull_requests {
uuid id PK
uuid repository_id FK
integer number
uuid author_id FK
string title
text body
string source_branch
string target_branch
string state
timestamptz merged_at
uuid merged_by FK
string merge_commit_sha
timestamptz created_at
timestamptz updated_at
}
labels {
uuid id PK
uuid owner_id FK
uuid repository_id FK
string name
string color
string description
string label_type
timestamptz created_at
}
app_settings {
uuid id PK
string key UK
text value
text description
uuid updated_by FK
timestamptz updated_at
}
audit_log {
uuid id PK
uuid actor_id FK
string action
string resource_type
uuid resource_id
string ip_address
text details
timestamptz created_at
}
users ||--o{ repositories : "possède"
users ||--o{ ssh_keys : "enregistre"
users ||--o{ teams : "crée"
users ||--o{ resources : "possède"
users ||--o{ resource_shares : "partage avec"
users ||--o{ personal_access_tokens : "crée"
users ||--o{ audit_log : "acteur"
repositories ||--o| resources : "enregistré dans"
resources ||--o{ resource_shares : "partagé"
teams ||--o{ team_members : "contient"
users ||--o{ team_members : "membre de"
teams ||--o{ team_repository_access : "accède à"
repositories ||--o{ team_repository_access : "accessible par"
repositories ||--o{ issues : "contient"
repositories ||--o{ pull_requests : "contient"
Tables rustwarden-core (framework)
Ces tables sont gérées par le framework. Ne les modifiez pas directement.
| Table | Rôle |
|---|---|
users | Comptes utilisateurs (authentification, profil) |
roles / permissions / role_permissions / user_roles | RBAC global (admin, user) |
resources | Registre générique de ressources (ownership, visibilité publique/privée) |
resource_shares | Partage de ressources avec niveaux (read/write/admin) |
refresh_tokens | Refresh tokens JWT |
jwt_blacklist | Tokens JWT révoqués (invalidation après déconnexion) |
app_settings | Configuration dynamique de l’application |
audit_log | Journal d’audit de toutes les actions significatives |
oauth_accounts | Comptes OAuth liés |
Tables gitrust
repositories
Dépôts Git hébergés sur la plateforme.
| Colonne | Type | Contraintes | Description |
|---|---|---|---|
id | UUID | PK | Identifiant unique |
owner_id | UUID | FK → users.id CASCADE, NOT NULL | Propriétaire du dépôt |
slug | VARCHAR(64) | NOT NULL | Nom URL du dépôt (ex. mon-projet) |
description | VARCHAR(500) | NULL | Description affichée dans l’UI |
disk_path | VARCHAR(512) | NOT NULL | Chemin absolu du bare repo sur disque |
default_branch | VARCHAR(255) | NOT NULL, DEFAULT 'main' | Branche par défaut |
is_empty | BOOLEAN | NOT NULL, DEFAULT true | Vrai si aucun commit poussé |
created_at | TIMESTAMPTZ | NOT NULL | Date de création |
updated_at | TIMESTAMPTZ | NOT NULL | Date de dernière modification |
Index : UNIQUE(owner_id, slug), INDEX(owner_id)
Chaque dépôt est aussi enregistré dans resources avec resource_type = "repository" pour gérer la visibilité (public/privé) et le partage via resource_shares.
ssh_keys
Clés publiques SSH des utilisateurs pour l’authentification Git.
| Colonne | Type | Contraintes | Description |
|---|---|---|---|
id | UUID | PK | Identifiant unique |
user_id | UUID | FK → users.id CASCADE, NOT NULL | Propriétaire de la clé |
title | VARCHAR(255) | NOT NULL | Libellé (ex. MacBook Pro) |
fingerprint | VARCHAR(128) | UNIQUE, NOT NULL | Fingerprint SHA256 calculé à l’insertion |
key_type | VARCHAR(32) | NOT NULL | Type : ssh-ed25519, ssh-rsa, ecdsa-* |
key_data | TEXT | NOT NULL | Clé au format authorized_keys |
last_used_at | TIMESTAMPTZ | NULL | Dernière utilisation SSH |
created_at | TIMESTAMPTZ | NOT NULL | Date d’ajout |
Validation : types autorisés — Ed25519, RSA ≥ 4096 bits, ECDSA P-256/P-384.
teams
Groupes d’utilisateurs pour le partage d’accès aux dépôts.
| Colonne | Type | Contraintes | Description |
|---|---|---|---|
id | UUID | PK | Identifiant unique |
owner_id | UUID | FK → users.id CASCADE, NOT NULL | Créateur / administrateur de l’équipe |
slug | VARCHAR(64) | NOT NULL | Nom URL de l’équipe |
description | VARCHAR(500) | NULL | Description |
created_at | TIMESTAMPTZ | NOT NULL | Date de création |
updated_at | TIMESTAMPTZ | NOT NULL | Date de dernière modification |
Index : UNIQUE(owner_id, slug)
team_members
Appartenance d’un utilisateur à une équipe avec un rôle.
| Colonne | Type | Contraintes | Description |
|---|---|---|---|
id | UUID | PK | Identifiant unique |
team_id | UUID | FK → teams.id CASCADE, NOT NULL | Équipe |
user_id | UUID | FK → users.id CASCADE, NOT NULL | Membre |
role | VARCHAR(20) | NOT NULL, DEFAULT 'read' | Rôle dans l’équipe |
created_at | TIMESTAMPTZ | NOT NULL | Date d’ajout |
Index : UNIQUE(team_id, user_id), INDEX(user_id)
team_repository_access
Permissions d’accès d’une équipe sur un dépôt.
| Colonne | Type | Contraintes | Description |
|---|---|---|---|
id | UUID | PK | Identifiant unique |
team_id | UUID | FK → teams.id CASCADE, NOT NULL | Équipe |
repository_id | UUID | FK → repositories.id CASCADE, NOT NULL | Dépôt |
permission | VARCHAR(20) | NOT NULL, DEFAULT 'read' | Niveau : read, write, admin |
created_at | TIMESTAMPTZ | NOT NULL | Date d’attribution |
Index : UNIQUE(team_id, repository_id), INDEX(repository_id)
personal_access_tokens
Jetons d’accès personnels pour l’authentification API et Git HTTPS.
| Colonne | Type | Contraintes | Description |
|---|---|---|---|
id | UUID | PK | Identifiant unique |
user_id | UUID | FK → users.id CASCADE, NOT NULL | Propriétaire du jeton |
name | VARCHAR(255) | NOT NULL | Nom descriptif (ex. CI pipeline) |
token_hash | VARCHAR(128) | UNIQUE, NOT NULL | Hash SHA-256 du jeton (le jeton brut n’est jamais stocké) |
scopes | VARCHAR(255) | NOT NULL | Portées autorisées (séparées par espace) |
expires_at | TIMESTAMPTZ | NULL | Expiration (NULL = sans expiration) |
last_used_at | TIMESTAMPTZ | NULL | Dernière utilisation |
created_at | TIMESTAMPTZ | NOT NULL | Date de création |
issues
Tickets de suivi (bugs, tâches, demandes de fonctionnalité).
| Colonne | Type | Contraintes | Description |
|---|---|---|---|
id | UUID | PK | Identifiant unique |
repository_id | UUID | FK → repositories.id CASCADE | Dépôt propriétaire |
number | INTEGER | NOT NULL (auto-incrémenté par dépôt) | Numéro affiché (#1, #2…) |
author_id | UUID | FK → users.id CASCADE | Auteur |
title | VARCHAR(255) | NOT NULL | Titre |
body | TEXT | NOT NULL DEFAULT '' | Corps (Markdown) |
state | VARCHAR(20) | NOT NULL DEFAULT 'open' | État : open, closed |
closed_by | UUID | FK → users.id NULL | Utilisateur qui a fermé l’issue |
created_at | TIMESTAMPTZ | NOT NULL | Date de création |
updated_at | TIMESTAMPTZ | NOT NULL | Date de dernière modification |
Index : UNIQUE(repository_id, number), INDEX(repository_id, state)
pull_requests
Propositions de merge d’une branche source vers une branche cible.
| Colonne | Type | Contraintes | Description |
|---|---|---|---|
id | UUID | PK | Identifiant unique |
repository_id | UUID | FK → repositories.id CASCADE | Dépôt |
number | INTEGER | NOT NULL (auto-incrémenté par dépôt) | Numéro affiché |
author_id | UUID | FK → users.id CASCADE | Auteur |
title | VARCHAR(255) | NOT NULL | Titre |
body | TEXT | NOT NULL DEFAULT '' | Description |
source_branch | VARCHAR(255) | NOT NULL | Branche source |
target_branch | VARCHAR(255) | NOT NULL | Branche cible (ex. main) |
state | VARCHAR(20) | NOT NULL DEFAULT 'open' | État : open, merged, closed |
merged_at | TIMESTAMPTZ | NULL | Date de merge |
merged_by | UUID | FK → users.id NULL | Utilisateur qui a mergé |
merge_commit_sha | VARCHAR(40) | NULL | SHA du commit de merge |
created_at | TIMESTAMPTZ | NOT NULL | Date de création |
updated_at | TIMESTAMPTZ | NOT NULL | Date de dernière modification |
Index : UNIQUE(repository_id, number), INDEX(repository_id, state)
labels
Étiquettes à deux niveaux : classification (scope owner) et subject (scope dépôt).
| Colonne | Type | Contraintes | Description |
|---|---|---|---|
id | UUID | PK | Identifiant unique |
owner_id | UUID | FK → users.id NULL | Propriétaire (labels de classification) |
repository_id | UUID | FK → repositories.id NULL | Dépôt (labels de sujet) |
name | VARCHAR(50) | NOT NULL | Nom du label |
color | VARCHAR(7) | NOT NULL | Couleur hexadécimale (ex. #e11d48) |
description | VARCHAR(255) | NULL | Description |
label_type | VARCHAR(20) | NOT NULL | 'classification' ou 'subject' |
created_at | TIMESTAMPTZ | NOT NULL | Date de création |
Index : UNIQUE(owner_id, repository_id, name, label_type)
app_settings
Configuration dynamique de l’application (modifiable à chaud via /admin/settings).
| Colonne | Type | Contraintes | Description |
|---|---|---|---|
id | UUID | PK | Identifiant unique |
key | VARCHAR(100) | UNIQUE, NOT NULL | Clé de configuration (ex. allow_registration) |
value | TEXT | NOT NULL | Valeur sous forme de texte |
description | TEXT | NULL | Description affichée dans l’UI admin |
updated_by | UUID | FK → users.id ON DELETE SET NULL | Dernier administrateur ayant modifié |
updated_at | TIMESTAMPTZ | NOT NULL | Date de dernière modification |
audit_log
Journal d’audit de toutes les actions significatives.
| Colonne | Type | Contraintes | Description |
|---|---|---|---|
id | UUID | PK | Identifiant unique |
actor_id | UUID | FK → users.id NULL | Utilisateur auteur de l’action |
action | VARCHAR(50) | NOT NULL | Type d’action (create, update, delete, reset_password…) |
resource_type | VARCHAR(50) | NULL | Type de ressource concernée |
resource_id | UUID | NULL | Identifiant de la ressource |
ip_address | VARCHAR(45) | NULL | Adresse IP source |
details | TEXT | NULL | Détails supplémentaires (JSON) |
created_at | TIMESTAMPTZ | NOT NULL | Horodatage |
Modèle de permissions
graph TB
subgraph "Accès individuel"
Owner["Owner (resources.owner_id)"]
Share["resource_shares (read/write/admin)"]
Public["Accès public (resources.is_public)"]
end
subgraph "Accès équipe"
TM["team_members (rôle dans l'équipe)"]
TRA["team_repository_access (permission sur le dépôt)"]
end
subgraph "Rôle effectif"
Eff["max(individuel, équipe)"]
end
Owner -->|"Owner (full)"| Eff
Share -->|"read/write/admin"| Eff
Public -->|"read only"| Eff
TM --> TRA
TRA -->|"read/write/admin"| Eff
| Rôle | Permissions |
|---|---|
| Reader | Clone, navigation (lecture seule) |
| Developer | Reader + push |
| Maintainer | Developer + paramètres, collaborateurs, protections de branche |
| Owner | Maintainer + suppression, transfert |
Ordre des migrations
| # | Migration | Tables créées |
|---|---|---|
| 1 | m20260305_000001_initial_schema | users, refresh_tokens, jwt_blacklist… (core) |
| 2 | m20260306_000002_create_app_settings_table | app_settings (core) |
| 3 | m20260309_000003_create_permissions_tables | roles, permissions, role_permissions, user_roles (core) |
| 4 | m20260309_000004_create_resources_tables | resources, resource_shares (core) |
| 5 | m20260310_000005_create_oauth_accounts_table | oauth_accounts (core) |
| 6 | m20260325_000001_create_repositories | repositories |
| 7 | m20260325_000002_create_ssh_keys | ssh_keys |
| 8 | m20260325_000003_create_teams | teams |
| 9 | m20260325_000004_create_team_members | team_members |
| 10 | m20260325_000005_create_team_repository_access | team_repository_access |
| 11 | m20260327_000006_create_personal_access_tokens | personal_access_tokens |
| 12 | m20260327_000007_create_issues | issues |
| 13 | m20260327_000008_create_issue_comments | issue_comments |
| 14 | m20260327_000009_create_labels | labels, issue_labels |
| 15 | m20260327_000010_add_label_type | alter labels |
| 16 | m20260327_000011_labels_owner_scope | alter labels |
| 17 | m20260327_000012_create_pull_requests | pull_requests |
| 18 | m20260327_000013_create_pr_comments | pr_comments |
Toutes les migrations sont appliquées automatiquement au démarrage par AppMigrator. Elles sont idempotentes.
Pour aller plus loin
- Auditer l’instance — requêtes SQL sur
audit_log - Paramètres dynamiques — table
app_settings - Sauvegarder et restaurer —
pg_dump/pg_restore
Ports et services
Référence des ports réseau utilisés par gitrust et ses dépendances.
Tableau des ports
| Port | Protocole | Service | Processus | Binding par défaut | Configurable |
|---|---|---|---|---|---|
| 4000 | TCP/HTTP | Interface web + API REST | gitrust (axum) | SERVER_HOST:SERVER_PORT | Oui |
| 2222 | TCP/SSH | Serveur Git SSH | gitrust (Russh) | SSH_LISTEN_ADDR:SSH_PORT | Oui |
| 5432 | TCP | PostgreSQL | postgres | 127.0.0.1:5432 | Via PG config |
| 6379 | TCP | Redis (sessions) | redis-server | 127.0.0.1:6379 | Via Redis config |
| 25 | TCP/SMTP | Envoi e-mail sortant (non chiffré) | Client SMTP gitrust | Outbound seulement | Via SMTP_PORT |
| 465 | TCP/SMTPS | Envoi e-mail sortant (TLS natif) | Client SMTP gitrust | Outbound seulement | Via SMTP_PORT |
| 587 | TCP/SMTP+STARTTLS | Envoi e-mail sortant (recommandé) | Client SMTP gitrust | Outbound seulement | Via SMTP_PORT |
Détail par service
Port 4000 — Interface web et API REST (HTTP)
gitrust expose son interface web HTMX et son API REST v1 sur ce port via le framework axum.
Configuration :
SERVER_HOST=127.0.0.1 # derrière un reverse-proxy (recommandé en prod)
SERVER_PORT=4000
En production : gitrust écoute sur 127.0.0.1:4000 et un reverse-proxy (Nginx, Caddy, Traefik) termine TLS et proxifie vers ce port. Les clients n’accèdent jamais directement au port 4000.
Règle firewall (si reverse-proxy sur la même machine) :
# Aucune ouverture nécessaire depuis l'extérieur — le proxy écoute sur 443
# Bloquer le port 4000 depuis l'extérieur :
sudo ufw deny 4000/tcp
En développement : gitrust peut écouter sur 0.0.0.0:4000 pour un accès depuis le LAN.
Port 2222 — Serveur Git SSH (Russh)
gitrust intègre son propre serveur SSH via la bibliothèque Russh — ce n’est pas le démon sshd du système d’exploitation. Les deux peuvent coexister sans conflit.
Configuration :
SSH_PORT=2222
SSH_LISTEN_ADDR=0.0.0.0 # accès direct
# ou
SSH_LISTEN_ADDR=127.0.0.1 # derrière nginx stream proxy
Accès direct (recommandé pour la simplicité) : ouvrir le port 2222 dans le firewall.
sudo ufw allow 2222/tcp
URL de clone : ssh://git@VOTRE_DOMAINE:2222/owner/repo.git
Via nginx stream proxy (port 22) : si vous souhaitez que les utilisateurs clonent sur le port standard 22, configurez nginx comme proxy de flux TCP :
# Dans nginx.conf, section stream (hors section http)
stream {
upstream gitrust_ssh {
server 127.0.0.1:2222;
}
server {
listen 22;
proxy_pass gitrust_ssh;
}
}
Dans ce cas, SSH_LISTEN_ADDR=127.0.0.1 et SSH_PUBLIC_HOST=VOTRE_DOMAINE (sans port dans les URLs de clone).
Attention : si le port 22 est déjà utilisé par
sshdpour l’administration du serveur, déplacezsshdsur un autre port avant de configurer le proxy nginx.
Règle firewall :
sudo ufw allow 2222/tcp # accès direct
# ou
sudo ufw allow 22/tcp # si proxy nginx stream
Port 5432 — PostgreSQL
PostgreSQL doit toujours écouter sur 127.0.0.1 (ou un réseau privé), jamais sur 0.0.0.0 en production.
Vérification :
sudo -u postgres psql -c "SHOW listen_addresses;"
# Attendu : localhost ou 127.0.0.1
Règle firewall : aucune ouverture vers l’extérieur.
# Bloquer explicitement depuis l'extérieur (si ufw)
sudo ufw deny 5432/tcp
Pour une architecture multi-serveurs (gitrust et PostgreSQL sur des machines distinctes), utilisez un réseau privé ou un tunnel SSH — jamais une exposition directe sur Internet.
Port 6379 — Redis (sessions multi-instances)
Redis n’est nécessaire que si SESSION_BACKEND=redis dans .env. Par défaut, gitrust utilise PostgreSQL pour les sessions.
Configuration Redis (dans /etc/redis/redis.conf) :
bind 127.0.0.1
requirepass VOTRE_MOT_DE_PASSE_REDIS
Règle firewall : aucune ouverture vers l’extérieur.
Ports SMTP sortants (25, 465, 587)
Ces ports sont utilisés en sortant uniquement : gitrust se connecte au serveur SMTP configuré dans SMTP_HOST. Gitrust n’héberge pas de serveur SMTP.
| Port | Usage | Recommandation |
|---|---|---|
| 25 | SMTP non chiffré | Interdit en production, souvent bloqué par les FAI et hébergeurs |
| 465 | SMTPS (TLS natif) | Recommandé avec SMTP_USE_TLS=true |
| 587 | SMTP + STARTTLS | Recommandé avec SMTP_USE_STARTTLS=true |
Aucune règle firewall entrante nécessaire pour SMTP.
Architecture réseau par topologie
Développement local
Navigateur → http://localhost:4000 (direct, pas de proxy)
Git client → ssh://localhost:2222
Tous les ports en 0.0.0.0, COOKIE_SECURE=false, APP_DEBUG=true.
Production mono-machine (recommandé)
Internet → :443 (HTTPS) → nginx → 127.0.0.1:4000 (gitrust HTTP)
Internet → :2222 (SSH) → gitrust directement
Internet ← SMTP sortant → smtp.prestataire.com:587
SERVER_HOST=127.0.0.1
SERVER_PORT=4000
SSH_LISTEN_ADDR=0.0.0.0
SSH_PORT=2222
Règles firewall :
sudo ufw allow 443/tcp # HTTPS via nginx
sudo ufw allow 2222/tcp # Git SSH
sudo ufw deny 4000/tcp # Masqué derrière nginx
sudo ufw deny 5432/tcp # PostgreSQL interne uniquement
Production avec proxy SSH sur port 22
Internet → :22 (SSH) → nginx stream → 127.0.0.1:2222 (gitrust SSH)
SSH_LISTEN_ADDR=127.0.0.1
SSH_PORT=2222
SSH_PUBLIC_HOST=gitrust.votre-domaine.com
URL de clone : git@gitrust.votre-domaine.com:owner/repo.git
Pour aller plus loin
Conformité ANSSI PA-074
Checklist de conformité au guide PA-074 de l’ANSSI (Recommandations de sécurité pour les applications web) appliquée à une instance gitrust. Pour chaque point : statut dans gitrust et action requise de l’administrateur.
Base :
docs/QA_RULES.mddu dépôt gitrust source, enrichi des contrôles applicables à l’administration d’une instance.
1. Authentification et gestion des sessions
| Critère ANSSI | Statut gitrust | Action administrateur requise |
|---|---|---|
| Mot de passe haché avec algorithme fort (bcrypt/Argon2) | Implémenté (bcrypt) | Aucune — automatique |
| Longueur minimale de mot de passe configurable | Implémenté (password_min_length dans app_settings) | Configurer password_min_length ≥ 12 via /admin/settings |
| Complexité du mot de passe configurable | Implémenté (majuscules, minuscules, chiffres, spéciaux) | Activer les règles souhaitées via /admin/settings |
| Expiration des mots de passe | Implémenté (password_expiration_enabled) | Activer si requis par votre politique SSI |
| Tokens comparés en temps constant | Implémenté (subtle::ConstantTimeEq) | Aucune |
| Durée de vie des sessions limitée | Implémenté (SESSION_TIMEOUT_MINUTES, défaut 30 min) | Ajuster selon votre politique (recommandé : ≤ 30 min) |
| Invalidation de session à la déconnexion | Implémenté (JWT blacklist + refresh token révoqué) | Aucune |
| Refresh token rotation à chaque usage | Implémenté | Aucune |
| Durée « Se souvenir de moi » bornée | Implémenté (≤ 90 jours, warning > 60 jours) | Vérifier REMEMBER_ME_EXPIRATION_DAYS ≤ 30 |
| Authentification multi-facteurs (2FA TOTP) | Implémenté | Activer totp_force_enabled=true via /admin/settings pour forcer le 2FA sur tous les comptes |
| Protection contre le brute force (rate limiting) | Implémenté (RATE_LIMIT_LOGIN_PER_MINUTE) | Vérifier RATE_LIMIT_LOGIN_PER_MINUTE ≤ 5 ; ajouter Fail2ban |
2. Chiffrement des communications
| Critère ANSSI | Statut gitrust | Action administrateur requise |
|---|---|---|
| HTTPS obligatoire en production | Non géré par gitrust (délégué au reverse-proxy) | Configurer TLS sur Nginx/Caddy avec certificat valide (Let’s Encrypt ou CA interne) |
| HSTS activé | Non géré par gitrust | Ajouter Strict-Transport-Security: max-age=63072000; includeSubDomains dans Nginx |
| TLS ≥ 1.2 uniquement | Non géré par gitrust | Configurer ssl_protocols TLSv1.2 TLSv1.3 dans Nginx |
Cookies Secure sur HTTPS | Implémenté (COOKIE_SECURE=true si APP_DEBUG=false) | Vérifier APP_DEBUG=false en production |
Cookies HttpOnly | Implémenté (défaut framework) | Aucune |
Cookies SameSite=Lax minimum | Implémenté (COOKIE_SAME_SITE=Lax par défaut) | Aucune |
| SSH via Ed25519 (algorithme moderne) | Implémenté (Russh, clé hôte Ed25519) | Aucune |
| TLS pour SMTP sortant | Implémenté (SMTP_USE_TLS / SMTP_USE_STARTTLS) | Vérifier SMTP_USE_STARTTLS=true ou SMTP_USE_TLS=true ; jamais les deux à false |
3. Protection contre les injections et les failles web
| Critère ANSSI | Statut gitrust | Action administrateur requise |
|---|---|---|
| Validation des entrées utilisateur (formulaires, params) | Implémenté (newtypes RepoSlug, Fingerprint, etc.) | Aucune |
| Protection CSRF sur toutes les actions mutantes | Implémenté (token CSRF sur POST/PUT/DELETE) | Aucune |
| Pas d’injection SQL (ORM paramétré) | Implémenté (SeaORM, requêtes préparées) | Aucune |
| Pas d’accès inter-utilisateurs (anti-IDOR) | Implémenté (vérification user_id côté serveur) | Aucune |
| Path traversal bloqué (dépôts Git) | Implémenté (RepoSlug validé, pas de .. ou /) | Aucune |
| Content Security Policy | Partiellement implémenté (CSP bloque les CDN externes) | Vérifier les en-têtes CSP dans Nginx si souhaité |
| Pas de ressources CDN externes | Implémenté (zéro CDN, tout servi depuis static/) | Aucune |
unsafe_code interdit (Rust) | Implémenté (#![forbid(unsafe_code)] dans core/web/hooks) | Aucune |
4. Gestion des secrets et des clés
| Critère ANSSI | Statut gitrust | Action administrateur requise |
|---|---|---|
JWT_SECRET minimum 32 bytes | Implémenté (refus au démarrage si trop court) | Générer avec openssl rand -hex 64 |
| Secrets OAuth chiffrés en base (AES-256-GCM) | Implémenté | Définir OAUTH_ENCRYPTION_KEY avec openssl rand -hex 32 |
| Personal access tokens hachés (SHA-256) | Implémenté | Aucune |
| Secrets jamais loggués | Implémenté (pas de Display/Debug sur les secrets) | Vérifier RUST_LOG=info ou warn en production |
| Clé SSH hôte avec permissions restrictives | Implémenté (600, owner = user systemd) | Vérifier après restauration depuis sauvegarde |
| Rotation de la clé JWT possible | Possible (modifier JWT_SECRET + redémarrage) | Planifier une rotation annuelle ou en cas de compromission |
| Zeroize des types sensibles | Implémenté (Zeroize/ZeroizeOnDrop) | Aucune |
5. Journalisation et audit
| Critère ANSSI | Statut gitrust | Action administrateur requise |
|---|---|---|
| Journal d’audit des actions significatives | Implémenté (table audit_log) | Vérifier que audit_log_actions couvre vos besoins via /admin/settings |
| Horodatage des événements | Implémenté (TIMESTAMPTZ avec fuseau) | Vérifier le fuseau horaire du serveur (timedatectl) |
| IP source enregistrée | Implémenté (ip_address dans audit_log) | Si derrière proxy : vérifier que X-Real-IP est transmis par Nginx |
| Non-répudiation (acteur identifié) | Implémenté (actor_id → users.id) | Aucune |
| Conservation des logs ≥ 12 mois | Non automatique (pas de purge auto actuellement) | Planifier une purge manuelle annuelle en conservant les événements de sécurité |
| Logs système via journald | Implémenté (tous les logs gitrust dans journald) | Configurer journald.conf avec SystemMaxUse et MaxRetentionSec appropriés |
| Export des logs d’audit | Implémenté (CSV depuis /admin/audit) | Archiver régulièrement les exports CSV |
6. Gestion des accès et des droits
| Critère ANSSI | Statut gitrust | Action administrateur requise |
|---|---|---|
| Principe du moindre privilège | Implémenté (4 rôles : Reader/Developer/Maintainer/Owner) | Vérifier que les utilisateurs ont le niveau minimal nécessaire |
| Séparation des rôles (utilisateur / admin) | Implémenté (rôle admin global distinct des rôles dépôt) | Limiter le nombre de comptes admin |
| Révocation immédiate des accès | Implémenté (désactivation compte = effet immédiat) | Désactiver les comptes des personnes ayant quitté l’équipe |
| Inscription publique désactivée par défaut | Implémenté (allow_registration=false par défaut) | Garder false pour les instances privées |
| Validation e-mail obligatoire | Implémenté (validation_email_required=true par défaut) | Maintenir activé en production |
| Audit des créations/suppressions de comptes | Implémenté (enregistré dans audit_log) | Consulter régulièrement /admin/audit |
7. Durcissement système (responsabilité de l’administrateur)
Ces points sont hors du périmètre de gitrust mais nécessaires pour la conformité globale.
| Critère ANSSI | Action requise |
|---|---|
| Service lancé sous utilisateur non-root | Créer l’utilisateur gitrust (voir tutoriel 02) |
| Durcissement systemd | Utiliser les directives NoNewPrivileges, ProtectSystem=strict, PrivateTmp (incluses dans gitrust.service) |
| Mises à jour de sécurité OS | sudo apt-get update && apt-get upgrade planifié (cron/unattended-upgrades) |
| Fail2ban contre le brute force SSH/HTTP | Voir Durcir avec Fail2ban |
| Sauvegarde chiffrée hors site | Chiffrer les archives de sauvegarde avec GPG ou age ; stocker sur un système distinct |
| Inventaire des dépendances (SBOM) | Activer CI_SBOM_ENABLED=true et CI_DTRACK_ENABLED=true pour le suivi CVE |
Résumé des actions prioritaires
Pour une instance conforme PA-074, effectuez ces actions dans l’ordre :
- TLS obligatoire — configurer Nginx avec certificat valide, HSTS, TLS 1.2+ uniquement
- JWT_SECRET fort —
openssl rand -hex 64dans.env - APP_DEBUG=false — force
COOKIE_SECURE=trueautomatiquement - Forcer le 2FA —
totp_force_enabled=truedans/admin/settings - Politique de mots de passe —
password_min_length=12et complexité dans/admin/settings - Inscription fermée —
allow_registration=falsedans/admin/settings - Rate limiting —
RATE_LIMIT_LOGIN_PER_MINUTE=3+ Fail2ban - Sauvegarde chiffrée — script
backup.sh+ chiffrement GPG + stockage hors site - Conservation des logs — configurer journald et archivage CSV trimestriel
- SBOM —
CI_SBOM_ENABLED=true+ Dependency-Track pour le suivi CVE
Pour aller plus loin
Explanations — Manuel d’administration
Les explanations construisent votre modèle mental de gitrust. Elles répondent à la question « Pourquoi ça fonctionne ainsi ? » et vous donnent le recul nécessaire pour prendre de bonnes décisions opérationnelles — bien au-delà de ce que les guides pratiques peuvent vous offrir.
Ces pages ne contiennent pas de commandes à exécuter. Pour les recettes, allez dans Guides pratiques (how-to). Pour les tableaux de valeurs, allez dans Référence.
Carte des explanations
graph TB
A[Architecture globale<br/>Composants + flux] --> B[Modèle de déploiement<br/>Topologies + limites]
A --> C[Stratégie de sauvegarde<br/>3-2-1 + RPO/RTO]
A --> D[CI Dagger<br/>Easy Mode vs Power Mode]
style A fill:#e3f2fd
style B fill:#f3e5f5
style C fill:#e8f5e9
style D fill:#fff3e0
Pages d’explanation
Architecture globale
Composants au runtime (processus axum, serveur SSH Russh, workers CI/Import, PostgreSQL, système de fichiers), flux d’une requête HTTP et d’un push SSH. Implications opérationnelles : pourquoi la clé SSH hôte ne doit jamais être régénérée, pourquoi la base de données et les dépôts bare doivent être sauvegardés ensemble.
Lisez cette page quand : vous débutez en administration gitrust, vous cherchez à comprendre quel composant est responsable d’un comportement, ou vous voulez anticiper l’impact d’un redémarrage.
Modèle de déploiement
Les trois topologies (dev local, mono-machine production, haute disponibilité), leurs compromis coût/complexité/disponibilité, les limites actuelles de la mise à l’échelle horizontale (workers stateful, volume partagé pour les dépôts bare).
Lisez cette page quand : vous évaluez si votre équipe a besoin d’une architecture HA, vous planifiez une migration vers un nouveau serveur, ou vous expliquez gitrust à un décideur technique.
CI Dagger : Easy Mode vs Power Mode
Pourquoi gitrust utilise Dagger plutôt qu’un système CI YAML classique. Différence entre le profil .gitrust-ci.yml (Easy Mode, interprété par le moteur CI de la plateforme) et le module .dagger/ (Power Mode, code natif Dagger). Quand conseiller le passage de l’un à l’autre.
Lisez cette page quand : un utilisateur vous demande pourquoi sa CI ne fait pas ce qu’il veut, vous évaluez les capacités CI de gitrust, ou vous voulez comprendre les implications opérationnelles des deux modes.
Stratégie de sauvegarde
La règle 3-2-1 appliquée aux trois sources de vérité de gitrust. RPO et RTO réalistes pour une instance mono-machine. Comparaison pg_dump vs réplication WAL. Pourquoi tester la restauration (drill) est non-négociable.
Lisez cette page quand : vous concevez votre politique de sauvegarde, vous devez justifier un choix technique devant votre direction, ou vous voulez comprendre ce que vous perdriez dans différents scénarios d’incident.
Navigation
- Vous cherchez à accomplir une tâche concrète → Guides pratiques (how-to)
- Vous cherchez une valeur ou une définition → Référence
- Vous démarrez depuis zéro → Tutoriels
Comprendre l’architecture globale de gitrust
Ce que vous allez comprendre
- Identifier les processus qui s’exécutent au sein d’une instance gitrust et leur rôle respectif
- Analyser le chemin d’une requête HTTP et d’un push SSH du client jusqu’à la réponse
- Évaluer les implications opérationnelles de cette architecture pour les décisions d’administration (sauvegarde, mise à l’échelle, durcissement)
Le problème concret
Vous administrez gitrust et vous devez décider : « Sur quel processus dois-je redémarrer quand je modifie la configuration SMTP ? Pourquoi la clé SSH hôte ne doit-elle jamais être régénérée ? Qu’est-ce qui se passe si PostgreSQL est indisponible pendant 2 minutes ? »
Ces questions n’ont de réponse claire que si vous comprenez l’architecture interne.
L’analogie
Pensez à gitrust comme à un immeuble de bureaux avec un seul accueil (le binaire gitrust) qui abrite plusieurs services distincts :
- Le guichet HTTP reçoit les navigateurs et les appels API
- Le guichet SSH reçoit les clients
git - La salle des archives (PostgreSQL) conserve toutes les données
- Les ateliers (workers CI, import) font du travail en arrière-plan
- Le vestiaire (système de fichiers) garde les manteaux (les dépôts bare)
Un seul processus, mais plusieurs « couloirs » internes. Si l’immeuble ferme (redémarrage), tous les guichets ferment simultanément.
Le modèle
Composants au runtime
graph TB
subgraph "Processus gitrust (binaire unique)"
AX[Serveur HTTP axum<br/>:4000]
GUARD[ssh-guard<br/>SecureListener + détecteurs]
SSH[Serveur SSH Russh<br/>:2222]
CIW[Worker CI async<br/>Dagger]
IMW[Worker Import async<br/>git clone]
EMW[Worker Email async<br/>SMTP queue]
end
subgraph "Dépendances externes"
PG[(PostgreSQL<br/>:5432)]
FS[Système de fichiers<br/>GIT_REPOS_BASE_PATH]
SMTP[Serveur SMTP<br/>sortant]
DAGGER[Dagger Engine<br/>Docker]
end
subgraph "Clients"
BR[Navigateur / API]
GIT[Client git SSH]
CI[Push git → CI trigger]
end
BR -->|HTTP/HTTPS via nginx| AX
GIT -->|SSH :2222| GUARD
GUARD -->|"AcceptOutcome::Accepted"| SSH
CI -->|git push → hook| CIW
AX -->|SeaORM| PG
AX -.->|"admin ACL/ban"| GUARD
GUARD -->|Bans, ACL, events| PG
SSH -->|Lit authorized_keys| PG
SSH -->|git-receive-pack| FS
AX -->|Lit/écrit dépôts| FS
CIW -->|dagger call| DAGGER
IMW -->|git clone| FS
EMW -->|SMTP| SMTP
Flux d’une requête HTTP (ex. : afficher un dépôt)
sequenceDiagram
participant B as Navigateur
participant N as Nginx (TLS)
participant AX as axum handler
participant DB as PostgreSQL
participant FS as Système de fichiers
B->>N: GET https://gitrust.domain/alice/mon-depot
N->>AX: proxy_pass http://127.0.0.1:4000/alice/mon-depot
AX->>DB: SELECT * FROM repositories WHERE owner=alice AND slug=mon-depot
DB-->>AX: Repository { id, disk_path, ... }
AX->>FS: git log --oneline HEAD (libgit2)
FS-->>AX: Liste des commits
AX-->>N: HTML (template Askama rendu)
N-->>B: HTTP 200 + HTML
Flux d’un push SSH (ex. : git push origin main)
sequenceDiagram
participant G as git client
participant SSH as Russh :2222
participant DB as PostgreSQL
participant FS as Dépôt bare (.git)
participant H as gitrust-hooks
G->>SSH: Connexion SSH, propose clé publique
SSH->>DB: SELECT id FROM ssh_keys WHERE fingerprint=SHA256:...
DB-->>SSH: user_id = alice
SSH-->>G: Authentifié
G->>SSH: git-receive-pack /alice/mon-depot.git
SSH->>DB: Vérifie permission write sur le dépôt
DB-->>SSH: OK (Developer ou Owner)
SSH->>FS: Reçoit les objets Git, met à jour les refs
FS-->>SSH: Succès
SSH->>H: Déclenche post-receive hook
H->>DB: Enregistre l'événement push
H-->>SSH: Fin
SSH-->>G: remote: OK
Implications opérationnelles
Un seul processus, une seule unité d’arrêt
gitrust est un binaire unique. Redémarrer le service (systemctl restart gitrust) interrompt simultanément :
- Le serveur HTTP (quelques secondes d’indisponibilité web)
- Le serveur SSH (les push en cours sont coupés)
- Les workers CI/Import (les tâches en cours peuvent être interrompues et reprises au prochain démarrage)
Conséquence : planifiez les redémarrages (mise à jour, changement de .env) en dehors des heures de pointe.
PostgreSQL est le point central
Toutes les décisions d’authentification, d’autorisation et de persistance passent par PostgreSQL. Si PostgreSQL est indisponible :
- Les connexions HTTP retournent
503 - Les connexions SSH sont refusées (impossible de valider la clé publique)
- Les workers mettent leurs tâches en file d’attente locale
Conséquence : la sauvegarde et la haute disponibilité de PostgreSQL sont prioritaires sur tout le reste.
Le système de fichiers et la base de données doivent être cohérents
Un dépôt existe à deux endroits : en base (repositories table) et sur disque (disk_path). Une restauration partielle (base seule, ou disque seul) laisse l’instance dans un état incohérent.
Conséquence : toujours sauvegarder et restaurer la base ET les dépôts ensembles (voir Sauvegarder et restaurer).
ssh-guard intercale un sas devant le serveur SSH
Toutes les connexions sur :2222 passent d’abord par SecureListener (ssh-guard). Cette couche extrait l’IP réelle (PROXY protocol si nginx stream est devant), consulte ACL et bans actifs, applique un cap TCP par IP, puis ne remet le TcpStream à russh que si tout est en règle. Un événement JSON stable est émis pour chaque décision (connection_accepted, connection_dropped, auth_failed, ip_banned, …) — pratique pour fail2ban et SIEM.
Conséquence : modifier SSH_GUARD_* dans .env change immédiatement le comportement défensif après redémarrage. Les ACL admin (allow/deny par CIDR) sont en revanche modifiables à chaud via l’UI : la table ssh_guard_acl est partagée par référence entre le routeur HTTP et le listener SSH. Voir Configurer ssh-guard et ssh-guard : détection d’attaques SSH.
La clé SSH hôte identifie l’instance
La clé Ed25519 dans SSH_HOST_KEY_PATH est l’identité SSH de votre serveur. Tous vos utilisateurs ont ce fingerprint dans leur ~/.ssh/known_hosts. La régénérer provoque une alerte REMOTE HOST IDENTIFICATION HAS CHANGED sur toutes les machines de vos utilisateurs.
Conséquence : sauvegardez la clé SSH hôte et ne la régénérez jamais sauf en cas de compromission avérée.
Alternatives et compromis
Pourquoi un seul binaire plutôt que des microservices ?
gitrust a choisi l’architecture monolithe modulaire (crates Rust séparées, un binaire) pour :
- Simplicité opérationnelle : un seul processus à surveiller, une seule unité de déploiement
- Performances : pas de sérialisation/désérialisation inter-services sur le chemin critique
- Cohérence transactionnelle : les transactions PostgreSQL couvrent plusieurs domaines (auth + repository + audit) sans coordinateur distribué
Le compromis : mise à l’échelle horizontale plus contrainte (voir Modèle de déploiement).
Pourquoi Russh plutôt que sshd ?
L’intégration du serveur SSH dans le processus gitrust permet :
- Authentification via la base de données (pas de
authorized_keysfichier) - Autorisation au niveau dépôt (pas juste au niveau système)
- Logs d’audit unifiés
Le compromis : gitrust gère sa propre implémentation SSH — les mises à jour de sécurité SSH dépendent des releases gitrust.
Vérifier votre compréhension
-
Un utilisateur ne peut plus faire
git pushmais peut se connecter à l’interface web. Quel composant est en cause ? Quelles informations cherchez-vous en premier dans les logs ? -
Vous devez augmenter la mémoire allouée aux workers CI sans affecter le serveur HTTP. Est-ce possible avec l’architecture actuelle ? Que faudrait-il changer ?
Pour aller plus loin
- Modèle de déploiement — topologies mono-machine et HA
- Ports et services — tableau des ports et bindings
- ssh-guard : détection d’attaques SSH
- Configurer ssh-guard
- Sauvegarder et restaurer
Comprendre les modèles de déploiement de gitrust
Ce que vous allez comprendre
- Comparer les trois topologies de déploiement (dev local, mono-machine prod, haute disponibilité) et leurs compromis
- Identifier les limites de la mise à l’échelle horizontale de gitrust dans sa version actuelle
- Évaluer quelle topologie correspond à votre contexte (taille d’équipe, contraintes de disponibilité, budget)
Le problème concret
Vous hébergez gitrust pour une équipe de 8 développeurs. Tout fonctionne bien, mais vous vous posez la question : faut-il un deuxième serveur pour la résilience ? Peut-on mettre gitrust derrière un load balancer ? Que se passe-t-il si le disque dur tombe ?
L’analogie
Les trois topologies ressemblent aux trois tailles de cuisine dans un restaurant :
- Cuisine de maison (dev local) : tout sur le même plan de travail, pratique, pas question d’accueillir 50 couverts
- Cuisine de restaurant (mono-machine prod) : séparation des postes, robuste, suffisant pour 20-50 couverts
- Cuisine industrielle (HA) : plusieurs cuisines qui travaillent en parallèle, mais la coordination est un métier à part entière
La plupart des équipes de 3-20 personnes n’ont besoin que de la cuisine de restaurant.
Le modèle
Topologie 1 — Développement local
graph LR
subgraph "Machine du développeur"
G[gitrust :4000 + :2222]
PG[(PostgreSQL :5432)]
FS[./data/repos]
end
DEV[Navigateur / git] --> G
G --> PG
G --> FS
Caractéristiques :
- Tout sur la même machine, tout en
localhost APP_DEBUG=true,COOKIE_SECURE=false,RUST_LOG=debug- PostgreSQL lancé via
docker compose up -ddansdatabase/ - Pas de reverse-proxy, pas de TLS
SESSION_BACKEND=memoryacceptable (données perdues au redémarrage)
Usage : test de fonctionnalités, développement de plugins, démonstrations offline.
Topologie 2 — Mono-machine production (recommandée)
graph LR
subgraph "Internet"
CLI[Navigateur / git client]
end
subgraph "Serveur unique"
NGX[Nginx :443 TLS]
G[gitrust 127.0.0.1:4000<br/>+ 0.0.0.0:2222]
PG[(PostgreSQL 127.0.0.1:5432)]
FS[/opt/gitrust/data/repos]
end
CLI -->|HTTPS :443| NGX
CLI -->|SSH :2222| G
NGX -->|proxy_pass| G
G --> PG
G --> FS
Caractéristiques :
- Nginx termine TLS et proxifie vers gitrust sur
127.0.0.1:4000 - PostgreSQL accessible uniquement en local
- Dépôts bare sur le disque local (ou volume attaché)
- Un seul processus gitrust, un seul PostgreSQL
- Sauvegarde avec
pg_dump+rsyncnocturne vers stockage externe
Dimensionnement indicatif :
| Équipe | vCPU | RAM | Disque |
|---|---|---|---|
| 1-5 personnes | 1-2 | 1 Go | 20 Go SSD |
| 6-20 personnes | 2-4 | 2-4 Go | 50-100 Go SSD |
| 20-50 personnes | 4-8 | 8 Go | 200+ Go SSD |
Limites : point de défaillance unique — si le serveur tombe, tout tombe. Acceptable si RTO < 1 h (restauration depuis sauvegarde sur nouvelle VM).
Topologie 3 — Haute disponibilité (HA)
graph LR
subgraph "Internet"
CLI[Navigateurs / git clients]
end
subgraph "Load balancer"
LB[HAProxy / Nginx LB<br/>:443 + :2222]
end
subgraph "Backends gitrust"
G1[gitrust-1 :4000 + :2222]
G2[gitrust-2 :4000 + :2222]
end
subgraph "Données partagées"
PG[(PostgreSQL primaire<br/>+ réplica lecture)]
NFS[Volume partagé NFS<br/>ou S3-compatible]
end
CLI --> LB
LB --> G1
LB --> G2
G1 --> PG
G2 --> PG
G1 --> NFS
G2 --> NFS
Caractéristiques :
- Plusieurs instances gitrust derrière un load balancer
- PostgreSQL en mode primary/replica (streaming replication)
SESSION_BACKEND=redisobligatoire (sessions partagées entre instances)- Dépôts bare sur volume partagé (NFS, CEPH, ou objet S3 via FUSE)
Limites de la mise à l’échelle horizontale
Workers stateful
Les workers CI et Import sont des goroutines async dans le processus gitrust. Leur état (pipelines en cours, file d’attente) n’est pas partagé entre instances. En HA, il faut s’assurer qu’un seul nœud exécute les workers CI, ou accepter que des pipelines soient redémarrés si le nœud tombe.
Contournement actuel : désigner un nœud « primary » pour les workers (CI_ENABLED=true sur un seul nœud, CI_ENABLED=false sur les autres).
Dépôts bare sur volume partagé
Les dépôts Git bare doivent être accessibles depuis tous les nœuds gitrust. Options :
- NFS : simple, suffisant pour la plupart des cas, mais latence réseau visible sur les gros push
- CEPH / GlusterFS : haute performance distribuée, complexité opérationnelle élevée
- S3-compatible (s3fs/mountpoint) : économique pour l’archivage, trop lent pour les accès Git fréquents
- Réplication rsync en quasi-temps-réel : simple mais non-atomique (risque de dépôt corrompu pendant une réplication)
Recommandation : pour la grande majorité des équipes (< 50 personnes), la topologie mono-machine avec une bonne stratégie de sauvegarde offre un rapport complexité/disponibilité bien meilleur que la HA.
PostgreSQL en réplication
La réplication streaming PostgreSQL (primary/replica) est bien documentée et fiable. Le failover automatique nécessite un outil supplémentaire (Patroni, repmgr). Sans failover automatique, la HA PostgreSQL n’apporte qu’un RPO bas (perte de données minimale), pas un RTO bas (indisponibilité réduite).
Alternatives et compromis
| Critère | Mono-machine | HA (2 nœuds) |
|---|---|---|
| Complexité opérationnelle | Faible | Élevée |
| Coût infra | Faible | 2-3x |
| RTO (temps de restauration) | 15-60 min (depuis sauvegarde) | < 5 min (failover) |
| RPO (perte de données max) | Depuis la dernière sauvegarde | Quelques secondes (WAL) |
| Maintenance sans coupure | Non (redémarrage = coupure) | Oui (rolling update) |
| Adapté pour | Équipes 1-50 pers. | Équipes > 50 pers. ou SLA strict |
Pour la cible principale de gitrust (équipes de 3-20 personnes) : la topologie mono-machine avec sauvegarde quotidienne offre un RTO acceptable. Investissez plutôt dans une bonne stratégie de sauvegarde que dans une architecture HA complexe.
Vérifier votre compréhension
-
Votre équipe passe de 10 à 40 personnes. Quels indicateurs surveilleriez-vous sur votre serveur mono-machine pour décider si une migration vers la HA est nécessaire ?
-
En topologie HA avec deux nœuds gitrust et un volume NFS partagé, quel est le scénario de défaillance qui ne peut pas être résolu par la HA seule ? Que faut-il prévoir en plus ?
Pour aller plus loin
- Architecture globale — composants et flux
- Stratégie de sauvegarde — 3-2-1 adapté à gitrust
- Ports et services
- Variables d’environnement —
SESSION_BACKEND,REDIS_URL
Comprendre la CI Dagger : Easy Mode et Power Mode
Ce que vous allez comprendre
- Expliquer pourquoi gitrust utilise Dagger plutôt qu’un système CI YAML classique
- Distinguer le « Easy Mode » (
.gitrust-ci.yml) du « Power Mode » (module.dagger/) - Évaluer quand conseiller à vos utilisateurs de passer de l’un à l’autre
Le problème concret
Un utilisateur vous signale que sa CI « ne fait pas ce qu’il veut » avec .gitrust-ci.yml. Il a besoin de logique conditionnelle, de dépendances entre jobs, et de construire plusieurs images Docker en parallèle. Devez-vous modifier le moteur CI ou lui dire qu’il doit changer d’approche ?
L’analogie
Easy Mode, c’est la recette de cuisine imprimée sur la boîte de gâteau : elle marche pour 95 % des cas, est accessible aux débutants, mais on ne peut pas y ajouter de la fleur d’oranger sans sortir du cadre.
Power Mode, c’est le livre de pâtisserie professionnel : vous écrivez vos propres recettes en code (Go, Python, TypeScript), vous pouvez tout faire, mais il faut savoir pâtisser.
Dagger est l’atelier qui garantit que les deux approches s’exécutent dans exactement le même environnement, reproductible sur n’importe quelle machine.
Le modèle
Pourquoi Dagger ?
Les systèmes CI YAML classiques (GitHub Actions, GitLab CI) ont un problème fondamental : la logique de pipeline est encodée dans du YAML qui devient vite illisible, non-testable, et non-composable. Dagger a été créé par l’équipe fondatrice de Docker pour résoudre ce problème.
graph LR
subgraph "CI YAML classique"
Y[.github/workflows/ci.yml<br/>GitLab CI / Jenkins]
Y --> E[Exécuté sur runner<br/>CI propriétaire]
E --> R1[Résultat dépend<br/>du runner]
end
subgraph "CI Dagger"
D[Pipeline en code<br/>Go / Python / TS]
D --> DE[Dagger Engine<br/>Docker containers]
DE --> R2[Reproductible<br/>local = CI = prod]
end
Avantages de Dagger pour gitrust :
- Reproductibilité : le même pipeline s’exécute identiquement en local et sur le serveur
- Isolation : chaque job dans son propre conteneur Docker
- Cache automatique : les layers Docker non modifiés ne sont pas reconstruits
- Pas de dépendance à un service CI externe (GitHub Actions, etc.)
Easy Mode : .gitrust-ci.yml
Le fichier .gitrust-ci.yml est un profil YAML simplifié interprété par le moteur CI générique de gitrust (un module Dagger Python interne à la plateforme).
flowchart LR
A[.gitrust-ci.yml<br/>dans le dépôt] -->|lecture| B[Moteur CI gitrust<br/>module Dagger Python]
B -->|dagger call| C[Dagger Engine]
C --> D[Container isolé<br/>build + test]
D --> E[Résultat → DB + UI]
Structure type :
# .gitrust-ci.yml
pipeline:
name: CI principale
image: rust:1.77-slim
steps:
- name: Vérifier le formatage
run: cargo fmt --check
- name: Linting
run: cargo clippy -- -D warnings
- name: Tests
run: cargo test --workspace
cache:
- path: ~/.cargo/registry
- path: target/
Ce que Easy Mode fait bien :
- Build + test + lint pour des projets simples à intermédiaires
- Configuration en quelques lignes
- Accessible aux utilisateurs sans expertise CI/CD
Limites du Easy Mode :
- Pas de logique conditionnelle complexe (si branche = main alors déployer)
- Pas de fan-out/fan-in (jobs parallèles avec synchronisation)
- Pas d’accès au Daggerverse (bibliothèque de modules Dagger communautaires)
- La logique reste dans le YAML de la plateforme, pas dans le code du projet
Power Mode : module .dagger/
Quand un dépôt contient un dossier .dagger/, gitrust détecte sa présence et exécute directement le module Dagger natif. La plateforme ne l’interprète pas : elle lance dagger call sur le module du projet.
flowchart LR
A[.dagger/<br/>main.go ou main.py] -->|détecté| B[gitrust CI Worker]
B -->|dagger call -m .dagger/ ci| C[Dagger Engine]
C --> D[Pipeline défini<br/>par l'utilisateur]
D --> E[Résultat → DB + UI]
Ce que Power Mode apporte :
- La logique pipeline est du code versionné avec le projet
- Tests unitaires des pipelines possibles
- Composition via le Daggerverse
- Accès à toutes les fonctionnalités Dagger (secrets, services, multi-platform builds)
- Aucune dépendance au moteur CI de gitrust pour la logique
Exemple minimal en Go :
// .dagger/main.go
package main
import (
"context"
"dagger.io/dagger"
)
func main() {
ctx := context.Background()
client, _ := dagger.Connect(ctx)
defer client.Close()
rust := client.Container().
From("rust:1.77-slim").
WithMountedCache("/usr/local/cargo/registry", client.CacheVolume("cargo-registry")).
WithDirectory("/src", client.Host().Directory(".")).
WithWorkdir("/src")
rust.WithExec([]string{"cargo", "fmt", "--check"}).
WithExec([]string{"cargo", "clippy", "--", "-D", "warnings"}).
WithExec([]string{"cargo", "test", "--workspace"}).
Sync(ctx)
}
Alternatives et compromis
| Critère | Easy Mode (.gitrust-ci.yml) | Power Mode (.dagger/) |
|---|---|---|
| Accessibilité | Excellent (quelques lignes YAML) | Courbe d’apprentissage |
| Puissance | Limitée aux cas simples | Illimitée |
| Logique conditionnelle | Non | Oui |
| Parallélisme avancé | Non | Oui |
| Testabilité du pipeline | Nulle | Totale |
| Composition (Daggerverse) | Non | Oui |
| Maintenance | Moteur géré par la plateforme | Chaque dépôt est autonome |
| Alignement philosophie Dagger | Partiel (YAML encode de la logique) | Total |
Recommandation pour vos utilisateurs :
- Commencer en Easy Mode pour les projets simples
- Migrer en Power Mode quand : besoin de logique conditionnelle, de fan-out/fan-in, d’intégration avec des services externes, ou quand le
.gitrust-ci.ymldépasse 50 lignes
Implications pour l’administration
En tant qu’administrateur :
CI_ENABLED=falsedésactive toute exécution CI (Easy et Power Mode), utile lors d’une maintenanceCI_MAX_CONCURRENTs’applique aux deux modes- Le moteur Easy Mode (
CI_ENGINE_PATH) doit être synchronisé lors des mises à jour de gitrust - Les modules Power Mode sont autonomes — leur mise à jour est la responsabilité de l’utilisateur
Vérifier votre compréhension
-
Un utilisateur vous demande d’ajouter une étape « déploiement vers staging si la branche est
main» dans son.gitrust-ci.yml. Quelle est la réponse correcte ? Quelle alternative proposez-vous ? -
En quoi le fait que Dagger garantisse la reproductibilité facilite-t-il le débogage d’un pipeline qui échoue uniquement sur le serveur et pas en local ?
Pour aller plus loin
Comprendre la stratégie de sauvegarde adaptée à gitrust
Ce que vous allez comprendre
- Appliquer la règle 3-2-1 aux trois sources de vérité de gitrust (base de données, dépôts bare, clé SSH hôte)
- Analyser les RPO et RTO réalistes pour une instance gitrust mono-machine
- Évaluer les compromis entre
pg_dumpclassique et réplication WAL en continu
Le problème concret
Votre serveur gitrust est hors service suite à une défaillance disque. Vous avez une sauvegarde de la base de données, mais pas des dépôts bare. Ou l’inverse. Combien de temps pour remettre l’instance en service ? Combien de données perdez-vous ?
L’analogie
Imaginez gitrust comme un cabinet d’architecte :
- La base de données est le registre des contrats et des clients (qui possède quoi, les métadonnées)
- Les dépôts bare sont les tiroirs de plans (les fichiers réels)
- La clé SSH hôte est la plaque officielle à l’entrée (l’identité du cabinet)
Si vous avez le registre mais pas les tiroirs, vous savez que le projet « Maison Martin » existe mais vous n’avez plus les plans. Si vous avez les tiroirs mais pas le registre, vous avez des dossiers anonymes sans savoir à qui ils appartiennent. Les deux doivent être sauvegardés ensemble.
Le modèle
La règle 3-2-1 appliquée à gitrust
graph TB
subgraph "3 copies"
C1[Copie 1 : données en production<br/>PostgreSQL + dépôts bare]
C2[Copie 2 : sauvegarde locale<br/>/var/backups/gitrust]
C3[Copie 3 : stockage distant<br/>S3 / NAS / autre serveur]
end
subgraph "2 supports différents"
S1[Disque SSD serveur principal]
S2[Disque NAS ou volume objet]
end
subgraph "1 copie hors site"
O1[Datacenter différent<br/>ou cloud object storage]
end
C1 --> S1
C2 --> S1
C3 --> S2
S2 --> O1
Les 3 copies :
- Production (données en vie sur le serveur)
- Sauvegarde locale (
/var/backups/gitrustsur le même serveur ou volume attaché) - Sauvegarde distante (rsync vers NAS,
rclonevers S3, etc.)
Les 2 supports : le disque du serveur principal + un support physiquement distinct.
1 hors site : en cas d’incendie, d’inondation ou de compromission du datacenter, une copie doit être inaccessible depuis le réseau principal.
Les trois sources de vérité de gitrust
| Source | Taille typique | Consistance | Fréquence recommandée |
|---|---|---|---|
PostgreSQL (pg_dump) | Quelques Mo à plusieurs Go | Cohérente à un instant T (--format=custom) | Quotidien minimum, idéalement 2x/jour |
| Dépôts bare (rsync) | Dizaines de Mo à plusieurs To | Cohérente par dépôt (Git garantit l’atomicité des packs) | Quotidien minimum |
Clé SSH hôte (ssh_host_ed25519_key) | < 1 Ko | Immuable après le premier démarrage | Une seule fois, puis conserver en lieu sûr |
RPO et RTO réalistes
RPO (Recovery Point Objective) : quantité maximale de données perdues.
| Stratégie | RPO |
|---|---|
pg_dump quotidien à 2h | ≤ 24 h de données perdues |
pg_dump toutes les 6 h | ≤ 6 h de données perdues |
| WAL streaming (wal-g) | ≤ quelques secondes |
Pour la cible principale de gitrust (équipes 3-20 personnes), un RPO de 24 h est généralement acceptable. Si votre équipe code intensément la nuit, envisagez 2 sauvegardes quotidiennes.
RTO (Recovery Time Objective) : temps pour remettre l’instance en service après incident.
| Scénario | RTO estimé |
|---|---|
| Restauration sur la même machine (disque remplacé) | 30-60 min |
| Restauration sur une nouvelle VM (même datacenter) | 15-30 min |
| Restauration sur une nouvelle VM (nouveau datacenter) | 60-90 min (transfert des sauvegardes) |
pg_dump vs réplication WAL
graph LR
subgraph "pg_dump (snapshot)"
A1[pg_dump à 2h00] --> B1[Fichier .dump]
B1 --> C1[Transfert vers NAS]
C1 --> D1[RPO ≤ 24h]
end
subgraph "WAL streaming (continu)"
A2[PostgreSQL primary] -->|WAL segments| B2[wal-g / pgbackrest]
B2 -->|continu| C2[Stockage objet S3]
C2 --> D2[RPO ≤ quelques secondes]
end
pg_dump — recommandé pour commencer :
- Simple à mettre en place et à tester
- Fichier standalone restaurable avec
pg_restore - RPO = interval entre deux dumps
- Fonctionne même si PostgreSQL est sous charge (cohérence transactionnelle garantie par PostgreSQL)
WAL streaming (wal-g, pgbackrest) — pour les cas avancés :
- RPO quasi nul (Point-in-Time Recovery)
- Nécessite un stockage objet (S3, MinIO)
- Complexité opérationnelle significativement plus élevée
- Indispensable si votre activité impose un RPO < 1 h
Recommandation : commencez par pg_dump quotidien + rsync. Migrez vers WAL streaming uniquement si votre SLA l’exige.
Cohérence entre base de données et dépôts bare
La sauvegarde la plus dangereuse est celle qui est partiellement réussie : base sauvegardée, dépôts non sauvegardés (ou l’inverse).
Un dépôt existe à deux endroits :
- En base :
repositories.disk_pathenregistre le chemin - Sur disque : le dossier
.gitbare contient les objets Git
Pour éviter l’incohérence, sauvegardez toujours les deux dans la même fenêtre temporelle. Le script backup.sh recommandé effectue les deux en séquence :
# Ordre correct : d'abord la DB, puis les dépôts
pg_dump → fichier .dump
rsync repos → destination
En pratique, le décalage entre les deux (quelques secondes/minutes) est acceptable : un dépôt créé entre les deux sauvegardes sera absent du rsync mais présent en base — une re-synchronisation après restauration suffit.
Tester la restauration (le drill)
Une sauvegarde non testée n’est pas une sauvegarde — c’est une espérance.
Planifiez un drill trimestriel sur une VM éphémère :
- Copier les fichiers de sauvegarde sur la VM
- Installer gitrust et PostgreSQL
- Restaurer avec les procédures documentées dans Sauvegarder et restaurer
- Vérifier : connexion admin, liste des dépôts,
git cloned’un dépôt connu - Mesurer le temps effectif → comparer avec votre RTO cible
- Documenter les étapes imprévues → améliorer le script
Le drill révèle les problèmes invisibles : fichier de sauvegarde corrompu, script qui suppose un chemin qui n’existe pas, mot de passe PostgreSQL non documenté.
Alternatives et compromis
| Approche | RPO | Complexité | Coût |
|---|---|---|---|
pg_dump quotidien + rsync | ≤ 24 h | Faible | Faible |
pg_dump 2x/jour + rsync | ≤ 12 h | Faible | Faible |
| WAL streaming + rsync | ≤ 1 min | Élevée | Moyen (stockage objet) |
| Réplication PostgreSQL (HA) + rsync | ≤ quelques s | Très élevée | Élevé |
Pour les équipes de 3-20 personnes avec gitrust, la première ligne est le bon point de départ.
Vérifier votre compréhension
-
Vous utilisez
pg_dumpquotidien à 2h00. Un développeur pousse 3 jours de travail le mercredi soir à 23h45. Le serveur tombe à minuit. Que perdez-vous, et pourquoi ? -
Quelle est la différence entre RPO et RTO ? Lequel est le plus facile à améliorer avec une architecture mono-machine ? Lequel nécessite une refonte architecturale ?
Pour aller plus loin
- Sauvegarder et restaurer — procédures pas-à-pas
- Modèle de déploiement — HA et ses implications sur la sauvegarde
- Architecture globale — les trois sources de vérité
ssh-guard : détection d’attaques SSH
Ce que vous allez comprendre
- Identifier les quatre motifs d’attaque détectés automatiquement et les seuils par défaut.
- Analyser comment ssh-guard décide de bannir une IP, dans quel ordre les règles s’appliquent et pourquoi.
- Évaluer les compromis (fail-open, dry-run, allowlist bypass) pour ajuster la posture de sécurité de votre instance.
Public : administrateurs gitrust qui veulent comprendre avant de toucher aux variables
SSH_GUARD_*. Pour la recette pas à pas, voir Configurer ssh-guard.
1. Le problème concret
Vous administrez une instance gitrust exposée sur Internet. Les logs russh remontent des dizaines de tentatives d’authentification ratées chaque jour, sans corrélation. Vous savez intuitivement qu’il y a une attaque par dictionnaire en cours, mais :
- vous ne savez pas combien d’IP distinctes la mènent ;
- vous ne pouvez pas distinguer un développeur qui a 3 clés mal configurées d’un scanner qui essaie 50 clés en série ;
- votre fail2ban actuel se base sur des regex sur les messages d’erreur de
russh, qui changent à chaque mise à jour ; - vous craignez de bannir un partenaire CI qui ouvre 200 connexions SSH par minute pour ses pipelines.
ssh-guard a été conçu exactement pour ce périmètre : observer, classer, corréler, bannir — sans casser les utilisations légitimes intensives.
2. L’analogie
Imaginez le hall d’entrée d’un immeuble. Quatre comportements doivent déclencher un signal pour le gardien :
- Quelqu’un sonne 5 fois en 5 minutes au même appartement → c’est une tentative de force brute. (brute-force)
- Quelqu’un sonne aux 10 appartements différents en 5 minutes → il cherche qui est chez lui pour cibler ensuite. (énumération d’utilisateurs)
- Quelqu’un présente 10 cartes magnétiques différentes en 5 minutes → il essaie un trousseau volé. (scanning de clés)
- Quelqu’un ouvre la porte 30 fois en 1 seconde → il sature le système, peu importe ses intentions. (flood TCP)
ssh-guard distingue les quatre, parce qu’ils appellent des réponses différentes. Le flood est un drop instantané (sinon le système se noie). Les trois autres déclenchent un ban temporaire après un nombre d’essais avéré.
3. Les quatre détecteurs
3.1 Vue synthétique
| Détecteur | Mesure | Seuil défaut | Fenêtre | Action |
|---|---|---|---|---|
| Brute-force | Échecs d’auth depuis une IP | 5 | 5 min | Ban 1 h |
| Énumération d’utilisateurs | Usernames distincts essayés depuis une IP | 10 | 5 min | Ban 1 h |
| Scan de clés | Fingerprints distincts essayés depuis une IP | 10 | 5 min | Ban 1 h |
| Flood TCP | Nouvelles connexions TCP par IP par seconde | 10 (burst 20) | 1 s glissante | Drop immédiat (pas de ban persistant) |
Tous les seuils sont ajustables via SSH_GUARD_* — voir Variables d’environnement.
3.2 Brute-force
Compte les événements auth_failed par IP dans la fenêtre glissante. Au seuil, émet brute_force_detected puis pose un ban auto avec TTL SSH_GUARD_AUTO_BAN_DURATION_SECS. Ne compte pas les auth_succeeded (c’est l’objectif : ignorer le bruit légitime).
3.3 Énumération d’utilisateurs
Compte les usernames distincts essayés par IP. Un attaquant qui sonde root, admin, git, deploy, ci, bot, … pour trouver un compte existant déclenche ce détecteur même si chacune de ses tentatives n’est faite qu’une fois.
Cas typiques de faux positif : un script CI qui clone plusieurs dépôts d’orgs différentes avec différents git@host:org/... — mais le user SSH reste git, donc le détecteur ne s’active pas. Un développeur qui se trompe trois fois de username en jouant avec son ~/.ssh/config reste largement sous le seuil.
3.4 Scan de clés
Compte les fingerprints SHA256 distincts présentés par IP. Aligné sur les recommandations CrowdSec : un humain n’a jamais 10 clés SSH distinctes en 5 minutes. Un trousseau volé qui défile en automatique, oui.
Cas typique d’observation : ssh-add -L côté client liste 8 clés, l’agent SSH les présente toutes successivement. Le seuil par défaut (10) absorbe ce cas.
3.5 Flood TCP
Implémenté avec un token bucket GCRA (algorithme de Generic Cell Rate, équivalent leaky bucket sans biais), keyé par IP. Différent des trois autres :
- Pas de ban persistant : sinon un scan de port créerait des milliers de lignes inutiles dans la DB.
- Drop au niveau du listener TCP : la connexion ne consomme aucune ressource au-delà du
accept(). - Budgets indépendants par IP : une IP rate-limitée n’affecte pas les autres.
C’est la première ligne de défense, peu coûteuse, qui absorbe les scans avant que les détecteurs comportementaux ne soient sollicités.
4. Le modèle de décision
4.1 Priorité des règles d’ACL
Quand une connexion arrive, ssh-guard consulte dans cet ordre :
flowchart LR
A[Connexion] --> B{Denylist admin ?}
B -- Oui --> X1[Drop banned]
B -- Non --> C{Ban auto actif ?}
C -- Oui --> X1
C -- Non --> D{Allowlist admin ?}
D -- Oui --> Y[Accept,<br/>bypass détecteurs]
D -- Non --> E{Flood OK ?}
E -- Non --> X2[Drop flood_limit]
E -- Oui --> Z[Accept normal]
Conséquences pratiques :
denyadmin > tout : un opérateur peut bannir une IP même si elle a été allowlistée par erreur ailleurs.auto_ban>allow: un ban posé par un détecteur ne saute pas en allowlistant l’IP après coup. Pour libérer l’IP, il faut explicitement la débannir. C’est volontaire — unallowne doit pas effacer la trace d’un comportement passé.allow>default: une IP allowlistée bypasse les détecteurs et le flood-limit, mais ses événementsauth_succeeded/auth_failedrestent loggés pour audit.
4.2 Cycle de vie d’un ban brute-force
sequenceDiagram
participant C as Client SSH (203.0.113.42)
participant L as SecureListener
participant T as AuthTracker
participant D as BruteForceDetector
participant B as BanManager
participant S as Sink JSON
participant F as Fail2ban
Note over C,F: Tentatives 1 à 4 (sous le seuil)
C->>L: TCP accept
L->>S: connection_accepted
C->>T: auth fail (russh)
T->>S: auth_failed
T->>D: on_event(auth_failed)
D->>D: count = 4 < seuil 5
Note over D: Aucun ban
Note over C,F: Tentative 5 (atteint le seuil)
C->>T: auth fail (russh)
T->>S: auth_failed
T->>D: on_event(auth_failed)
D->>D: count = 5 >= seuil
D->>S: brute_force_detected
D->>B: auto_ban(BruteForce)
B->>S: ip_banned (expires_at = now + 1h)
F->>F: Lit ip_banned, applique iptables/UFW
Note over C,F: Tentative 6 (bannie)
C->>L: TCP accept
L->>B: effective_status(203.0.113.42)
B-->>L: BannedAuto
L->>S: connection_dropped (banned)
L-->>C: connexion fermée
4.3 Idempotence
Un détecteur ne rebannit jamais une IP déjà bannie. Le BanManager.auto_ban retourne Ok(None) dans trois cas :
dry_runactif (un événementip_bannedest tout de même émis pour fail2ban) ;- l’IP est allowlistée ;
- un ban auto actif couvre déjà l’IP.
Sans cette idempotence, chaque nouvelle tentative ratée après le ban produirait un nouvel ip_banned et noierait le sink.
5. Les leviers admin
5.1 Modes opérationnels
| Variable | Effet |
|---|---|
SSH_GUARD_ENABLED=false | Coupe totale. ssh-guard devient un pass-through. À éviter sauf urgence. |
SSH_GUARD_DRY_RUN=true | Les détecteurs tournent et émettent les événements ip_banned, mais aucun ban n’est persisté. Mode parfait pour valider un nouveau seuil ou laisser fail2ban faire le ban réel. |
SSH_GUARD_PROFILE=private | Profil réseau interne : tous les détecteurs désactivés (seuil u32::MAX), les événements continuent d’être émis pour audit. |
5.2 Allowlist d’un partenaire CI ou d’un VPN
Une IP allowlistée bypasse les quatre détecteurs et le flood-limit. C’est le levier propre pour autoriser :
- un runner CI qui ouvre des dizaines de connexions SSH par minute ;
- un bastion VPN qui multiplexe plusieurs développeurs vers une seule IP source ;
- un job de monitoring qui teste la disponibilité SSH.
Configuré via l’API admin (table ssh_guard_acl, kind = Allow, CIDR du runner). Voir Configurer ssh-guard.
5.3 Denylist d’un AS abusif
Un opérateur peut bannir un CIDR entier (ex. 198.51.100.0/24) en mode permanent. C’est le pendant inverse de l’allowlist, prioritaire sur tout (y compris les bans auto qui n’auraient pas encore été posés).
6. Pourquoi ces compromis
6.1 Fail-open quand PostgreSQL est indisponible
Si la lecture du store échoue (DB down, latence anormale), ssh-guard laisse passer la connexion et log un warn. Raison : éviter de transformer une panne DB en panne SSH généralisée. Conséquence acceptée : pendant la fenêtre de panne, un attaquant peut atteindre le handshake russh (mais doit toujours présenter une clé valide pour s’authentifier).
Pour basculer en fail-closed, il faut modifier le code (voir page développeur). C’est rare dans la vraie vie ; la plupart des admins préfèrent la disponibilité.
6.2 Bans temporaires plutôt que permanents
Le défaut SSH_GUARD_AUTO_BAN_DURATION_SECS=3600 (1 heure) est un compromis :
- assez long pour ralentir significativement un attaquant patient ;
- assez court pour qu’un faux positif (un script mal écrit chez un utilisateur légitime) se résolve sans intervention admin.
Mettre 0 rend les bans permanents — à n’utiliser qu’avec un workflow admin pour purger régulièrement la table.
6.3 Fenêtre glissante de 5 minutes
Suffisamment large pour qu’un attaquant ne puisse pas « réinitialiser » le compteur en attendant 30 secondes entre tentatives. Suffisamment courte pour qu’un développeur qui se trompe 4 fois de mot de passe à 8h, puis revient à 14h, ne déclenche pas de ban.
6.4 Allowlist qui bypasse les détecteurs
Une IP allowlistée n’est plus surveillée par les détecteurs. Donc une compromission depuis une IP « de confiance » ne sera pas détectée par ssh-guard. Le compromis est explicite : l’allowlist est un signal de confiance opérationnelle, pas une garantie de sécurité. Si un partenaire CI est compromis, l’attaque sera vue par les logs auth_succeeded (qui sont toujours émis), pas par les détecteurs.
7. Quand ssh-guard ne suffit pas
ssh-guard est une couche de défense. Elle ne dispense pas de :
- un firewall en amont (UFW, iptables) qui ferme les ports inutiles ;
- des clés SSH fortes (ed25519, ou RSA 4096+) côté utilisateur ;
- une politique de rotation des PAT pour l’API HTTP (gitrust ne sert pas que SSH) ;
- un fail2ban pour l’HTTP (login web, API), couvert par d’autres jails — voir Durcir avec Fail2ban.
Les quatre motifs d’attaque détectés sont les plus fréquents au niveau SSH, mais ils ne couvrent pas : attaques par chronométrie, exfiltration via tunnel, abus de privilèges après auth réussie. Ces vecteurs nécessitent d’autres outils (audit syscalls, EDR, revue de RBAC).
8. Vérifier votre compréhension
- Une IP allowlistée présente 50 fingerprints différents en 30 secondes. Combien d’événements
key_scanning_detectedsont émis ? L’IP est-elle bannie ? - Vous activez
SSH_GUARD_DRY_RUN=truepuis vous voyez 12 événementsip_banneddans la dernière heure. Combien d’IP sont effectivement bloquées par ssh-guard ? Que faut-il pour qu’elles soient bloquées ? - PostgreSQL est inaccessible pendant 10 minutes. Une attaque brute-force depuis une IP nouvelle démarre à la 5e minute. (a) Le détecteur va-t-il déclencher un ban ? (b) Les événements
auth_failedapparaissent-ils dans les logs ?
9. Pour aller plus loin
- Configurer ssh-guard — recettes par profil de déploiement
- Événements ssh-guard (JSON) — schéma stable du flux d’événements
- Variables d’environnement — SSH_GUARD_*
- Durcir avec Fail2ban — chaîner ssh-guard et fail2ban
- Architecture globale — où ssh-guard se place dans le runtime gitrust
- Conformité ANSSI PA-074
Manuel développeur
Ce manuel s’adresse à deux publics : les contributeurs au code gitrust (core ou via fork rustwarden) et les développeurs externes qui construisent des intégrations sur l’API publique. Il fournit tout le nécessaire pour comprendre l’architecture des 6 crates, contribuer du code conforme aux standards du projet, ou bâtir un client qui parle à l’API gitrust. Il ne couvre pas l’installation d’une instance de production ni l’usage quotidien de la forge.
Parcours d’apprentissage — contributeur core
Le parcours contributeur représente environ 4 heures au total, avec un étayage dégressif : le premier tutoriel est entièrement guidé, le troisième (capstone) demande de résoudre sans guide pas-à-pas.
graph LR
A[Utilisateur autonome] --> B[01-getting-started<br/>45 min]
B --> C[02-premiere-contribution<br/>90 min]
C --> D[03-creer-un-worker-async<br/>capstone 120 min]
D --> E[Core contributor]
Checkpoint de parcours : avant de passer au tutoriel 02, vous devez avoir compilé gitrust localement, lancé les tests unitaires et obtenu une instance de développement fonctionnelle. Si ce n’est pas le cas, relisez le tutoriel 01.
Checkpoint de parcours : avant le tutoriel 03 (capstone), vous devez avoir soumis une PR complète avec tests, passé la gate QA et reçu une review. Si ce n’est pas le cas, relisez le tutoriel 02.
Tutoriels — apprendre en faisant
Les tutoriels sont guidés pas-à-pas avec des sorties verbatim attendues. Suivez-les dans l’ordre — chacun suppose le précédent complété.
- 01 — Getting started : build local, tests, instance de dev — ~45 min
- 02 — Première contribution : clone, build, test, PR — ~90 min
- 03 — Créer un worker async (capstone) — ~120 min
How-to — recettes pour les tâches courantes
Les how-to répondent à « comment faire X » sans explication de fond. Consultez-les dans n’importe quel ordre selon votre besoin.
- Workflow de contribution : Git + review + QA
- Lancer les tests unitaires
- Lancer les tests E2E (Playwright)
- Ajouter une route web (patron rustwarden → axum)
- Ajouter un service métier
- Ajouter une migration de base de données
- Implémenter des webhooks
- Implémenter un endpoint API
- Importer un dépôt externe (code)
- Contribuer à rustwarden upstream
- Passer la QA avant merge
Référence — informations techniques exactes
La référence documente de façon exhaustive l’architecture, les API internes, le schéma de données et les règles QA. Destinée à la consultation ponctuelle.
- Architecture des 6 crates
- Services et API interne
- Schéma de base de données (ERD)
- Règles QA et gates ANSSI
- API REST v1 — auth, pagination, codes d’erreur
- Git hooks post-receive
- rustwarden-core API — helpers, extracteurs, middleware
Explication — comprendre le pourquoi
Les explanations construisent les modèles mentaux nécessaires pour contribuer de façon éclairée. Lisez-les quand vous voulez comprendre les décisions d’architecture, pas seulement les appliquer.
- Vue d’ensemble de l’architecture gitrust
- Flux de requêtes (diagrammes de séquence)
- Modèle de permissions RBAC 4 niveaux
- Fonctionnement de la 2FA
- Hiérarchie des labels
- CI Dagger : Easy Mode vs Power Mode
- Décisions UI : HTMX + DaisyUI
- Patron worker async
Tutoriels
Les tutoriels sont des parcours d’apprentissage guidés. Vous suivez une série d’étapes précises pour construire quelque chose de concret. L’objectif n’est pas de comprendre en profondeur chaque décision — c’est d’acquérir la confiance de faire fonctionner gitrust entre vos mains.
Diátaxis — mode apprentissage : chaque tutoriel est orienté vers l’expérience pratique. Vous apprendrez en faisant, pas en lisant.
Parcours recommandé
flowchart LR
T01["01 — Installer\nl'environnement\n~45 min"]
T02["02 — Première\ncontribution\n~90 min"]
T03["03 — Worker\nasync\n~120 min"]
T01 --> T02
T02 --> T03
style T01 fill:#4ade80,color:#000
style T02 fill:#60a5fa,color:#000
style T03 fill:#f472b6,color:#000
Chaque tutoriel indique ses prérequis au début. Ne sautez pas un niveau sans avoir vérifié les prérequis — les étapes suivantes supposent que vous avez les outils installés et un dépôt fonctionnel en local.
Les tutoriels
01 — Installer l’environnement de développement
Durée estimée : 45 minutes | Niveau : débutant
Vous installez Rust, PostgreSQL, et les outils gitrust. À la fin, vous lancez l’application en local et vous voyez la page d’accueil.
Prérequis : Linux ou macOS, accès sudo.
02 — Première contribution : corriger un bug
Durée estimée : 90 minutes | Niveau : intermédiaire
Vous identifiez un bug réel, créez une branche, écrivez un test qui échoue (TDD), corrigez le code, passez la QA, et ouvrez une Pull Request.
Prérequis : tutoriel 01 terminé, fork du dépôt gitrust.
03 — Créer un worker async de bout en bout
Durée estimée : 120 minutes | Niveau : avancé
Tutoriel capstone. Vous construisez un worker complet : migration DB, struct d’état, canal mpsc, Semaphore, handler SSE, et tests d’intégration.
Prérequis : tutoriels 01 et 02 terminés, connaissance de base de Rust async.
Ce que les tutoriels ne font pas
Les tutoriels ne vous expliquent pas pourquoi les choses fonctionnent ainsi. Pour comprendre les décisions d’architecture, consultez la section Explication. Pour accomplir une tâche spécifique sans apprentissage guidé, consultez les Guides pratiques.
Premiers pas avec gitrust : compilez, configurez et poussez votre premier commit
Source : ce tutoriel intègre le contenu de
gitrust/docs/GETTING_STARTED.mdavec la structure pédagogique complète (objectifs Bloom, modèle mental, checkpoints observables, section « Et si ça ne marche pas »).
Objectifs
À la fin de ce tutoriel, vous saurez :
- O1. Compiler gitrust depuis les sources et lancer l’instance de développement localement
- O2. Configurer l’environnement de développement (base de données, variables d’environnement, assets CSS)
- O3. Exécuter un premier push SSH vers votre instance locale et vérifier le résultat dans l’interface web
Pré-requis
- Technique : Rust stable ≥ 1.80 (
rustc --version), PostgreSQL ≥ 15 (psql --version), Node.js ≥ 18 (node --version), Git 2.x (git --version), Docker (optionnel, recommandé pour la base de données) - Pédagogique : être un utilisateur autonome de gitrust (parcours
user_manualcomplété ou équivalent) ; familiarité avec Rust (au moins 3 mois de pratique) et avec HTTP/PostgreSQL - Temps estimé : ~45 minutes
Vue d’ensemble
Avant d’exécuter la première commande, prenons 3 minutes pour comprendre ce que vous allez monter et comment les pièces s’articulent.
gitrust est un monorepo Rust à 6 crates qui compile en un seul binaire. Quand vous lancez cargo run, ce binaire démarre deux serveurs simultanément : un serveur HTTP (:4000) pour l’interface web et l’API REST, et un serveur SSH (:2222) pour les opérations git push/pull. Ils partagent la même base de données PostgreSQL et le même stockage de dépôts bare sur disque.
graph TB
subgraph sources ["Sources gitrust (monorepo Rust)"]
C1[rustwarden-core<br/>auth, sessions, middleware]
C2[gitrust-core<br/>modèles, services métier]
C3[gitrust-git<br/>opérations Git bare]
C4[gitrust-web<br/>handlers HTTP + templates]
C5[gitrust-ssh<br/>serveur SSH Git]
C6[gitrust-hooks<br/>post-receive, pre-receive]
end
subgraph runtime ["Processus unique (cargo run)"]
HTTP["HTTP :4000<br/>UI + API REST"]
SSH["SSH :2222<br/>git push/pull"]
end
subgraph storage ["Stockage"]
PG[(PostgreSQL<br/>comptes, dépôts,<br/>issues, PRs)]
REPOS[Dépôts bare<br/>data/repos/]
end
C1 & C2 & C3 & C4 & C5 & C6 -->|cargo build| HTTP
C1 & C2 & C3 & C4 & C5 & C6 -->|cargo build| SSH
HTTP & SSH --> PG
HTTP & SSH --> REPOS
Ce que vous allez faire : cloner les sources → préparer PostgreSQL → copier et remplir .env → compiler les assets CSS → lancer cargo run → vérifier l’instance → pousser un premier commit via SSH.
Étape 1 : Vérifiez les pré-requis système
Avant de cloner quoi que ce soit, vérifiez que tous les outils nécessaires sont installés :
rustc --version && cargo --version && psql --version && node --version && git --version
Sortie attendue (versions exactes peuvent varier) :
rustc 1.82.0 (f6e511eec 2024-10-15)
cargo 1.82.0 (8f40fc59f 2024-10-15)
psql (PostgreSQL) 16.3
v20.11.0
git version 2.43.0
Si des dépendances système manquent, installez-les :
# Debian / Ubuntu
sudo apt install build-essential pkg-config libssl-dev libpq-dev cmake git
# macOS
brew install postgresql openssl cmake
Checkpoint : toutes les commandes de vérification doivent retourner un numéro de version sans erreur. Si rustc n’est pas trouvé, installez Rust via curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh puis rechargez votre shell.
Étape 2 : Clonez le projet
git clone https://demo.gitrust.eu/gitrust/gitrust.git
cd gitrust
Sortie attendue :
Cloning into 'gitrust'...
remote: Enumerating objects: 12847, done.
remote: Counting objects: 100% (12847/12847), done.
Receiving objects: 100% (12847/12847), 18.42 MiB | 4.20 MiB/s, done.
Checkpoint : vérifiez que la structure de crates est présente :
ls crates/
Sortie attendue :
gitrust-core gitrust-git gitrust-hooks gitrust-ssh gitrust-web rustwarden-core
Étape 3 : Configurez l’environnement (.env)
Copiez le fichier d’exemple et éditez les 5 valeurs essentielles :
cp .env.example .env
Ouvrez .env dans votre éditeur et renseignez ces valeurs :
# Connexion à la base de données
DATABASE_URL=postgres://gitrust:motdepasse@localhost:5432/gitrust
# Clé secrète JWT (générez-la avec : openssl rand -hex 32)
JWT_SECRET=votre_cle_secrete_64_caracteres
# Compte administrateur créé au premier lancement
ADMIN_USERNAME=admin
ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=MotDePasseFort123!
Les autres variables ont des valeurs par défaut raisonnables pour le développement. Consultez
.env.examplepour la liste complète.
Générez une vraie valeur pour JWT_SECRET :
openssl rand -hex 32
Sortie attendue (valeur différente à chaque exécution) :
4a7f2c9e1b8d3f6a0e5c2b9d7f4a1e8c3b6d9f2a5e8c1b4d7f0a3e6c9b2d5f8a
Copiez cette valeur dans .env pour JWT_SECRET.
Checkpoint : vérifiez qu’aucune valeur d’exemple reste dans .env :
grep -E "votre_cle|example\.com|MotDePasse" .env
La commande doit retourner vide (aucune ligne). Si elle retourne des lignes, ces valeurs placeholder sont encore présentes — l’application démarrera mais avec une configuration non sécurisée.
Étape 4 : Préparez la base de données PostgreSQL
Vous avez deux options :
Option A — Docker (recommandé pour débuter) :
cd database && docker compose up -d && cd ..
Sortie attendue :
[+] Running 2/2
✔ Container gitrust_postgres_dev Started 0.8s
✔ Container gitrust_pgadmin Started 1.1s
Option B — PostgreSQL existant sur la machine :
psql -U postgres << 'SQL'
CREATE USER gitrust WITH PASSWORD 'motdepasse';
CREATE DATABASE gitrust OWNER gitrust;
SQL
Sortie attendue :
CREATE ROLE
CREATE DATABASE
Adaptez le mot de passe dans DATABASE_URL dans .env si vous utilisez l’option B.
Checkpoint : vérifiez que la base est accessible depuis l’application :
psql "$DATABASE_URL" -c "SELECT version();"
Sortie attendue :
version
-------------------------------------------------------------------------------------------------------
PostgreSQL 16.3 on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 13.2.0-23ubuntu4) 13.2.0, 64-bit
(1 row)
Si vous obtenez psql: error: connection to server failed, vérifiez que PostgreSQL tourne et que DATABASE_URL correspond.
Étape 5 : Compilez les assets CSS
gitrust utilise Tailwind CSS + DaisyUI. Les assets doivent être compilés localement — aucun CDN, tous les fichiers sont servis depuis static/ :
npm install
npx tailwindcss -i static/css/input.css -o static/css/style.css --minify
Sortie attendue :
added 98 packages in 3s
Rebuilding...
Done in 1842ms.
Checkpoint : vérifiez que le fichier CSS est généré :
ls -lh static/css/style.css
Sortie attendue :
-rw-r--r-- 1 vous vous 45K avr 17 10:15 static/css/style.css
Si le fichier fait 0 octet ou n’existe pas, la compilation a échoué — relancez npm install puis la commande npx tailwindcss.
Étape 6 : Lancez l’application
cargo run
La première compilation prend 2 à 5 minutes (toutes les dépendances sont compilées). Les compilations suivantes sont incrémentales et beaucoup plus rapides.
Au premier lancement, gitrust exécute automatiquement :
- Les migrations de base de données (création de toutes les tables)
- La création du compte administrateur (depuis
.env) - La génération de la clé SSH host (
data/ssh_host_ed25519_key) - La création du répertoire de stockage des dépôts (
data/repos/)
Sortie attendue (après compilation) :
Compiling gitrust-web v0.9.0 (/home/vous/gitrust/crates/gitrust-web)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 187.42s
Running `target/debug/gitrust`
2026-04-17T10:20:00Z INFO gitrust > Starting gitrust v0.9.0
2026-04-17T10:20:00Z INFO gitrust > Running database migrations... applied 42
2026-04-17T10:20:01Z INFO gitrust > Admin account created: admin
2026-04-17T10:20:01Z INFO gitrust > SSH host key: SHA256:xxxxxxxxxxxxxxxxxxxx
Server listening on http://0.0.0.0:4000
Checkpoint : ouvrez http://localhost:4000 dans votre navigateur. Vous devez voir la page d’accueil gitrust. Connectez-vous avec les identifiants ADMIN_USERNAME / ADMIN_PASSWORD définis dans .env. Si la page ne charge pas, vérifiez que le message Server listening on http://0.0.0.0:4000 est bien apparu dans les logs.
Étape 7 : Créez un dépôt et poussez votre premier commit
Dans l’interface web (http://localhost:4000), cliquez sur « New repository », donnez un nom (ex. mon-projet) et cliquez Créer.
Ajoutez votre clé SSH publique dans Settings > SSH Keys en collant le contenu de ~/.ssh/id_ed25519.pub.
Puis dans votre terminal :
git clone ssh://git@localhost:2222/admin/mon-projet.git
cd mon-projet
echo "# Mon projet" > README.md
git add README.md
git commit -m "Initial commit"
git push origin main
Sortie attendue :
Cloning into 'mon-projet'...
warning: You appear to have cloned an empty repository.
[main (root-commit) a1b2c3d] Initial commit
1 file changed, 1 insertion(+)
create mode 100644 README.md
Enumerating objects: 3, done.
Counting objects: 100% (3/3), done.
Writing objects: 100% (3/3), 226 bytes | 226.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
To ssh://localhost:2222/admin/mon-projet.git
* [new branch] main -> main
Checkpoint : retournez dans le navigateur sur http://localhost:4000/admin/mon-projet. La page doit afficher le fichier README.md avec le contenu # Mon projet et indiquer 1 commit sur la branche main.
Fonctionnalités supplémentaires disponibles
CI intégrée (optionnelle)
La CI s’exécute sur un serveur de build via SSH + rsync. Configurez dans .env :
# Même machine (défaut pour le développement)
CI_REMOTE_HOST=localhost
# Ou serveur dédié
CI_REMOTE_HOST=192.168.13.110
CI_REMOTE_USER=ci-runner
Easy Mode — ajoutez .gitrust-ci.yml à la racine de votre dépôt :
language: rust
build:
command: "cargo build --release"
tests:
command: "cargo test"
Power Mode — placez un module Dagger dans .dagger/ à la racine. gitrust exécutera dagger call -m .dagger/ ci.
Notifications
Configurez SMTP dans .env pour les notifications e-mail :
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=user
SMTP_PASSWORD=password
SMTP_FROM=noreply@gitrust.eu
Langue de l’interface
gitrust est disponible en 6 langues (fr, en, de, es, pt, it). Définissez la langue par défaut de l’instance :
DEFAULT_LOCALE=fr
Récapitulatif
- ✓ O1 accompli : gitrust est compilé depuis les sources et tourne localement —
cargo runaffiche « Server listening on http://0.0.0.0:4000 » - ✓ O2 accompli : l’environnement est configuré —
.envrempli, PostgreSQL connecté, assets CSS générés, migrations appliquées - ✓ O3 accompli : un premier push SSH a réussi — le commit est visible dans l’interface web à
http://localhost:4000/admin/mon-projet
Et si ça ne marche pas
| Symptôme | Cause probable | Correction |
|---|---|---|
error[E0463]: can't find crate for 'std' à la compilation | Toolchain Rust manquante ou mauvaise version | Exécutez rustup update stable && rustup default stable. Vérifiez avec rustc --version ≥ 1.80 |
connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed | PostgreSQL non démarré ou DATABASE_URL incorrect | Démarrez PostgreSQL (sudo systemctl start postgresql ou docker compose up -d dans database/). Vérifiez que DATABASE_URL dans .env correspond |
Permission denied (publickey) lors du git push | Clé SSH non enregistrée dans gitrust ou mauvais port | Vérifiez dans Settings > SSH Keys que la clé est présente. Testez avec ssh -T git@localhost -p 2222 — vous devez voir « Vous êtes authentifié » |
Prochaine étape
→ 02 — Première contribution : clone, build, test, PR : apprenez à créer une feature branch, écrire des tests, et soumettre votre première pull request en suivant le workflow gitrust (~90 min)
Votre première contribution à gitrust
Objectifs
À la fin de ce tutoriel, vous saurez :
- O1. Créer une branche de fonctionnalité depuis
mainet identifier un good first issue dans le backlog. - O2. Écrire un test unitaire qui échoue avant d’écrire le code de production (cycle TDD rouge-vert).
- O3. Appliquer la chaîne QA complète (
cargo fmt,cargo clippy,cargo test) et ouvrir une pull request sur la plateforme gitrust.
Pré-requis
- Technique : Rust ≥ 1.77 installé,
git≥ 2.40, accès en lecture/écriture à une instance gitrust (locale oudemo.gitrust.eu), tutoriel01-getting-startedcomplété (environnement local qui compile). - Pédagogique : tutoriel
01-getting-startedcomplété. - Temps estimé : ~90 minutes.
Vue d’ensemble
Contribuer à gitrust suit un cycle court et répétable : fork ou branche → test rouge → code minimal → tests verts → QA → PR.
sequenceDiagram
participant Vous
participant Git
participant Cargo
participant Plateforme
Vous->>Git: git switch -c feat/mon-fix
Vous->>Cargo: cargo test → ROUGE (test échoue)
Vous->>Cargo: écrire le code minimal
Vous->>Cargo: cargo test → VERT
Vous->>Cargo: cargo fmt + cargo clippy
Vous->>Git: git push origin feat/mon-fix
Vous->>Plateforme: ouvrir Pull Request
Le cycle TDD (Test-Driven Development) est essentiel ici : en écrivant d’abord le test, vous définissez précisément ce que votre code doit faire avant de l’écrire. C’est ce que gitrust appelle rouge → vert → refactor.
Étape 1 : Identifier un good first issue
Rendez-vous sur la page issues du dépôt gitrust (ou sur votre instance locale). Filtrez par le label de classification good-first-issue.
Lisez l’issue attentivement. Pour ce tutoriel, nous allons simuler le cas d’un issue fictif :
Issue #42 —
RepoSlugdevrait rejeter les slugs commençant par un tiret.
Assignez-vous l’issue (bouton « Assignee » dans la sidebar) pour signaler que vous travaillez dessus.
Checkpoint : vous devriez voir votre nom dans la liste des assignés de l’issue.
Étape 2 : Créer une branche de travail
Depuis la racine du dépôt gitrust, créez une branche nommée selon la convention fix/<numéro>-description-courte ou feat/<numéro>-description-courte :
git switch main
git pull --rebase origin main
git switch -c fix/42-repo-slug-rejects-leading-dash
Sortie attendue :
Switched to a new branch 'fix/42-repo-slug-rejects-leading-dash'
Checkpoint : git branch doit afficher votre branche avec un * devant.
Étape 3 : Écrire un test qui échoue (rouge)
Avant d’écrire le moindre code de production, écrivez le test qui décrit le comportement attendu. Ouvrez le fichier contenant la validation des slugs :
crates/gitrust-core/src/models/repository.rs
Repérez le bloc #[cfg(test)] existant en bas du fichier (ou créez-le s’il est absent). Ajoutez ce test dans la section tests :
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
// Tests existants...
#[test]
fn repo_slug_rejects_leading_dash() {
// Un slug qui commence par '-' doit être invalide.
// Ce test DOIT échouer avant notre correctif.
let result = RepoSlug::new("-invalid-slug");
assert!(
result.is_err(),
"Un slug commençant par '-' devrait être rejeté"
);
}
}
}
Exécutez maintenant les tests pour confirmer que ce test échoue :
cargo test --package gitrust-core repo_slug_rejects_leading_dash
Sortie attendue (rouge) :
test models::repository::tests::repo_slug_rejects_leading_dash ... FAILED
failures:
models::repository::tests::repo_slug_rejects_leading_dash
test result: FAILED. 0 passed; 1 failed
Checkpoint : le test échoue. C’est normal — c’est exactement l’état « rouge ».
Jargon :
RepoSlugest un newtype — un type Rust qui enveloppeStringet applique une validation à la construction viaRepoSlug::new(). Cette technique garantit qu’unRepoSlugvalide ne peut jamais exister sans passer par la validation.
Étape 4 : Écrire le code minimal (vert)
Localisez la fonction RepoSlug::new dans crates/gitrust-core/src/models/repository.rs. Elle ressemble à ceci (simplifié) :
#![allow(unused)]
fn main() {
impl RepoSlug {
pub fn new(s: &str) -> Result<Self, ValidationError> {
if s.is_empty() || s.len() > 64 {
return Err(ValidationError::InvalidSlug("longueur invalide".into()));
}
if s.contains("..") || s.contains('/') {
return Err(ValidationError::InvalidSlug("caractères interdits".into()));
}
// ... autres validations
Ok(Self(s.to_lowercase()))
}
}
}
Ajoutez uniquement la vérification manquante — ne modifiez rien d’autre :
#![allow(unused)]
fn main() {
impl RepoSlug {
pub fn new(s: &str) -> Result<Self, ValidationError> {
if s.is_empty() || s.len() > 64 {
return Err(ValidationError::InvalidSlug("longueur invalide".into()));
}
// Nouveau : rejeter les slugs commençant par un tiret
if s.starts_with('-') {
return Err(ValidationError::InvalidSlug(
"un slug ne peut pas commencer par un tiret".into(),
));
}
if s.contains("..") || s.contains('/') {
return Err(ValidationError::InvalidSlug("caractères interdits".into()));
}
Ok(Self(s.to_lowercase()))
}
}
}
Relancez le test :
cargo test --package gitrust-core repo_slug_rejects_leading_dash
Sortie attendue (vert) :
test models::repository::tests::repo_slug_rejects_leading_dash ... ok
test result: ok. 1 passed; 0 failed
Checkpoint : le test passe. Vous êtes en état « vert ».
Étape 5 : Vérifier que rien d’autre ne régresse
Un seul test qui passe ne suffit pas. Vérifiez l’ensemble de la suite du crate :
cargo test --package gitrust-core
Sortie attendue :
running XX tests
...
test result: ok. XX passed; 0 failed; 0 ignored
Checkpoint : aucun test ne régresse.
Étape 6 : Passer la chaîne QA
Gitrust impose trois gates obligatoires avant toute PR. Exécutez-les dans cet ordre :
Gate 1 — Formatage :
cargo fmt --all -- --check
Si des différences apparaissent, appliquez le format automatiquement :
cargo fmt --all
Gate 2 — Linting (zéro warning) :
cargo clippy --workspace -- -D warnings
Sortie attendue :
Checking gitrust-core v0.1.0
...
Finished `dev` profile
Gate 3 — Tests complets :
cargo test --workspace
Sortie attendue :
test result: ok. XXX passed; 0 failed; 0 ignored
Checkpoint : les trois commandes se terminent sans erreur ni warning.
Étape 7 : Committer selon la convention
Gitrust utilise des messages de commit conventionnels (feat:, fix:, test:, refactor:, docs:). Commitez votre travail :
git add crates/gitrust-core/src/models/repository.rs
git commit -m "fix: rejeter les slugs commençant par un tiret (issue #42)"
Ne committez jamais avec
git add -Asans avoir inspectégit diff --stagedau préalable. Vérifiez qu’aucun fichier.env,target/, ou fichier généré n’est inclus.
Checkpoint : git log --oneline -3 montre votre commit en tête.
Étape 8 : Pousser et ouvrir une PR
Poussez votre branche vers l’origine :
git push origin fix/42-repo-slug-rejects-leading-dash
Rendez-vous sur la plateforme gitrust. Un bandeau « Vous venez de pousser une branche — ouvrir une PR ? » devrait apparaître. Sinon, naviguez vers /{votre-username}/gitrust/pulls/new.
Remplissez le formulaire :
- Titre :
fix: rejeter les slugs commençant par un tiret - Corps : Renseignez le contexte, la cause du bug, la solution choisie, et ajoutez
Closes #42pour lier automatiquement l’issue. - Branche source :
fix/42-repo-slug-rejects-leading-dash - Branche cible :
main
Cliquez sur « Ouvrir la pull request ».
Checkpoint : la PR apparaît dans la liste /{owner}/gitrust/pulls avec le statut open.
Récapitulatif
- O1 accompli en identifiant l’issue #42 et en créant la branche
fix/42-repo-slug-rejects-leading-dash. - O2 accompli en écrivant
repo_slug_rejects_leading_dashqui échouait avant l’ajout des.starts_with('-'). - O3 accompli en passant
cargo fmt,cargo clippy -- -D warningsetcargo test --workspacesans erreur, puis en ouvrant la PR liée à l’issue.
Et si ça ne marche pas
| Symptôme | Cause probable | Correction |
|---|---|---|
cargo clippy signale needless_pass_by_value | Votre nouvelle fonction prend une String alors qu’un &str suffit | Changez le paramètre en &str |
cargo test échoue sur un test non lié | Votre modification a cassé un invariant adjacent | Relisez les tests voisins ; revenez à un diff minimal |
git push refusé avec remote: pre-receive hook declined | La branche est protégée ou votre token SSH n’est pas enregistré | Vérifiez /settings/keys sur la plateforme |
Prochaine étape
→ Tutoriel 03 — Créer un worker async (capstone)
Créer un worker asynchrone de bout en bout (capstone)
Objectifs
À la fin de ce tutoriel, vous saurez :
- O1. Concevoir le modèle de données SeaORM pour une table de jobs avec une state machine à 5 états.
- O2. Implémenter la boucle d’un worker Tokio avec sémaphore de concurrence et timeout.
- O3. Tester l’ensemble du cycle (enqueue → exécution → SSE) avec des tests d’intégration hermétiques.
Pré-requis
- Technique : tutoriels 01 et 02 complétés ; notions de Tokio (async/await,
mpsc,spawn) ; PostgreSQL local ou testcontainer disponible. - Pédagogique : tutoriel
02-premiere-contributioncomplété. - Temps estimé : ~120 minutes.
Vue d’ensemble
Ce tutoriel est un capstone : vous combinez des recettes connues (migration, service, handler) sans guide pas-à-pas détaillé. L’objectif est de reproduire le patron du worker d’import (import_service + import_worker) pour un cas générique appelé notify_worker — un worker qui envoie des notifications externes de manière asynchrone.
flowchart LR
UI[Handler POST\n/notify] -->|create_job| DB[(notify_jobs)]
UI -->|try_send| Chan[mpsc NotifyTask]
UI -->|302| Page[GET /notify/:id]
Chan --> Worker[NotifyWorker\nSémaphore N]
Worker -->|update status| DB
Worker -->|HTTP POST externe| External[Endpoint distant]
Page -. SSE .-> DB
Le patron est identique à celui utilisé pour l’import de dépôts externe (voir crates/gitrust-core/src/services/import_service.rs) et pour le worker CI. Vous n’inventez rien : vous combinez des pièces existantes.
Modèle mental : pensez à une file de caissiers de supermarché. La file d’attente (
mpsc channel) accumule les clients (tâches). Le nombre de caisses ouvertes est le sémaphore. Chaque caissier (worker goroutine) traite un client et met à jour le tableau d’affichage (DB + SSE).
Étape 1 : Concevoir la state machine
Avant d’écrire une ligne de code, formalisez la state machine de votre job.
stateDiagram-v2
[*] --> pending : create_job()
pending --> running : worker démarre
pending --> cancelled : POST /cancel
running --> success : HTTP 200 reçu
running --> failed : erreur ou timeout
running --> cancelled : flag DB checked
Les 5 états sont : pending, running, success, failed, cancelled.
Règles de transition :
- Seul
pendingpeut être annulé avant démarrage. runningpeut être annulé via un flag en DB vérifié par le worker.successetfailedsont terminaux.
Checkpoint : dessinez la state machine sur papier avant de continuer. Répondez à : « que se passe-t-il si le serveur redémarre pendant un job running ? » (réponse : au redémarrage, les jobs running doivent être marqués failed avec le message « server restarted »).
Étape 2 : Créer la migration SeaORM
Créez le fichier de migration :
crates/gitrust-core/src/migrations/m20260501_000023_create_notify_jobs.rs
#![allow(unused)]
fn main() {
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(NotifyJobs::Table)
.if_not_exists()
.col(ColumnDef::new(NotifyJobs::Id).uuid().not_null().primary_key())
.col(ColumnDef::new(NotifyJobs::OwnerId).uuid().not_null())
.col(ColumnDef::new(NotifyJobs::TargetUrl).text().not_null())
.col(ColumnDef::new(NotifyJobs::Payload).text().not_null())
.col(
ColumnDef::new(NotifyJobs::Status)
.string()
.not_null()
.default("pending"),
)
.col(ColumnDef::new(NotifyJobs::ErrorMessage).text().null())
.col(ColumnDef::new(NotifyJobs::StartedAt).timestamp_with_time_zone().null())
.col(ColumnDef::new(NotifyJobs::FinishedAt).timestamp_with_time_zone().null())
.col(
ColumnDef::new(NotifyJobs::CreatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.col(
ColumnDef::new(NotifyJobs::UpdatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.table(NotifyJobs::Table)
.name("notify_jobs_owner_status_idx")
.col(NotifyJobs::OwnerId)
.col(NotifyJobs::Status)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(NotifyJobs::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum NotifyJobs {
Table,
Id,
OwnerId,
TargetUrl,
Payload,
Status,
ErrorMessage,
StartedAt,
FinishedAt,
CreatedAt,
UpdatedAt,
}
}
Enregistrez la migration dans crates/gitrust-core/src/migrations/mod.rs en ajoutant m20260501_000023_create_notify_jobs::Migration à la liste du Migrator.
Test rouge à faire passer :
#![allow(unused)]
fn main() {
#[tokio::test]
async fn migration_creates_notify_jobs_table() {
let db = setup_test_db().await;
// Si la migration s'est appliquée, la requête ne renvoie pas d'erreur.
let count: i64 = db
.query_one(Statement::from_string(
DatabaseBackend::Postgres,
"SELECT COUNT(*) FROM notify_jobs".to_owned(),
))
.await
.expect("La table notify_jobs doit exister")
.unwrap()
.try_get("", "count")
.unwrap();
assert_eq!(count, 0);
}
}
Checkpoint : cargo test --package gitrust-core migration_creates_notify_jobs_table passe au vert.
Étape 3 : Définir les DTOs et l’enum de statut
Créez crates/gitrust-core/src/dto/notify_dto.rs :
#![allow(unused)]
fn main() {
use serde::{Deserialize, Serialize};
/// Statut d'un job de notification — state machine à 5 états.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum NotifyStatus {
Pending,
Running,
Success,
Failed,
Cancelled,
}
impl NotifyStatus {
pub fn as_str(&self) -> &'static str {
match self {
Self::Pending => "pending",
Self::Running => "running",
Self::Success => "success",
Self::Failed => "failed",
Self::Cancelled => "cancelled",
}
}
pub fn from_str(s: &str) -> Option<Self> {
match s {
"pending" => Some(Self::Pending),
"running" => Some(Self::Running),
"success" => Some(Self::Success),
"failed" => Some(Self::Failed),
"cancelled" => Some(Self::Cancelled),
_ => None,
}
}
}
/// Payload envoyé via SSE au client.
#[derive(Debug, Serialize)]
pub struct NotifyJobSse {
pub status: String,
pub error_message: Option<String>,
}
/// Input pour créer un job.
#[derive(Debug, Deserialize)]
pub struct CreateNotifyJobInput {
pub target_url: String,
pub payload: String,
}
}
Tests round-trip obligatoires :
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn status_round_trip() {
for (s, expected) in [
("pending", NotifyStatus::Pending),
("running", NotifyStatus::Running),
("success", NotifyStatus::Success),
("failed", NotifyStatus::Failed),
("cancelled", NotifyStatus::Cancelled),
] {
let status = NotifyStatus::from_str(s).expect("doit parser");
assert_eq!(status, expected);
assert_eq!(status.as_str(), s);
}
}
#[test]
fn unknown_status_returns_none() {
assert!(NotifyStatus::from_str("unknown").is_none());
}
}
}
Checkpoint : cargo test --package gitrust-core status_round_trip passe.
Étape 4 : Implémenter le service
Créez crates/gitrust-core/src/services/notify_service.rs. Le service ne fait que orchestrer la DB — aucune logique métier IO ici :
#![allow(unused)]
fn main() {
use chrono::Utc;
use sea_orm::{ActiveModelTrait, ActiveValue::Set, DatabaseConnection, EntityTrait};
use uuid::Uuid;
use crate::{
dto::notify_dto::{CreateNotifyJobInput, NotifyStatus},
error::GitrustError,
models::notify_job,
};
pub struct NotifyService;
impl NotifyService {
/// Crée un job en statut `pending` et retourne son ID.
pub async fn create_job(
db: &DatabaseConnection,
owner_id: Uuid,
input: CreateNotifyJobInput,
) -> Result<Uuid, GitrustError> {
let id = Uuid::new_v4();
let now = Utc::now();
let model = notify_job::ActiveModel {
id: Set(id),
owner_id: Set(owner_id),
target_url: Set(input.target_url),
payload: Set(input.payload),
status: Set(NotifyStatus::Pending.as_str().to_owned()),
error_message: Set(None),
started_at: Set(None),
finished_at: Set(None),
created_at: Set(now),
updated_at: Set(now),
};
model.insert(db).await.map_err(GitrustError::Database)?;
Ok(id)
}
/// Passe le job en `running`.
pub async fn mark_running(db: &DatabaseConnection, id: Uuid) -> Result<(), GitrustError> {
Self::update_status(db, id, NotifyStatus::Running, None).await
}
/// Passe le job en `success`.
pub async fn mark_success(db: &DatabaseConnection, id: Uuid) -> Result<(), GitrustError> {
Self::update_status(db, id, NotifyStatus::Success, None).await
}
/// Passe le job en `failed` avec un message d'erreur.
pub async fn mark_failed(
db: &DatabaseConnection,
id: Uuid,
error: &str,
) -> Result<(), GitrustError> {
Self::update_status(db, id, NotifyStatus::Failed, Some(error)).await
}
/// Passe le job en `cancelled`.
pub async fn mark_cancelled(db: &DatabaseConnection, id: Uuid) -> Result<(), GitrustError> {
Self::update_status(db, id, NotifyStatus::Cancelled, None).await
}
/// Vérifie si le job a été annulé (pour le poll du worker).
pub async fn is_cancelled(
db: &DatabaseConnection,
id: Uuid,
) -> Result<bool, GitrustError> {
let job = notify_job::Entity::find_by_id(id)
.one(db)
.await
.map_err(GitrustError::Database)?
.ok_or_else(|| GitrustError::NotFound("job introuvable".into()))?;
Ok(job.status == NotifyStatus::Cancelled.as_str())
}
async fn update_status(
db: &DatabaseConnection,
id: Uuid,
status: NotifyStatus,
error: Option<&str>,
) -> Result<(), GitrustError> {
use sea_orm::ActiveModelTrait;
let now = Utc::now();
let mut model: notify_job::ActiveModel = notify_job::Entity::find_by_id(id)
.one(db)
.await
.map_err(GitrustError::Database)?
.ok_or_else(|| GitrustError::NotFound("job introuvable".into()))?
.into();
model.status = Set(status.as_str().to_owned());
model.updated_at = Set(now);
model.error_message = Set(error.map(str::to_owned));
if status == NotifyStatus::Running {
model.started_at = Set(Some(now));
}
if matches!(status, NotifyStatus::Success | NotifyStatus::Failed | NotifyStatus::Cancelled) {
model.finished_at = Set(Some(now));
}
model.update(db).await.map_err(GitrustError::Database)?;
Ok(())
}
}
}
Étape 5 : Implémenter le worker
Le worker tourne dans un tokio::spawn séparé. Il lit les tâches depuis un canal mpsc et les exécute avec un sémaphore de concurrence :
#![allow(unused)]
fn main() {
// crates/gitrust-core/src/services/notify_worker.rs
use std::sync::Arc;
use sea_orm::DatabaseConnection;
use tokio::sync::{mpsc, Semaphore};
use uuid::Uuid;
use super::notify_service::NotifyService;
/// Tâche envoyée via le canal mpsc au worker.
pub struct NotifyTask {
pub job_id: Uuid,
pub target_url: String,
pub payload: String,
}
pub struct NotifyWorkerConfig {
/// Nombre maximal de jobs concurrents.
pub max_concurrent: usize,
/// Timeout en secondes par job.
pub timeout_secs: u64,
}
/// Boucle principale du worker. À appeler avec `tokio::spawn`.
pub async fn run(
db: DatabaseConnection,
mut rx: mpsc::Receiver<NotifyTask>,
config: NotifyWorkerConfig,
) {
let semaphore = Arc::new(Semaphore::new(config.max_concurrent));
while let Some(task) = rx.recv().await {
let permit = semaphore.clone().acquire_owned().await.expect("semaphore fermé");
let db = db.clone();
let timeout = config.timeout_secs;
tokio::spawn(async move {
let _permit = permit; // libéré à la fin du bloc
execute_task(&db, task, timeout).await;
});
}
}
async fn execute_task(db: &DatabaseConnection, task: NotifyTask, timeout_secs: u64) {
// Vérifier si annulé avant de démarrer
match NotifyService::is_cancelled(db, task.job_id).await {
Ok(true) => return,
Err(e) => {
tracing::error!(job_id = %task.job_id, error = %e, "Erreur lecture job");
return;
}
Ok(false) => {}
}
if let Err(e) = NotifyService::mark_running(db, task.job_id).await {
tracing::error!(job_id = %task.job_id, error = %e, "Impossible de marquer running");
return;
}
let result = tokio::time::timeout(
std::time::Duration::from_secs(timeout_secs),
send_notification(&task.target_url, &task.payload),
)
.await;
match result {
Ok(Ok(())) => {
if let Err(e) = NotifyService::mark_success(db, task.job_id).await {
tracing::error!(job_id = %task.job_id, error = %e, "Impossible de marquer success");
}
}
Ok(Err(e)) => {
let msg = format!("Erreur HTTP: {e}");
if let Err(db_err) = NotifyService::mark_failed(db, task.job_id, &msg).await {
tracing::error!(job_id = %task.job_id, error = %db_err, "Impossible de marquer failed");
}
}
Err(_elapsed) => {
let msg = format!("Timeout après {timeout_secs}s");
if let Err(e) = NotifyService::mark_failed(db, task.job_id, &msg).await {
tracing::error!(job_id = %task.job_id, error = %e, "Impossible de marquer failed (timeout)");
}
}
}
}
async fn send_notification(url: &str, payload: &str) -> Result<(), reqwest::Error> {
let client = reqwest::Client::new();
client
.post(url)
.header("Content-Type", "application/json")
.body(payload.to_owned())
.send()
.await?
.error_for_status()?;
Ok(())
}
}
Checkpoint : cargo build --package gitrust-core compile sans erreur.
Étape 6 : Ajouter le handler et la route SSE
Dans crates/gitrust-web/src/handlers/notify.rs, créez le handler POST qui enqueues le job et redirige, ainsi que le handler SSE :
#![allow(unused)]
fn main() {
use axum::{
extract::{Path, State},
response::{sse::{Event, KeepAlive, Sse}, Redirect},
Extension, Form,
};
use sea_orm::DatabaseConnection;
use tokio::time::{interval, Duration};
use tokio_stream::wrappers::IntervalStream;
use tokio_stream::StreamExt;
use uuid::Uuid;
use gitrust_core::{
dto::notify_dto::CreateNotifyJobInput,
services::{notify_service::NotifyService, notify_worker::NotifyTask},
};
use crate::auth::AuthUser;
pub type NotifySender = tokio::sync::mpsc::Sender<NotifyTask>;
/// POST /notify — crée un job et redirige vers la page de suivi.
pub async fn create_notify_job(
State(db): State<DatabaseConnection>,
Extension(tx): Extension<NotifySender>,
user: AuthUser,
Form(input): Form<CreateNotifyJobInput>,
) -> Result<Redirect, crate::error::AppError> {
let job_id = NotifyService::create_job(&db, user.user_id, input.clone()).await?;
// Essai d'envoi dans le canal — si plein, le job reste `pending` jusqu'au
// prochain redémarrage (les jobs `pending` peuvent être re-envoyés au démarrage).
let _ = tx.try_send(NotifyTask {
job_id,
target_url: input.target_url,
payload: input.payload,
});
Ok(Redirect::to(&format!("/notify/{job_id}")))
}
/// GET /notify/:id/stream — SSE : pousse les mises à jour de statut au client.
pub async fn notify_job_stream(
State(db): State<DatabaseConnection>,
Path(id): Path<Uuid>,
) -> Sse<impl tokio_stream::Stream<Item = Result<Event, std::convert::Infallible>>> {
let stream = IntervalStream::new(interval(Duration::from_secs(1)))
.map(move |_| {
let db = db.clone();
async move {
// Poll la DB pour l'état courant
use gitrust_core::models::notify_job;
use sea_orm::EntityTrait;
let job = notify_job::Entity::find_by_id(id)
.one(&db)
.await
.ok()
.flatten();
let data = match job {
Some(j) => format!(
r#"{{"status":"{}","error":{}}}"#,
j.status,
j.error_message
.as_deref()
.map(|e| format!(r#""{}""#, e.replace('"', "\\\"")))
.unwrap_or("null".to_owned())
),
None => r#"{"status":"not_found"}"#.to_owned(),
};
Ok(Event::default().data(data))
}
})
.then(|fut| fut);
Sse::new(stream).keep_alive(KeepAlive::default())
}
}
Enregistrez les routes dans routes.rs :
#![allow(unused)]
fn main() {
.route("/notify", post(handlers::notify::create_notify_job))
.route("/notify/:id", get(handlers::notify::notify_job_page))
.route("/notify/:id/stream", get(handlers::notify::notify_job_stream))
.route("/notify/:id/cancel", post(handlers::notify::cancel_notify_job))
}
Étape 7 : Intégrer le worker dans main.rs
Dans crates/gitrust-web/src/main.rs, initialisez le canal et démarrez le worker :
#![allow(unused)]
fn main() {
// Dans la fonction main(), après la construction de l'app :
let (notify_tx, notify_rx) = tokio::sync::mpsc::channel::<NotifyTask>(100);
let notify_db = db.clone();
tokio::spawn(gitrust_core::services::notify_worker::run(
notify_db,
notify_rx,
NotifyWorkerConfig {
max_concurrent: std::env::var("NOTIFY_MAX_CONCURRENT")
.unwrap_or("4".into())
.parse()
.unwrap_or(4),
timeout_secs: std::env::var("NOTIFY_TIMEOUT_SECS")
.unwrap_or("30".into())
.parse()
.unwrap_or(30),
},
));
// Injecter l'émetteur dans le router via Extension
let app = routes::routes(&static_path)
.layer(Extension(notify_tx))
.with_state(db);
}
Étape 8 : Tests d’intégration
Écrivez au minimum ces trois tests dans crates/gitrust-core/src/services/notify_service_test.rs :
#![allow(unused)]
fn main() {
#[tokio::test]
async fn create_job_inserts_with_pending_status() {
let db = setup_test_db().await;
let id = NotifyService::create_job(
&db,
Uuid::new_v4(),
CreateNotifyJobInput {
target_url: "https://example.com/hook".into(),
payload: r#"{"event":"test"}"#.into(),
},
)
.await
.expect("create_job doit réussir");
let job = notify_job::Entity::find_by_id(id)
.one(&db)
.await
.unwrap()
.unwrap();
assert_eq!(job.status, "pending");
assert!(job.started_at.is_none());
}
#[tokio::test]
async fn mark_running_sets_started_at() {
let db = setup_test_db().await;
let id = create_test_job(&db).await;
NotifyService::mark_running(&db, id).await.unwrap();
let job = notify_job::Entity::find_by_id(id).one(&db).await.unwrap().unwrap();
assert_eq!(job.status, "running");
assert!(job.started_at.is_some());
}
#[tokio::test]
async fn cancel_before_running_is_terminal() {
let db = setup_test_db().await;
let id = create_test_job(&db).await;
NotifyService::mark_cancelled(&db, id).await.unwrap();
let is_cancelled = NotifyService::is_cancelled(&db, id).await.unwrap();
assert!(is_cancelled);
}
}
Checkpoint : cargo test --package gitrust-core notify — tous les tests passent.
Récapitulatif
- O1 accompli en concevant la table
notify_jobsavec 5 états (pending/running/success/failed/cancelled) et la migration SeaORM correspondante. - O2 accompli en implémentant
notify_worker::runavecSemaphore,tokio::time::timeout, et lecture du flag d’annulation via la DB. - O3 accompli en écrivant les tests
create_job_inserts_with_pending_status,mark_running_sets_started_atetcancel_before_running_is_terminal, tous hermétiques (pas de réseau réel).
Et si ça ne marche pas
| Symptôme | Cause probable | Correction |
|---|---|---|
channel closed au démarrage | Le Receiver est droppé avant le spawn du worker | Vérifiez que tokio::spawn(run(..., notify_rx, ...)) est appelé avant que notify_rx ne soit droppé |
Le job reste pending indéfiniment | Le canal est plein ou le worker n’est pas démarré | Ajoutez un log dans execute_task ; vérifiez que tokio::spawn est bien appelé dans main |
| SSE ne reçoit aucun événement | Le Content-Type: text/event-stream n’est pas reconnu | Vérifiez que le handler retourne bien Sse<...> et non String |
Prochaine étape
Vous êtes désormais core contributor. Consultez les how-to pour les recettes quotidiennes :
Guides pratiques
Les guides pratiques répondent à une question concrète : « comment faire X ? ». Ils supposent que vous savez déjà ce que vous voulez accomplir. Pas d’introduction, pas de contexte superflu — directement à l’action.
Diátaxis — mode tâche : vous avez un objectif précis. Le guide vous amène au résultat le plus directement possible.
Contribuer au projet
| Guide | Quand l’utiliser |
|---|---|
| Workflow contribution et PR | Ouvrir une Pull Request, conventions de commit, rebase |
| Passer la QA avant merge | Checklist des 7 gates QA, sécurité, clippy, tests |
Ajouter des fonctionnalités
| Guide | Quand l’utiliser |
|---|---|
| Ajouter une route web | Nouvelle page SSR avec template Askama |
| Ajouter un service métier | Nouveau service avec CRUD, anti-IDOR, tests |
| Ajouter une migration DB | Nouvelle table, colonne, index, contrainte |
| Implémenter des webhooks | Événements sortants avec signature HMAC, retry |
| Implémenter un endpoint API REST | Endpoint JSON v1, DTO, pagination, rate limiting |
Navigation croisée
- Vous ne savez pas comment la base de données est structurée ? → Schéma DB
- Vous voulez comprendre le modèle de permissions avant d’écrire un service ? → RBAC
- Vous démarrez de zéro ? → Tutoriels
Contribuer via le workflow Git et pull request
Ce guide décrit le workflow Git utilisé par le projet gitrust pour toute contribution : nouvelle fonctionnalité, correctif, ou documentation.
Pré-requis
- Accès en lecture/écriture au dépôt (ou à votre fork).
- Environnement de développement opérationnel (voir
tutorials/01-getting-started.md).
Créer une branche depuis main
Toujours partir d’un main à jour :
git switch main
git pull --rebase origin main
git switch -c <type>/<numéro>-<description-courte>
Convention de nommage des branches :
| Préfixe | Usage |
|---|---|
feat/ | Nouvelle fonctionnalité |
fix/ | Correctif de bug |
refactor/ | Refactorisation sans changement de comportement |
docs/ | Documentation uniquement |
test/ | Tests uniquement |
Exemples : feat/42-webhook-dispatch, fix/88-slug-leading-dash, docs/15-api-rest.
Conventions de commit
Gitrust suit les Conventional Commits. Le format est :
<type>(<scope optionnel>): <description en impératif>
[corps optionnel]
[footer : Closes #N, BREAKING CHANGE, etc.]
Types acceptés : feat, fix, refactor, test, docs, chore, perf.
Exemples valides :
feat(webhooks): ajouter la signature HMAC-SHA256 sur les livraisons
fix(slug): rejeter les slugs commençant par un tiret (Closes #42)
test(import): ajouter test hermétique pour le timeout worker
Règle des petits commits atomiques : un commit = un changement logique cohérent. Évitez les commits « WIP » ou « fix fix fix » — utilisez git commit --amend ou git rebase -i pour réécrire l’historique avant de pousser.
Rebase avant push
Avant de pousser votre branche, rebasez-la sur main pour obtenir un historique linéaire :
git fetch origin
git rebase origin/main
En cas de conflit, résolvez-les fichier par fichier, puis :
git add <fichier-résolu>
git rebase --continue
Ne fusionnez jamais main dans votre branche de feature (git merge main) — cela crée des commits de merge parasites.
Checklist pre-merge (QA obligatoire)
Avant d’ouvrir la PR, vérifiez chaque point :
-
cargo fmt --all -- --check— zéro diff de formatage -
cargo clippy --workspace -- -D warnings— zéro warning -
cargo test --workspace— 100 % de tests passants -
npm run test:e2e— tests Playwright passants (si templates ou routes modifiés) - CSS rebuild :
npx tailwindcss -i static/css/input.css -o static/css/style.css --minify(si templates modifiés) - Aucun secret, token ou clé en clair dans le code ou les logs
- Aucune modification dans
crates/rustwarden-core/(framework read-only)
Voir la checklist complète dans passer-la-qa-avant-merge.md.
Ouvrir la pull request
Poussez votre branche :
git push origin <votre-branche>
Sur la plateforme gitrust, naviguez vers /{owner}/gitrust/pulls/new et remplissez :
- Titre : message de commit principal (convention Conventional Commits).
- Corps : contexte du problème, solution choisie, alternatives considérées, lien vers l’issue (
Closes #N). - Reviewers : assignez au moins un reviewer.
- Labels : appliquez les labels de classification appropriés.
Critères d’approbation
Un reviewer approuve quand :
- Tous les gates QA passent (CI verte).
- La logique métier est correcte et les cas limites sont couverts par des tests.
- Aucun
.unwrap(),.expect(), oupanic!()non justifié. - Le diff ne contient pas de changements de périmètre non demandés.
Stratégie de merge par défaut
Gitrust utilise le merge commit pour conserver la traçabilité des branches dans l’historique. Le squash est réservé aux PRs de correction mineure (1-2 commits). Le rebase-merge est proscrit car il réécrit les SHA des commits existants.
Après le merge
Supprimez votre branche locale et distante :
git switch main
git pull --rebase origin main
git branch -d <votre-branche>
git push origin --delete <votre-branche>
Vérifiez que l’issue liée est bien fermée automatiquement par le Closes #N dans le message de commit ou le corps de la PR.
Lancer les tests unitaires
Référence des commandes et de la stratégie de tests pour gitrust.
Infrastructure existante
Tests unitaires Rust
cargo test --workspace # Tous les tests
cargo test -p gitrust-core # Un crate spécifique
cargo test -p gitrust-git # Tests Git (tempdir)
Les tests unitaires sont intégrés dans chaque crate via #[cfg(test)].
Tests e2e Playwright
17 specs couvrant : auth, admin, repos, issues, labels, teams, tokens, settings, navigation.
# Pré-requis (première fois)
npm install
./scripts/e2e-setup-db.sh # Crée la DB gitrust_test
# Lancer les tests
npm run test:e2e # Mode headless
npm run test:e2e:ui # Mode interactif (navigateur visible)
npm run test:e2e:debug # Mode debug
npm run test:e2e:report # Voir le rapport HTML
Configuration : playwright.config.ts (port 4001, DB gitrust_test, locale fr-FR).
Structure des répertoires
tests/
e2e/ # Specs Playwright (TypeScript)
global-setup.ts # Setup : lancer l'app, créer admin
global-teardown.ts # Teardown : arrêter l'app
fixtures.ts # Fixtures Playwright partagées
auth.spec.ts # Tests authentification
admin.spec.ts # Tests administration
issues.spec.ts # Tests issues
...
integration/ # Tests d'intégration Rust (DB réelle) [à créer]
fixtures/ # Fichiers de test (repos, configs) [à créer]
seeds/ # Seeds Rust pour initialiser la DB de test
mod.rs
seed.rs
Tests à ajouter
1. Tests fonctionnels API REST (intégration Rust)
Tests avec une DB PostgreSQL réelle pour vérifier les endpoints API.
Emplacement : tests/integration/ ou crates/gitrust-web/tests/
Approche recommandée : utiliser reqwest + un serveur de test :
#![allow(unused)]
fn main() {
// tests/integration/api_test.rs
use reqwest::Client;
#[tokio::test]
#[ignore] // Nécessite une DB + serveur running
async fn test_api_login() {
let client = Client::new();
let res = client.post("http://localhost:4001/api/v1/auth/login")
.json(&serde_json::json!({
"login": "admin",
"password": "test_password"
}))
.send()
.await
.unwrap();
assert_eq!(res.status(), 200);
}
}
Alternative : axum::test pour tester les handlers sans serveur HTTP :
#![allow(unused)]
fn main() {
use axum::body::Body;
use axum::http::Request;
use tower::ServiceExt;
#[tokio::test]
async fn test_health_check() {
let app = create_test_app().await; // Build router with test DB
let response = app
.oneshot(Request::builder().uri("/api/hello").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), 200);
}
}
2. Tests fonctionnels CI
| Test | Description | Priorité |
|---|---|---|
| CI detection | Repo avec .gitrust-ci.yml -> Easy, .dagger/ -> Power, rien -> None | Haute |
| Pipeline CRUD | Créer, lister, mettre à jour, annuler un pipeline | Haute |
| Config CI | Activer/désactiver CI, modifier triggers, vérifier effet | Haute |
| Variables héritage | Team var + repo var -> merge correct | Moyenne |
| Auto-cancel | Nouveau push annule les pipelines en cours | Moyenne |
| Logs streaming | Append logs + lecture paginée | Moyenne |
| Notifications CI | Pipeline échoue -> notification créée | Moyenne |
Pour les tests CI : mocker dagger avec un script shell qui simule success/failure :
#!/bin/bash
# tests/fixtures/mock-dagger.sh
echo "Step 1: Building..."
echo "Step 2: Testing..."
if [ "$MOCK_FAIL" = "true" ]; then
echo "FAILED" >&2
exit 1
fi
echo "All checks passed"
exit 0
Configurer CI_DAGGER_BIN=tests/fixtures/mock-dagger.sh dans les tests.
3. Tests e2e Playwright à ajouter
Nouvelles specs pour les fonctionnalités CI et notifications :
| Spec | Couverture |
|---|---|
ci-pipelines.spec.ts | Liste pipelines, détail, status badges |
ci-config.spec.ts | Config CI (enable/disable, triggers, timeout) |
ci-variables.spec.ts | CRUD variables CI, masquage secrets |
notifications.spec.ts | Liste notifications, marquer lu, préférences |
api-docs.spec.ts | Swagger UI accessible, spec chargée |
i18n.spec.ts | Changement de langue, textes traduits |
docker-smoke.spec.ts | docker compose up + smoke test |
Exemple de spec CI :
// tests/e2e/ci-pipelines.spec.ts
import { test, expect } from './fixtures';
test.describe('CI Pipelines', () => {
test('affiche la page CI vide', async ({ authenticatedPage }) => {
await authenticatedPage.goto('/admin/mon-repo/ci');
await expect(authenticatedPage.locator('text=Aucun pipeline')).toBeVisible();
});
test('bouton trigger CI visible pour le maintainer', async ({ authenticatedPage }) => {
await authenticatedPage.goto('/admin/mon-repo/ci');
await expect(authenticatedPage.locator('text=Lancer CI')).toBeVisible();
});
});
4. Tests settings ↔ comportement
Les tests les plus critiques vérifient que changer un setting modifie le comportement observable :
| Setting | Action | Vérification |
|---|---|---|
| PAT révoqué | Appel API | 401 immédiatement |
ci_enabled = false | Push | Pas de pipeline créé |
trigger_on_push = false | Push | Pas de pipeline |
auto_cancel = true | 2 pushes rapides | 1er pipeline annulé |
email_on_pipeline_failure = false | Pipeline échoue | Pas d’email, notif in-app ok |
DEFAULT_LOCALE = en | Charger page | Texte en anglais |
Commandes de test
# Tests unitaires Rust
cargo test --workspace
# Tests avec filtrage
cargo test -p gitrust-core -- ci_service
cargo test -p gitrust-core -- notification
# Tests e2e Playwright
npm run test:e2e
# Tests e2e spécifiques
npx playwright test tests/e2e/ci-pipelines.spec.ts
npx playwright test --grep "notifications"
# Lint
cargo clippy --workspace -- -D warnings
# Format check
cargo fmt --all -- --check
CI locale (exécuter avant un push)
# Script rapide de vérification pré-push
cargo fmt --all -- --check && \
cargo clippy --workspace -- -D warnings && \
cargo test --workspace && \
echo "All checks passed"
Lancer les tests end-to-end (E2E)
Les tests E2E de gitrust utilisent Playwright pour piloter un navigateur Chromium contre une instance de l’application configurée avec une base de données et un répertoire de dépôts dédiés.
Pré-requis
- Node.js (déjà présent pour Tailwind/DaisyUI)
- PostgreSQL accessible en local (Docker via
database/docker-compose.yml) psql,createdb,dropdbdisponibles dans le PATH- Chromium installé via Playwright (
npx playwright install chromium)
Commandes
# Créer (ou remettre à zéro) la base de données de test
npm run test:e2e:setup
# Lancer tous les tests E2E
npm run test:e2e
# Mode debug interactif (inspecteur Playwright, pas-à-pas)
npm run test:e2e:debug
# Mode UI (interface graphique Playwright)
npm run test:e2e:ui
# Consulter le rapport HTML après exécution
npm run test:e2e:report
Architecture
.env.test # Variables d'environnement de test
playwright.config.ts # Configuration Playwright
scripts/
e2e-setup-db.sh # Crée la DB gitrust_test + nettoie depot_test
e2e-server.sh # Démarre l'app avec .env.test
tests/e2e/
fixtures.ts # Fixtures partagées (loginAs, credentials)
global-setup.ts # Seed : crée les utilisateurs via /register
global-teardown.ts # Nettoyage post-tests
auth.spec.ts # Tests d'authentification
repository.spec.ts # Tests de gestion des dépôts
admin.spec.ts # Tests du panneau d'administration
navigation.spec.ts # Tests de navigation et UI
Isolation de l’environnement de test
| Ressource | Développement | Tests E2E |
|---|---|---|
| Base de données | gitrust | gitrust_test |
| Port HTTP | 4000 | 4001 |
| Port SSH | 2222 | 2223 |
| Dépôts Git | ./data/repos/ | ./depot_test/ |
| Rate limiting | Standard | Relâché (×20) |
| Validation email | Activée | Désactivée |
Fonctionnement
npm run test:e2e:setupsupprime puis recrée la basegitrust_testet videdepot_test/.- Playwright démarre automatiquement l’application via
scripts/e2e-server.sh, qui charge les variables de.env.testavant d’exécutercargo run. - Le global-setup crée les utilisateurs de test (alice, bob, charlie) en passant par le formulaire
/register— approche 100% E2E, aucune insertion directe en base. - Les tests s’exécutent séquentiellement (un seul worker) pour éviter les conflits sur la DB partagée.
- Le global-teardown est exécuté à la fin de la session.
Fixtures
Le fichier fixtures.ts fournit une fixture loginAs réutilisable dans tous les tests :
import { test, expect } from './fixtures';
test('mon test', async ({ page, loginAs }) => {
await loginAs('alice');
// alice est connectée, on peut naviguer
});
Utilisateurs disponibles : admin, alice, bob, charlie.
Liste des tests
auth.spec.ts — Authentification
| Test | Description |
|---|---|
| login avec des identifiants valides | Vérifie le login standard et la redirection vers /dashboard |
| login avec un mauvais mot de passe | Vérifie l’affichage d’un message d’erreur |
| login avec un utilisateur inexistant | Vérifie le rejet d’un username inconnu |
| logout déconnecte l’utilisateur | Vérifie la déconnexion et l’impossibilité d’accéder au dashboard |
| accès au dashboard sans login redirige vers login | Vérifie la protection des routes authentifiées |
| option remember me est présente | Vérifie la présence de la checkbox remember_me |
| formulaire d’inscription est accessible | Vérifie la présence de tous les champs du formulaire |
| inscription avec username déjà pris échoue | Vérifie l’unicité des usernames |
| inscription avec mots de passe différents échoue | Vérifie la validation de confirmation du mot de passe |
repository.spec.ts — Gestion des dépôts
| Test | Description |
|---|---|
| créer un dépôt avec nom et description | Crée un dépôt complet et vérifie la redirection |
| créer un dépôt sans description | Vérifie que la description est optionnelle |
| créer un dépôt avec un nom déjà pris échoue | Vérifie l’unicité des slugs par utilisateur |
| le formulaire de création nécessite un login | Vérifie la protection de la route /new |
| le dashboard liste les dépôts de l’utilisateur | Vérifie l’affichage des dépôts sur le dashboard |
| un dépôt public est accessible sans login | Vérifie l’accès anonyme aux dépôts publics |
admin.spec.ts — Administration
| Test | Description |
|---|---|
| l’admin peut accéder au panneau d’administration | Vérifie l’accès admin à /admin |
| un utilisateur normal ne peut pas accéder à l’admin | Vérifie le contrôle d’accès |
| lister les utilisateurs | Vérifie l’affichage de la liste des utilisateurs |
| créer un nouvel utilisateur via l’admin | Teste le formulaire de création d’utilisateur admin |
| créer un utilisateur avec un username déjà pris échoue | Vérifie la gestion des doublons côté admin |
| les liens du menu admin sont présents | Vérifie la navigation du panneau admin |
navigation.spec.ts — Navigation et UI
| Test | Description |
|---|---|
| la page de login est accessible | Vérifie le rendu de la page de login |
| la navbar est présente après login | Vérifie la présence de la barre de navigation |
| le sidebar est présent après login | Vérifie la présence du menu latéral |
| le lien vers le dashboard fonctionne | Vérifie la navigation vers le dashboard |
| le lien vers la création de dépôt fonctionne | Vérifie le lien /new |
| la page utilise le thème DaisyUI | Vérifie la présence de l’attribut data-theme |
Ajouter un nouveau test
- Créer un fichier
tests/e2e/mon-module.spec.ts - Importer les fixtures :
import { test, expect } from './fixtures'; - Utiliser
loginAspour les tests nécessitant une authentification - Lancer avec
npm run test:e2eou cibler le fichier :npx playwright test mon-module
Ajouter une route web (handler axum + template Askama)
Ce guide explique comment ajouter un endpoint HTTP SSR complet dans gitrust : route, handler, template Askama, et test E2E Playwright.
Pré-requis
- Familiarité avec le tutoriel
01-getting-started.md. - Notions de base d’Axum (extracteurs,
State,IntoResponse).
Vue d’ensemble du flux
routes.rs → handler (handlers/*.rs) → service (gitrust-core) → template Askama
Gitrust suit le patron vertical slice : chaque route traverse toutes les couches. Ne créez jamais un handler sans template, ni un service sans route.
Étape 1 : Déclarer la route dans routes.rs
Ouvrez crates/gitrust-web/src/routes.rs. Ajoutez votre route dans la section qui correspond à son contexte (routes authentifiées, routes repo, routes admin…) :
#![allow(unused)]
fn main() {
// Exemple : GET /settings/notifications
.route(
"/settings/notifications",
get(handlers::notifications::preferences_form)
.post(handlers::notifications::preferences_submit),
)
}
La signature de routes.rs utilise Router<DatabaseConnection> — l’état partagé est toujours la connexion DB injectée via State.
Étape 2 : Créer le handler
Créez ou complétez crates/gitrust-web/src/handlers/mon_handler.rs :
#![allow(unused)]
fn main() {
use axum::{extract::State, response::IntoResponse};
use sea_orm::DatabaseConnection;
use crate::{
auth::AuthUser,
error::AppError,
templates::MonPageTemplate,
};
use gitrust_core::services::mon_service::MonService;
/// GET /settings/mon-endpoint
pub async fn mon_form(
State(db): State<DatabaseConnection>,
user: AuthUser,
) -> Result<impl IntoResponse, AppError> {
let data = MonService::get_data(&db, user.user_id).await?;
Ok(MonPageTemplate {
username: user.username.clone(),
is_admin: user.has_role("admin"),
current_path: "/settings/mon-endpoint".to_owned(),
data,
})
}
/// POST /settings/mon-endpoint
pub async fn mon_submit(
State(db): State<DatabaseConnection>,
user: AuthUser,
axum::Form(input): axum::Form<MonInput>,
) -> Result<impl IntoResponse, AppError> {
MonService::update_data(&db, user.user_id, input).await?;
Ok(axum::response::Redirect::to("/settings/mon-endpoint"))
}
}
Règles obligatoires :
- Pas de
.unwrap()ni.expect()— toutes les erreurs remontent via?versAppError. AppErrorimplémenteIntoResponseet mappe les variantes vers les codes HTTP appropriés.AuthUser(extracteur rustwarden-core) rejette automatiquement les requêtes non authentifiées avec401.
Étape 3 : Déclarer le module handler
Dans crates/gitrust-web/src/handlers/mod.rs, ajoutez :
#![allow(unused)]
fn main() {
pub mod mon_handler;
}
Étape 4 : Créer la struct de template
Dans crates/gitrust-web/src/templates.rs, ajoutez la struct Askama :
#![allow(unused)]
fn main() {
#[derive(Template)]
#[template(path = "settings/mon_endpoint.html")]
pub struct MonPageTemplate {
pub username: String,
pub is_admin: bool,
pub current_path: String,
pub data: MonData, // votre DTO métier
}
}
Les champs username, is_admin, et current_path sont présents sur toutes les structs de template — ils alimentent la sidebar contextuelle.
Étape 5 : Créer le template Askama
Créez crates/gitrust-web/templates/settings/mon_endpoint.html :
{% extends "base.html" %}
{% block title %}Mon endpoint — gitrust{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<h1 class="text-2xl font-bold mb-6">Mon endpoint</h1>
<form method="POST" action="/settings/mon-endpoint">
<input type="hidden" name="_csrf" value="{{ csrf_token }}">
<div class="form-control mb-4">
<label class="label">
<span class="label-text">Valeur</span>
</label>
<input
type="text"
name="valeur"
value="{{ data.valeur }}"
class="input input-bordered"
required
>
</div>
<button type="submit" class="btn btn-primary">Enregistrer</button>
</form>
</div>
{% endblock %}
Règle CSRF : tout formulaire POST doit inclure <input type="hidden" name="_csrf" value="{{ csrf_token }}">. Le middleware rustwarden-core rejette les requêtes sans token CSRF valide avec 403.
Étape 6 : Gestion des erreurs dans les templates
Pour afficher les erreurs de validation à l’utilisateur, utilisez le pattern flash :
#![allow(unused)]
fn main() {
// Dans le handler POST, en cas d'erreur de validation :
return Ok(axum::response::Redirect::to(
"/settings/mon-endpoint?error=valeur+invalide"
));
}
Dans le template :
{% if let Some(err) = query_params.error %}
<div class="alert alert-error">{{ err }}</div>
{% endif %}
Étape 7 : Écrire les tests E2E Playwright
Créez tests/e2e/mon_endpoint.spec.ts :
import { test, expect } from "@playwright/test";
test("la page mon-endpoint est accessible après login", async ({ page }) => {
// Authentification (helper existant)
await page.goto("/login");
await page.fill("[name=username]", "alice");
await page.fill("[name=password]", "SecurePass123!");
await page.click("[type=submit]");
await page.goto("/settings/mon-endpoint");
await expect(page).toHaveTitle(/Mon endpoint/);
});
test("soumettre le formulaire redirige vers la même page", async ({ page }) => {
// ... login ...
await page.goto("/settings/mon-endpoint");
await page.fill("[name=valeur]", "nouvelle-valeur");
await page.click("[type=submit]");
await expect(page).toHaveURL("/settings/mon-endpoint");
});
Lancez les tests E2E :
npm run test:e2e
Exemple complet : la page de gestion des labels
Pour un exemple réel de ce patron appliqué à une feature complète, consultez :
- Route :
crates/gitrust-web/src/routes.rs— section Issues / Labels - Handler :
crates/gitrust-web/src/handlers/labels.rs - Template :
crates/gitrust-web/templates/repository/labels.html - Service :
crates/gitrust-core/src/services/label_service.rs - Tests E2E :
tests/e2e/labels.spec.ts
Voir aussi
Ajouter un service métier dans gitrust-core
Ce guide couvre la création d’un nouveau service dans crates/gitrust-core/src/services/, en respectant les conventions du projet.
Pré-requis
- Lecture de
reference/services-api-interne.mdpour comprendre les patterns existants. - Migration de base de données créée si nécessaire (voir
ajouter-migration-db.md).
Convention générale
Tous les services gitrust-core suivent ce patron uniforme :
Handler (axum) → Service (logique métier) → SeaORM (base de données)
Règles structurelles :
- Les services sont des structs sans état (pas de champs, uniquement des méthodes
async fnassociées). - Le premier paramètre de chaque méthode est toujours
db: &DatabaseConnection. - Le type de retour est toujours
Result<T, GitrustError>. - Aucun
unwrap()/expect()/panic!()en production. - Aucun SQL brut — tout passe par SeaORM.
Créer le fichier du service
Créez crates/gitrust-core/src/services/mon_service.rs :
#![allow(unused)]
fn main() {
use sea_orm::{
ActiveModelTrait, ActiveValue::Set, DatabaseConnection, EntityTrait,
};
use uuid::Uuid;
use crate::{
dto::mon_dto::{CreateMonInput, MonOutput},
error::GitrustError,
models::mon_model,
};
pub struct MonService;
impl MonService {
/// Crée un nouvel élément.
///
/// # Erreurs
/// - `GitrustError::Validation` si le nom est vide ou trop long.
/// - `GitrustError::Conflict` si un élément avec le même nom existe déjà.
/// - `GitrustError::Database` pour toute erreur SeaORM.
pub async fn create(
db: &DatabaseConnection,
owner_id: Uuid,
input: CreateMonInput,
) -> Result<mon_model::Model, GitrustError> {
// 1. Validation aux frontières
if input.name.is_empty() || input.name.len() > 64 {
return Err(GitrustError::Validation(
"Le nom doit comporter entre 1 et 64 caractères".into(),
));
}
// 2. Vérification unicité
let existing = mon_model::Entity::find()
.filter(mon_model::Column::OwnerId.eq(owner_id))
.filter(mon_model::Column::Name.eq(&input.name))
.one(db)
.await
.map_err(GitrustError::Database)?;
if existing.is_some() {
return Err(GitrustError::Conflict(format!(
"Un élément nommé '{}' existe déjà",
input.name
)));
}
// 3. Insertion
let now = chrono::Utc::now();
let model = mon_model::ActiveModel {
id: Set(Uuid::new_v4()),
owner_id: Set(owner_id),
name: Set(input.name),
description: Set(input.description),
created_at: Set(now),
updated_at: Set(now),
};
model.insert(db).await.map_err(GitrustError::Database)
}
/// Liste les éléments d'un propriétaire.
pub async fn list_by_owner(
db: &DatabaseConnection,
owner_id: Uuid,
) -> Result<Vec<mon_model::Model>, GitrustError> {
mon_model::Entity::find()
.filter(mon_model::Column::OwnerId.eq(owner_id))
.order_by_asc(mon_model::Column::Name)
.all(db)
.await
.map_err(GitrustError::Database)
}
/// Trouve un élément par ID et vérifie l'appartenance.
///
/// # Erreurs
/// - `GitrustError::NotFound` si l'ID est inconnu.
/// - `GitrustError::Forbidden` si `owner_id` ne correspond pas (anti-IDOR).
pub async fn find_by_id(
db: &DatabaseConnection,
id: Uuid,
owner_id: Uuid,
) -> Result<mon_model::Model, GitrustError> {
let model = mon_model::Entity::find_by_id(id)
.one(db)
.await
.map_err(GitrustError::Database)?
.ok_or_else(|| GitrustError::NotFound("Élément introuvable".into()))?;
// Anti-IDOR : vérification d'ownership systématique
if model.owner_id != owner_id {
return Err(GitrustError::Forbidden);
}
Ok(model)
}
/// Supprime un élément (vérifie l'ownership).
pub async fn delete(
db: &DatabaseConnection,
id: Uuid,
owner_id: Uuid,
) -> Result<(), GitrustError> {
let model = Self::find_by_id(db, id, owner_id).await?;
mon_model::Entity::delete_by_id(model.id)
.exec(db)
.await
.map_err(GitrustError::Database)?;
Ok(())
}
}
}
Déclarer le module dans services/mod.rs
Ouvrez crates/gitrust-core/src/services/mod.rs et ajoutez :
#![allow(unused)]
fn main() {
pub mod mon_service;
}
Injecter le service dans un handler via State
Les services gitrust ne sont pas des structs instanciées — leurs méthodes sont associées (pas de self). Vous n’avez pas besoin de les injecter dans le State d’Axum. Il suffit d’appeler la méthode statique depuis le handler :
#![allow(unused)]
fn main() {
use gitrust_core::services::mon_service::MonService;
pub async fn list_handler(
State(db): State<DatabaseConnection>,
user: AuthUser,
) -> Result<impl IntoResponse, AppError> {
let items = MonService::list_by_owner(&db, user.user_id).await?;
Ok(MonListTemplate { items, username: user.username, is_admin: user.has_role("admin"), current_path: "/mon-chemin".into() })
}
}
Écrire les tests unitaires
Gitrust impose des tests sur vraie DB (pas de mocks) pour la couche persistance. Utilisez une base SQLite in-memory pour les tests unitaires rapides, ou testcontainers PostgreSQL pour les tests qui dépendent de comportements PG spécifiques (contraintes, index partiels, etc.).
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
use sea_orm::{Database, DatabaseConnection};
async fn setup_db() -> DatabaseConnection {
let db = Database::connect("sqlite::memory:").await.unwrap();
// Appliquer les migrations nécessaires
crate::migrations::run_migrations(&db).await.unwrap();
db
}
#[tokio::test]
async fn create_inserts_model() {
let db = setup_db().await;
let owner = Uuid::new_v4();
let result = MonService::create(
&db,
owner,
CreateMonInput {
name: "mon-element".into(),
description: None,
},
)
.await;
assert!(result.is_ok());
let model = result.unwrap();
assert_eq!(model.name, "mon-element");
assert_eq!(model.owner_id, owner);
}
#[tokio::test]
async fn create_rejects_empty_name() {
let db = setup_db().await;
let err = MonService::create(
&db,
Uuid::new_v4(),
CreateMonInput { name: "".into(), description: None },
)
.await
.unwrap_err();
assert!(matches!(err, GitrustError::Validation(_)));
}
#[tokio::test]
async fn find_by_id_rejects_wrong_owner() {
let db = setup_db().await;
let owner = Uuid::new_v4();
let other = Uuid::new_v4();
let model = MonService::create(
&db,
owner,
CreateMonInput { name: "priv".into(), description: None },
)
.await
.unwrap();
// Un autre utilisateur ne doit pas accéder à cet élément
let err = MonService::find_by_id(&db, model.id, other).await.unwrap_err();
assert!(matches!(err, GitrustError::Forbidden));
}
}
}
Gestion des erreurs — variantes GitrustError
| Variante | Code HTTP | Quand l’utiliser |
|---|---|---|
GitrustError::NotFound(msg) | 404 | Ressource introuvable par ID |
GitrustError::Validation(msg) | 400 | Input utilisateur invalide |
GitrustError::Forbidden | 403 | Ownership non vérifiée (anti-IDOR) |
GitrustError::Conflict(msg) | 409 | Contrainte d’unicité violée |
GitrustError::Database(err) | 500 | Erreur SeaORM remontée telle quelle |
N’utilisez jamais GitrustError::Database pour une erreur de validation — la distinction est importante pour les messages d’erreur utilisateur.
Voir aussi
Ajouter une migration de base de données (SeaORM)
Ce guide couvre la création, l’application et le rollback d’une migration SeaORM dans gitrust.
Pré-requis
- Environnement local fonctionnel (voir
tutorials/01-getting-started.md). - PostgreSQL local démarré.
Convention de nommage
Chaque fichier de migration suit le format :
m{AAAAMMJJ}_{NNNNNN}_{description_snake_case}.rs
AAAAMMJJ: date du jour en UTC.NNNNNN: numéro séquentiel sur 6 chiffres, incrémenté par rapport au dernier fichier existant.description_snake_case: description courte en minuscules.
Exemple : m20260501_000023_create_notify_jobs.rs
Consultez le tableau des migrations existantes dans reference/schema-base-donnees.md avant d’attribuer un numéro pour éviter les collisions.
Structure d’un fichier de migration
Créez crates/gitrust-core/src/migrations/m20260501_000023_create_notify_jobs.rs :
#![allow(unused)]
fn main() {
use sea_orm_migration::prelude::*;
/// Nom de migration dérivé automatiquement du nom du module.
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(NotifyJobs::Table)
.if_not_exists()
.col(ColumnDef::new(NotifyJobs::Id).uuid().not_null().primary_key())
.col(ColumnDef::new(NotifyJobs::OwnerId).uuid().not_null())
.col(
ColumnDef::new(NotifyJobs::Status)
.string()
.not_null()
.default("pending"),
)
.col(ColumnDef::new(NotifyJobs::ErrorMessage).text().null())
.col(
ColumnDef::new(NotifyJobs::CreatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.col(
ColumnDef::new(NotifyJobs::UpdatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.to_owned(),
)
.await?;
// Index composite pour les requêtes fréquentes
manager
.create_index(
Index::create()
.table(NotifyJobs::Table)
.name("notify_jobs_owner_status_idx")
.col(NotifyJobs::OwnerId)
.col(NotifyJobs::Status)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(NotifyJobs::Table).to_owned())
.await
}
}
/// Identifiant Iden pour chaque colonne et la table.
#[derive(DeriveIden)]
enum NotifyJobs {
Table,
Id,
OwnerId,
Status,
ErrorMessage,
CreatedAt,
UpdatedAt,
}
}
Règles obligatoires :
up()doit toujours utiliser.if_not_exists()pour être idempotent.down()doit annuler exactement ce queup()a créé. Toute migration sansdown()fonctionnel sera refusée en review.- Les colonnes
created_atetupdated_atsontTIMESTAMPTZ NOT NULL(pasTIMESTAMP— gitrust est UTC strict). - Les clés primaires sont des
UUID(jamais des entiers auto-incrémentés).
Enregistrer la migration dans le Migrator
Ouvrez crates/gitrust-core/src/migrations/mod.rs et ajoutez la migration dans la liste du Migrator en respectant l’ordre chronologique :
#![allow(unused)]
fn main() {
pub mod m20260501_000023_create_notify_jobs;
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![
// ... migrations existantes dans l'ordre ...
Box::new(m20260501_000023_create_notify_jobs::Migration),
]
}
}
}
L’ordre est strict : SeaORM exécute les migrations dans l’ordre de la liste. Ne réorganisez jamais les migrations existantes.
Appliquer la migration en local
cargo run --bin gitrust -- migrate
Sortie attendue :
Applying migration 'm20260501_000023_create_notify_jobs'
Migration applied successfully
Vérifiez la table dans PostgreSQL :
psql $DATABASE_URL -c "\d notify_jobs"
Tester le rollback
cargo run --bin gitrust -- migrate down
Sortie attendue :
Rolling back migration 'm20260501_000023_create_notify_jobs'
Rollback applied successfully
Vérifiez que la table a bien disparu :
psql $DATABASE_URL -c "\dt notify_jobs"
# → doit retourner "no relations found"
Cas d’usage courants
Ajouter une colonne à une table existante
#![allow(unused)]
fn main() {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(Repositories::Table)
.add_column(
ColumnDef::new(Repositories::IsArchived)
.boolean()
.not_null()
.default(false),
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(Repositories::Table)
.drop_column(Repositories::IsArchived)
.to_owned(),
)
.await
}
}
Créer un index unique composite
#![allow(unused)]
fn main() {
manager
.create_index(
Index::create()
.table(Labels::Table)
.name("labels_owner_repo_name_type_idx")
.col(Labels::OwnerId)
.col(Labels::RepositoryId)
.col(Labels::Name)
.col(Labels::LabelType)
.unique()
.to_owned(),
)
.await
}
Gotchas PostgreSQL
| Situation | Comportement | Solution |
|---|---|---|
Ajouter une colonne NOT NULL sans DEFAULT sur une table peuplée | Erreur PG : column cannot be added without a default value | Toujours fournir un DEFAULT lors de l’ajout d’une colonne NOT NULL |
ALTER TABLE ... DROP COLUMN avec des FK dépendantes | Erreur PG : contrainte FK bloquante | Supprimer d’abord les FK avec ForeignKey::drop() dans down() |
TIMESTAMPTZ vs TIMESTAMP | TIMESTAMP ignore le fuseau horaire, crée des bugs sur les serveurs non-UTC | Utiliser toujours timestamp_with_time_zone() |
| Migration dans une transaction | Toutes les migrations gitrust s’exécutent dans une transaction implicite | Ne pas appeler BEGIN/COMMIT manuellement dans up() |
Voir aussi
Implémenter des webhooks dans gitrust
Ce guide couvre l’ajout d’un nouveau déclencheur webhook : modèle de données, service de dispatch, signature HMAC, configuration UI par dépôt, et rotation de secret.
Pré-requis
- Lecture de
ajouter-service-metier.mdetajouter-migration-db.md. - Compréhension du patron vertical slice (voir
tutorials/01-getting-started.md).
Vue d’ensemble
sequenceDiagram
participant Push as git push
participant Hook as receive-pack handler
participant Svc as WebhookService
participant Disp as WebhookDispatcher
participant Ext as Endpoint HTTP distant
Push->>Hook: réception des objets
Hook->>Svc: list_by_repo_and_event(repo_id, "push")
Svc-->>Hook: Vec<Webhook>
Hook->>Disp: deliver(webhook, "push", payload)
Disp->>Ext: POST url (payload signé HMAC-SHA256)
Ext-->>Disp: 200 OK
Disp->>Svc: record_delivery(webhook_id, status, duration)
Modèle de données
La table webhooks stocke la configuration par dépôt :
CREATE TABLE webhooks (
id UUID PRIMARY KEY,
repository_id UUID NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,
url TEXT NOT NULL,
secret_hash TEXT NOT NULL, -- HMAC secret haché SHA-256 (jamais en clair)
events TEXT NOT NULL, -- JSON array : ["push","pull_request","issues"]
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX webhooks_repo_active_idx ON webhooks(repository_id, is_active);
La table webhook_deliveries trace chaque envoi :
CREATE TABLE webhook_deliveries (
id UUID PRIMARY KEY,
webhook_id UUID NOT NULL REFERENCES webhooks(id) ON DELETE CASCADE,
event TEXT NOT NULL,
payload TEXT NOT NULL,
response_status INTEGER NULL,
response_body TEXT NULL,
duration_ms INTEGER NULL,
success BOOLEAN NOT NULL,
created_at TIMESTAMPTZ NOT NULL
);
Créez les deux migrations correspondantes (voir ajouter-migration-db.md).
WebhookService — méthodes publiques
#![allow(unused)]
fn main() {
// crates/gitrust-core/src/services/webhook_service.rs
pub struct WebhookService;
impl WebhookService {
/// Crée un webhook pour un dépôt.
pub async fn create(
db: &DatabaseConnection,
repo_id: Uuid,
url: String,
secret: &str, // secret en clair — haché avant stockage
events: Vec<String>,
) -> Result<webhook::Model, GitrustError>;
/// Liste les webhooks actifs pour un dépôt et un événement donné.
pub async fn list_by_repo_and_event(
db: &DatabaseConnection,
repo_id: Uuid,
event: &str,
) -> Result<Vec<webhook::Model>, GitrustError>;
/// Met à jour l'URL, le secret ou les événements.
pub async fn update(
db: &DatabaseConnection,
webhook_id: Uuid,
repo_id: Uuid, // anti-IDOR : vérifie l'appartenance
url: Option<String>,
new_secret: Option<&str>,
events: Option<Vec<String>>,
) -> Result<webhook::Model, GitrustError>;
/// Désactive / active un webhook.
pub async fn set_active(
db: &DatabaseConnection,
webhook_id: Uuid,
repo_id: Uuid,
active: bool,
) -> Result<(), GitrustError>;
/// Supprime un webhook et ses deliveries en cascade.
pub async fn delete(
db: &DatabaseConnection,
webhook_id: Uuid,
repo_id: Uuid,
) -> Result<(), GitrustError>;
/// Enregistre le résultat d'une livraison.
pub async fn record_delivery(
db: &DatabaseConnection,
webhook_id: Uuid,
event: &str,
payload: &str,
response_status: Option<i32>,
response_body: Option<&str>,
duration_ms: i32,
success: bool,
) -> Result<(), GitrustError>;
/// Liste les dernières deliveries d'un webhook (pagination).
pub async fn list_deliveries(
db: &DatabaseConnection,
webhook_id: Uuid,
repo_id: Uuid,
page: u64,
per_page: u64,
) -> Result<Vec<webhook_delivery::Model>, GitrustError>;
}
}
Sécurité — stockage du secret : le secret ne doit jamais être stocké en clair. Lors de la création ou mise à jour, hachez-le avec SHA-256 avant insertion. La signature HMAC utilise le secret en clair fourni par l’utilisateur au moment de la création (retourné une seule fois) ou récupéré depuis un secret manager.
#![allow(unused)]
fn main() {
fn hash_secret(secret: &str) -> String {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(secret.as_bytes());
format!("{:x}", hasher.finalize())
}
}
WebhookDispatcher — envoi avec retry et signature HMAC
#![allow(unused)]
fn main() {
// crates/gitrust-core/src/services/webhook_dispatcher.rs
pub struct WebhookDispatcher;
impl WebhookDispatcher {
/// Envoie le payload à l'URL du webhook avec signature HMAC-SHA256.
/// Retry exponentiel : 3 tentatives (0s, 5s, 25s).
pub async fn deliver(
webhook: &webhook::Model,
event: &str,
payload: serde_json::Value,
secret_plain: &str, // secret en clair pour la signature
) -> DeliveryResult;
}
pub struct DeliveryResult {
pub success: bool,
pub response_status: Option<i32>,
pub response_body: Option<String>,
pub duration_ms: i32,
pub attempts: u32,
}
}
Calcul de la signature HMAC
#![allow(unused)]
fn main() {
use hmac::{Hmac, Mac};
use sha2::Sha256;
fn compute_hmac_signature(secret: &str, body: &str) -> String {
let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes())
.expect("HMAC accepte toutes les longueurs de clé");
mac.update(body.as_bytes());
let result = mac.finalize();
format!("sha256={}", hex::encode(result.into_bytes()))
}
}
Le header HTTP envoyé avec chaque livraison :
X-Gitrust-Signature-256: sha256=<hmac_hex>
X-Gitrust-Event: push
X-Gitrust-Delivery: <uuid de la delivery>
Content-Type: application/json
Retry exponentiel
#![allow(unused)]
fn main() {
const RETRY_DELAYS: [u64; 3] = [0, 5, 25]; // secondes
for (attempt, delay) in RETRY_DELAYS.iter().enumerate() {
if *delay > 0 {
tokio::time::sleep(Duration::from_secs(*delay)).await;
}
match client.post(&webhook.url).send().await {
Ok(resp) if resp.status().is_success() => return Ok(DeliveryResult { success: true, attempts: attempt + 1, .. }),
Ok(resp) => last_error = format!("HTTP {}", resp.status()),
Err(e) => last_error = e.to_string(),
}
}
}
Règle SSRF : l’URL du webhook est validée à la création. Les adresses RFC1918 (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) et localhost sont interdites en production.
Injecter le dispatch dans le hook post-receive
Dans le handler receive_pack (crates/gitrust-web/src/handlers/git_http.rs), après la réception des objets Git :
#![allow(unused)]
fn main() {
// Déclencher les webhooks "push"
let payload = serde_json::json!({
"repository": { "id": repo.id, "slug": repo.slug, "owner": owner.username },
"pusher": { "username": user.username },
"ref": pushed_ref,
"commits": commit_summaries,
});
let webhooks = WebhookService::list_by_repo_and_event(&db, repo.id, "push").await?;
for webhook in webhooks {
// Le dispatch est non-bloquant : on ne veut pas bloquer le push si le
// serveur distant est lent.
let db_clone = db.clone();
let payload_clone = payload.clone();
tokio::spawn(async move {
let result = WebhookDispatcher::deliver(&webhook, "push", payload_clone, &secret_plain).await;
let _ = WebhookService::record_delivery(
&db_clone,
webhook.id,
"push",
&payload_clone.to_string(),
result.response_status,
result.response_body.as_deref(),
result.duration_ms,
result.success,
).await;
});
}
}
UI de configuration par dépôt
Ajoutez les routes dans routes.rs :
#![allow(unused)]
fn main() {
.route(
"/{owner}/{repo}/settings/webhooks",
get(handlers::webhooks::list_webhooks).post(handlers::webhooks::create_webhook),
)
.route(
"/{owner}/{repo}/settings/webhooks/{id}",
get(handlers::webhooks::webhook_detail),
)
.route(
"/{owner}/{repo}/settings/webhooks/{id}/edit",
post(handlers::webhooks::edit_webhook),
)
.route(
"/{owner}/{repo}/settings/webhooks/{id}/delete",
post(handlers::webhooks::delete_webhook),
)
.route(
"/{owner}/{repo}/settings/webhooks/{id}/test",
post(handlers::webhooks::test_webhook),
)
}
Rotation du secret
Quand un utilisateur régénère le secret via l’UI, le nouveau secret est :
- Affiché une seule fois à l’utilisateur (jamais re-lisible depuis l’UI).
- Haché SHA-256 et stocké dans
webhooks.secret_hash. - Utilisé immédiatement pour les livraisons suivantes.
Les livraisons en cours avec l’ancien secret ne sont pas réessayées — informez l’utilisateur dans l’UI.
Checklist de test
- Créer un webhook avec URL + secret + événements → visible dans la liste.
- Push sur le dépôt → delivery créée avec statut
successoufailed. - Désactiver le webhook → push ne déclenche aucune livraison.
- URL RFC1918 → rejet à la création avec message d’erreur.
- Bouton « Test » → delivery avec événement
pingcréée. - Rotation du secret → livraisons suivantes signées avec le nouveau secret.
Voir aussi
Implémenter un endpoint REST /api/v1/...
Ce guide couvre l’ajout d’un nouvel endpoint JSON à l’API REST v1 de gitrust : modèle de requête/réponse, authentification PAT/JWT, codes d’erreur standardisés, et tests.
Pré-requis
- Lecture de
ajouter-service-metier.md. - Référence complète de l’API :
reference/api-rest-v1.md.
Vue d’ensemble
Les handlers API et les handlers SSR partagent les mêmes services. La seule différence est le format de réponse :
Handler SSR → service → réponse HTML (template Askama)
Handler API → service → réponse JSON (serde_json)
Ne dupliquez jamais la logique métier entre un handler SSR et un handler API.
Structure des handlers API
Les handlers API se trouvent dans crates/gitrust-web/src/handlers/api/. Chaque domaine a son fichier :
handlers/api/
├── repos.rs — dépôts, branches, commits
├── issues.rs — issues et commentaires
├── pulls.rs — pull requests
├── ci.rs — pipelines CI
├── user.rs — profil utilisateur courant
└── docs.rs — Swagger UI
Étape 1 : Définir les types de requête et réponse
Créez ou complétez les DTOs dans crates/gitrust-core/src/dto/ :
#![allow(unused)]
fn main() {
// crates/gitrust-core/src/dto/mon_api_dto.rs
use serde::{Deserialize, Serialize};
use uuid::Uuid;
/// Corps de la requête POST /api/v1/repos/{owner}/{repo}/mon-resource
#[derive(Debug, Deserialize)]
pub struct CreateMonResourceRequest {
pub name: String,
pub description: Option<String>,
}
/// Réponse JSON pour un élément unique
#[derive(Debug, Serialize)]
pub struct MonResourceResponse {
pub id: Uuid,
pub name: String,
pub description: Option<String>,
pub created_at: chrono::DateTime<chrono::Utc>,
}
/// Réponse JSON pour une liste paginée
#[derive(Debug, Serialize)]
pub struct PaginatedResponse<T> {
pub items: Vec<T>,
pub total: u64,
pub page: u64,
pub per_page: u64,
}
}
Étape 2 : Déclarer la route dans routes.rs
Dans la fonction api_v1_routes() de crates/gitrust-web/src/routes.rs :
#![allow(unused)]
fn main() {
.route(
"/api/v1/repos/{owner}/{repo}/mon-resource",
get(handlers::api::mon_resource::list).post(handlers::api::mon_resource::create),
)
.route(
"/api/v1/repos/{owner}/{repo}/mon-resource/{id}",
get(handlers::api::mon_resource::detail)
.patch(handlers::api::mon_resource::update)
.delete(handlers::api::mon_resource::delete_resource),
)
}
Étape 3 : Implémenter le handler
#![allow(unused)]
fn main() {
// crates/gitrust-web/src/handlers/api/mon_resource.rs
use axum::{
extract::{Path, Query, State},
http::StatusCode,
response::IntoResponse,
Json,
};
use sea_orm::DatabaseConnection;
use serde::Deserialize;
use crate::auth::AuthUser;
use gitrust_core::{
dto::mon_api_dto::{CreateMonResourceRequest, MonResourceResponse, PaginatedResponse},
services::mon_service::MonService,
};
#[derive(Deserialize)]
pub struct PaginationParams {
#[serde(default = "default_page")]
pub page: u64,
#[serde(default = "default_per_page")]
pub per_page: u64,
}
fn default_page() -> u64 { 1 }
fn default_per_page() -> u64 { 30 }
/// GET /api/v1/repos/{owner}/{repo}/mon-resource
pub async fn list(
State(db): State<DatabaseConnection>,
Path((owner, repo)): Path<(String, String)>,
Query(pagination): Query<PaginationParams>,
user: AuthUser,
) -> impl IntoResponse {
let per_page = pagination.per_page.min(100); // cap à 100
match MonService::list_paginated(
&db, &owner, &repo, user.user_id, pagination.page, per_page,
)
.await
{
Ok((items, total)) => {
let response = PaginatedResponse {
items: items.into_iter().map(MonResourceResponse::from).collect(),
total,
page: pagination.page,
per_page,
};
let mut headers = axum::http::HeaderMap::new();
headers.insert("X-Total-Count", total.to_string().parse().unwrap());
headers.insert("X-Page", pagination.page.to_string().parse().unwrap());
headers.insert("X-Per-Page", per_page.to_string().parse().unwrap());
(StatusCode::OK, headers, Json(response)).into_response()
}
Err(e) => api_error(e).into_response(),
}
}
/// POST /api/v1/repos/{owner}/{repo}/mon-resource
pub async fn create(
State(db): State<DatabaseConnection>,
Path((owner, repo)): Path<(String, String)>,
user: AuthUser,
Json(body): Json<CreateMonResourceRequest>,
) -> impl IntoResponse {
match MonService::create_for_repo(&db, &owner, &repo, user.user_id, body.into()).await {
Ok(model) => (StatusCode::CREATED, Json(MonResourceResponse::from(model))).into_response(),
Err(e) => api_error(e).into_response(),
}
}
}
Authentification : PAT et JWT Bearer
L’extracteur AuthUser supporte deux modes d’authentification :
- Cookie JWT (
jwt_token) — utilisé par le navigateur web. - Header
Authorization: Bearer <token>— utilisé par les clients API avec un PAT ou un JWT.
Les Personal Access Tokens sont vérifiés via PatService::validate_token() dans le middleware. Aucune modification n’est nécessaire côté handler — AuthUser gère les deux méthodes transparentement.
Codes de retour en cas d’échec d’auth :
401 Unauthorized— token absent, expiré, ou invalide. Réponse JSON, jamais de redirect HTML.403 Forbidden— token valide mais permissions insuffisantes.
Format des erreurs JSON standardisé
Toutes les erreurs API retournent le même format :
{
"error": "not_found",
"message": "Le dépôt 'alice/myrepo' est introuvable",
"status": 404
}
La fonction utilitaire api_error à placer dans handlers/api/mod.rs :
#![allow(unused)]
fn main() {
use axum::{http::StatusCode, response::IntoResponse, Json};
use serde_json::json;
use gitrust_core::error::GitrustError;
pub fn api_error(err: GitrustError) -> impl IntoResponse {
let (status, code) = match &err {
GitrustError::NotFound(_) => (StatusCode::NOT_FOUND, "not_found"),
GitrustError::Validation(_) => (StatusCode::BAD_REQUEST, "validation_error"),
GitrustError::Forbidden => (StatusCode::FORBIDDEN, "forbidden"),
GitrustError::Conflict(_) => (StatusCode::CONFLICT, "conflict"),
_ => (StatusCode::INTERNAL_SERVER_ERROR, "internal_error"),
};
(
status,
Json(json!({
"error": code,
"message": err.to_string(),
"status": status.as_u16()
})),
)
}
}
Pagination
Tous les endpoints de liste supportent ?page=N&per_page=N (défaut : page=1, per_page=30, max per_page=100).
Headers de réponse :
X-Total-Count: 142
X-Page: 2
X-Per-Page: 30
Link: <https://demo.gitrust.eu/api/v1/repos/alice/myrepo/issues?page=1&per_page=30>; rel="prev",
<https://demo.gitrust.eu/api/v1/repos/alice/myrepo/issues?page=3&per_page=30>; rel="next"
Rate limiting
Les endpoints API sont couverts par le middleware de rate limiting de rustwarden-core :
| Catégorie | Limite | Fenêtre |
|---|---|---|
| Endpoints lecture | 1000 req | 1 heure |
| Endpoints écriture | 200 req | 1 heure |
| Trigger CI manuel | 10 req | 1 heure |
En cas de dépassement : 429 Too Many Requests avec header Retry-After: <secondes>.
Documenter l’endpoint (OpenAPI)
Ajoutez les annotations utoipa sur le handler :
#![allow(unused)]
fn main() {
#[utoipa::path(
get,
path = "/api/v1/repos/{owner}/{repo}/mon-resource",
params(
("owner" = String, Path, description = "Propriétaire du dépôt"),
("repo" = String, Path, description = "Slug du dépôt"),
("page" = Option<u64>, Query, description = "Numéro de page (défaut: 1)"),
("per_page" = Option<u64>, Query, description = "Éléments par page (défaut: 30, max: 100)"),
),
responses(
(status = 200, description = "Liste paginée", body = PaginatedResponse<MonResourceResponse>),
(status = 401, description = "Non authentifié"),
(status = 403, description = "Accès refusé"),
(status = 404, description = "Dépôt introuvable"),
),
security(("bearer_token" = []), ("cookie_auth" = []))
)]
pub async fn list(...) { ... }
}
Tester l’endpoint
Test curl rapide :
# Avec un PAT
curl -H "Authorization: Bearer <votre-pat>" \
https://demo.gitrust.eu/api/v1/repos/alice/myrepo/mon-resource
# Vérifier le format d'erreur sur auth invalide
curl -H "Authorization: Bearer token-invalide" \
https://demo.gitrust.eu/api/v1/repos/alice/myrepo/mon-resource
# → {"error":"unauthorized","message":"Token invalide","status":401}
Test d’intégration Rust :
#![allow(unused)]
fn main() {
#[tokio::test]
async fn list_returns_401_without_auth() {
let app = setup_test_app().await;
let response = app
.oneshot(
Request::builder()
.uri("/api/v1/repos/alice/myrepo/mon-resource")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
let body: serde_json::Value = parse_json_body(response).await;
assert_eq!(body["error"], "unauthorized");
}
#[tokio::test]
async fn create_returns_201_with_valid_pat() {
let app = setup_test_app().await;
let pat = create_test_pat(&app).await;
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/api/v1/repos/alice/myrepo/mon-resource")
.header("Authorization", format!("Bearer {pat}"))
.header("Content-Type", "application/json")
.body(Body::from(r#"{"name":"test"}"#))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::CREATED);
}
}
Voir aussi
Importer un dépôt externe
Référence du flux d’import de dépôts externes dans gitrust : rôle de la base de données, schéma de séquence complet et pistes d’optimisation.
1. Rôle de la base de données
Pendant un import, la DB sert quatre fonctions distinctes :
| Rôle | Table | Fréquence |
|---|---|---|
| File d’attente persistante | import_jobs | 1 INSERT à la création |
| Suivi d’état pour l’UI | import_jobs | 1 UPDATE toutes les ~1.5 s |
| Enregistrement final du dépôt | repositories + resources | 2 INSERT à la fin |
| Journal d’audit | audit_log | 1 INSERT à la fin |
Le clone lui-même ne passe jamais par la DB : seuls les métadonnées et compteurs de progression y transitent. Les objets Git sont écrits directement sur disque dans {GIT_REPOS_BASE_PATH}/{owner}/{slug}.git/.
Pourquoi persister l’état ?
- Reprise après redémarrage : le serveur peut crasher pendant un clone de plusieurs minutes. La table permet de marquer ces jobs comme
failedau redémarrage. - SSE : l’UI lit la progression via un endpoint SSE qui interroge la DB toutes les 2 s. Sans DB, il faudrait un canal in-memory plus un mécanisme de routage vers le bon client.
- Multi-navigateurs : si l’utilisateur ferme l’onglet puis le rouvre, il retrouve l’état exact du job.
- Audit / historique : conservation des imports passés avec leurs statistiques.
2. Schéma du flux actuel
sequenceDiagram
autonumber
participant UI as Navigateur
participant H as Handler HTTP
participant Svc as ImportService
participant Chan as mpsc Channel
participant W as ImportWorker
participant G as git2 (libgit2)
participant DB as PostgreSQL
participant FS as Disque
UI->>H: POST /import<br/>(url, slug, pat)
H->>Svc: create_job
Svc->>DB: INSERT import_jobs (pending)
H->>Chan: try_send(ImportTask+PAT)
H-->>UI: 302 /imports/{id}
UI->>H: GET /imports/{id}/stream (SSE)
Note over UI,H: EventSource ouvert
Chan->>W: ImportTask
W->>Svc: mark_running
Svc->>DB: SELECT import_jobs<br/>UPDATE status=running
W->>G: RepoBuilder.bare(true).clone()
G->>FS: init_bare + fetch
loop callbacks transfer_progress (flood)
G-->>W: stats (objets/bytes)
alt throttle 1500 ms écoulé
W->>Svc: update_progress (tokio::spawn)
Svc->>DB: SELECT + UPDATE import_jobs
else dans le throttle
W--xW: ignore
end
end
loop toutes les 2 s
H->>DB: SELECT import_jobs WHERE id=?
DB-->>H: état courant
H-->>UI: SSE event (JSON)
UI->>UI: bar.value = percent
end
G-->>FS: objets écrits
G-->>W: Ok
W->>Svc: check cancel (SELECT import_jobs)
W->>Svc: update_progress (Finalizing)
Svc->>DB: SELECT + UPDATE
W->>RepositoryService: create
RepositoryService->>DB: INSERT resources<br/>INSERT repositories
W->>DB: UPDATE repositories<br/>(import_source_url, is_empty=false)
W->>Svc: mark_success
Svc->>DB: SELECT + UPDATE import_jobs<br/>(status=success, duration)
W->>AuditService: log REPO_IMPORTED
AuditService->>DB: INSERT audit_log
H->>DB: SELECT (prochain tick SSE)
H-->>UI: terminal=true
UI->>UI: window.location.reload()
3. Coût DB par import
Pour un clone de 90 secondes avec throttle 1500 ms :
| Étape | Opérations DB | Cumul |
|---|---|---|
| create_job | 1 INSERT | 1 |
| mark_running | 1 SELECT + 1 UPDATE | 3 |
| update_progress pendant clone | ~60 x (1 SELECT + 1 UPDATE) | 123 |
| check cancel | 1 SELECT | 124 |
| update_progress finalizing | 1 SELECT + 1 UPDATE | 126 |
| RepositoryService::create | 2 INSERT (repo + resource) | 128 |
| update repositories | 1 UPDATE | 129 |
| mark_success | 1 SELECT + 1 UPDATE | 131 |
| audit log | 1 INSERT | 132 |
| SSE stream (45 ticks à 2 s) | 45 SELECT | 177 |
~177 requêtes pour un import, dont 120+ sont des progress updates.
4. Pistes d’optimisation (sans changer libgit2)
4.1 Éliminer le SELECT avant UPDATE dans update_progress
update_progress fait actuellement load_model (SELECT) puis active.update(db) (UPDATE + RETURNING). Le SELECT est redondant : on connaît l’ID, on veut juste patcher 4 colonnes.
Gain : divise par 2 les requêtes de progression (~60 requêtes en moins pour un clone de 90 s).
Refonte suggérée avec UpdateMany (pattern déjà utilisé dans ci_service.rs::cancel_running_pipelines) :
#![allow(unused)]
fn main() {
import_job::Entity::update_many()
.col_expr(Column::ReceivedObjects, Expr::value(received as i32))
.col_expr(Column::TotalObjects, Expr::value(total as i32))
.col_expr(Column::ReceivedBytes, Expr::value(bytes as i64))
.col_expr(Column::Phase, Expr::value(phase.as_str()))
.col_expr(Column::UpdatedAt, Expr::value(Utc::now()))
.filter(Column::Id.eq(job_id))
.exec(db)
.await?;
}
Même principe pour mark_running, mark_success, mark_failed, mark_cancelled → toutes divisent leurs aller-retours DB par 2.
4.2 Downsampler la progression dans le worker (dédié)
Plan alternatif : le callback écrit simplement dans un tokio::sync::watch<TransferStats> (en mémoire, zéro DB). Une tâche dédiée consomme ce channel et UPDATE la DB à 1 Hz seulement.
flowchart LR G[git2 callback] -->|watch::send<br/>très haute fréquence| W[watch channel] W --> T[Tâche update<br/>ticker 1 Hz] T -->|1 UPDATE / s| DB
Gains :
- 1 seul émetteur DB au lieu de N tokio::spawn concurrents → zéro contention sur le pool
- Fréquence déterministe (1 Hz) indépendante de la vitesse réseau
- Code plus clair (la tâche est async, on peut
awaitsans détour)
4.3 Supprimer le RETURNING implicite de SeaORM
ActiveModel::update(db) renvoie le modèle complet via ... RETURNING *. Pour les progress updates, on n’utilise pas le retour. UpdateMany::exec n’émet pas de RETURNING → moins de bytes sur le wire, moins de désérialisation.
4.4 Pool DB dédié pour le worker
Aujourd’hui le worker partage le pool principal avec les handlers HTTP. Un pool de 4-8 connexions dédié au worker éviterait qu’un import sature le pool et fasse timeouter les requêtes utilisateur.
4.5 Debouncer le SSE côté serveur
Le SSE SELECT toutes les 2 s même quand rien ne change. Alternative : PostgreSQL LISTEN/NOTIFY sur une chaîne import_job_{uuid}. Chaque UPDATE émet un NOTIFY, la connexion SSE fait LISTEN → push direct, zéro polling.
Coût : nécessite une connexion DB dédiée par SSE (LISTEN est stateful). Compromis à évaluer.
5. Optimisation majeure : shell-out vers git clone --bare
Indépendant des optimisations DB mais de loin le plus gros gain. libgit2 est structurellement 2-5x plus lent que git CLI sur gros clones HTTPS (pas de multiplexage, résolution de deltas mono-threadée, etc.).
flowchart TB
subgraph Actuel[Flux actuel — libgit2]
A1[RepoBuilder::clone] --> A2[libgit2: fetch + resolve]
A2 -->|lent 2-5x| A3[bare repo]
end
subgraph Cible[Flux optimisé — git CLI]
B1[Command::new git] --> B2[git clone --bare --progress]
B2 -->|stderr| B3[Parser regex<br/>Receiving objects: X%]
B3 --> B4[update watch channel]
B2 -->|vitesse native| B5[bare repo]
end
Avantages :
- Vitesse native (parité avec
gitCLI) - Progression native via stderr
Receiving objects: 42% (368/876), 1.10 MiB | 2.05 MiB/s - HEAD correctement positionné (plus besoin du workaround
RepoBuilder) - Moins de dépendance sur libgit2 pour le cas du clone initial
Contrepartie : binaire git requis sur le serveur (déjà le cas — utilisé par SbomService::git_archive).
6. Priorités recommandées
Si on voulait optimiser maintenant, ordre proposé :
- Shell-out
git clone --bare— gain réel pour l’utilisateur (clone 3-5x plus rapide). update_manypourupdate_progress— divise la pression DB par 2, changement mécanique de ~20 lignes.watchchannel + tâche 1 Hz — architecture plus propre, élimine les problèmes de pool.- Pool DB dédié worker — filet de sécurité opérationnel.
- LISTEN/NOTIFY pour SSE — seulement si beaucoup de clients simultanés.
Les gains 2-4 sont utiles même avec libgit2 ; le gain 1 est le plus visible pour l’utilisateur final.
Contribuer à rustwarden
Ce document décrit les interactions entre gitrust et le framework rustwarden-core, et explique comment étendre le framework côté gitrust sans modifier ses sources.
Services framework utilisés
| Service | Usage dans gitrust |
|---|---|
UserService | CRUD users, roles, delete_user, admin_reset_password, change_password_direct, unassign_role_from_user |
RoleService | Liste/création/suppression roles, get_role_by_name |
PasswordService | hash_password (async), verify_password (async), generate_secure_password |
ResourceService | register/unregister repos, set_public, effective_permission, find_by_type_and_id |
AuthService | authenticate_user (login) |
JwtService | generate_token (login) |
EmailValidationService | Vérification email |
PasswordResetService | Flux “mot de passe oublié” |
AppSettingsService | get_bool pour registration_enabled |
Hook on_user_deleted implémenté
Le hook on_user_deleted dans gitrust-hooks supprime le répertoire utilisateur sur le filesystem (repos_base_path/username/) avant la cascade DB. Appelé automatiquement par UserService::delete_user.
Règle fondamentale
Ne jamais modifier crates/rustwarden-core/. Ce crate est le framework partagé. Toute extension passe par :
- Des wrappers côté
gitrust-core(services qui délèguent au framework) - Des trait impls (
RustwardenHooksdansgitrust-hooks) - Des extensions via les mécanismes d’injection Axum (
Extension,State)
Aucun contournement en cours
Tous les anciens contournements (entités directes, hash manuels) ont été remplacés par les services du framework suite à la mise à jour du 2026-03-26.
Ajouter une fonctionnalité nécessitant un service framework absent
Si un service nécessaire n’existe pas dans rustwarden-core :
- Implémenter le service côté
gitrust-coreen réutilisant les primitives du framework (ex:PasswordService::hash_passwordpour le hashage) - Si la fonctionnalité est générique et bénéficierait au framework lui-même, ouvrir une issue ou une PR upstream sur le dépôt rustwarden-core
- Ne pas dupliquer la logique d’auth, de session, ou de JWT — ces domaines appartiennent exclusivement au framework
Pattern d’extension via hooks
Les hooks (gitrust-hooks/src/lib.rs) implémentent RustwardenHooks et sont injectés dans le routeur Axum au démarrage :
#![allow(unused)]
fn main() {
// Dans main.rs
let hooks = Arc::new(GitrustHooks::new(config.clone()));
// Passé via .merge_routes(app_routes(hooks))
// Puis disponible via axum::Extension dans les handlers
}
Pour réagir à un nouvel événement du framework, ajouter la méthode correspondante dans l’impl RustwardenHooks de gitrust-hooks, sans toucher au code du framework qui émet l’événement.
Passer la QA avant un merge
Ce guide liste dans l’ordre exact toutes les vérifications à effectuer avant d’ouvrir ou de merger une pull request dans gitrust.
Pourquoi cette checklist existe
Gitrust vise la conformité ANSSI PA-074. Chaque gate protège contre une catégorie spécifique de régressions : qualité de code, sécurité mémoire, vulnérabilités de dépendances, ou comportement utilisateur. Aucun gate ne peut être ignoré sauf exception documentée dans la PR.
Ordre d’exécution local
Exécutez les gates dans cet ordre — un gate qui échoue bloque les suivants.
Gate 1 — Formatage
cargo fmt --all -- --check
En cas de diff : appliquez le format, puis relancez --check.
cargo fmt --all
cargo fmt --all -- --check # doit maintenant retourner 0
Sortie attendue (succès) :
(aucune sortie — code de retour 0)
Gate 2 — Linting (zéro warning)
cargo clippy --workspace -- -D warnings
Sortie attendue :
Finished `dev` profile [unoptimized + debuginfo] target(s) in Xs
Aucun warning: ne doit apparaître. Les warnings sont traités comme des erreurs (-D warnings).
Lints Rust obligatoires (déjà configurés dans lib.rs de chaque crate) :
#![allow(unused)]
#![forbid(unsafe_code)] // crates core, web, hooks
#![deny(clippy::unwrap_used)]
#![deny(clippy::expect_used)]
#![deny(clippy::panic)]
#![deny(clippy::indexing_slicing)]
#![deny(clippy::mem_forget)]
fn main() {
}
Gate 3 — Tests unitaires et d’intégration
cargo test --workspace
Sortie attendue :
test result: ok. XXX passed; 0 failed; 0 ignored; 0 measured
Règles de test :
- Tests sur vraie DB (pas de mocks pour la couche persistance).
- Tests négatifs obligatoires : pour chaque chemin heureux, un test couvre l’accès non authentifié, l’input invalide, et la ressource inexistante.
- Pas de
sleep()dans les tests — utilisezretry/pollavec timeout. - Chaque test crée ses propres données (pas de dépendance sur l’ordre d’exécution).
Gate 4 — Tests E2E Playwright (si UI modifiée)
À exécuter si vous avez modifié des templates, routes SSR, ou le comportement des pages :
npm run test:e2e
Les tests E2E se trouvent dans tests/e2e/. Sortie attendue :
XX passed (Xs)
En cas d’échec : consultez tests/e2e/reports/ pour les captures d’écran et traces.
Gate 5 — Rebuild CSS (si templates modifiés)
À exécuter si vous avez modifié un fichier dans templates/ ou ajouté des classes Tailwind :
npx tailwindcss -i static/css/input.css -o static/css/style.css --minify
Commitez le fichier static/css/style.css généré dans votre PR.
Gate 6 — Audit des dépendances
cargo audit
Sortie attendue :
Fetching advisory database from `https://github.com/RustSec/advisory-db.git`
Scanning Cargo.lock for vulnerabilities (X crates)
0 vulnerabilities found
En cas de vulnérabilité : mettez à jour la dépendance concernée ou ajoutez une exemption documentée dans deny.toml.
Gate 7 — Licences et dépendances bannies
cargo deny check
Sortie attendue :
Checking advisories
Checking bans
Checking licenses
Checking sources
Aucune violation ne doit apparaître. Gitrust interdit les licences GPL et AGPL dans les dépendances de production.
Checklist sécurité (features touchant auth/secrets/permissions)
Pour tout changement touchant l’authentification, les tokens, les permissions ou les secrets :
- Secrets jamais en clair en base — hash SHA-256 ou bcrypt systématique.
- Secrets jamais loggés — vérifiez
tracing::info/debug,DisplayetDebugimpls. - Comparaison en temps constant —
subtle::ConstantTimeEqpour la validation de secrets (pas de==). - Rate limiting présent sur les endpoints d’authentification.
- Token CSRF sur tous les formulaires
POST/PUT/DELETE. - Vérification d’ownership côté serveur (anti-IDOR) —
user_idvérifié sur chaque ressource accédée. - Expiration obligatoire sur tout token ou session.
- Zeroize — les types contenant des secrets implémentent
Zeroize/ZeroizeOnDrop. - Path traversal — validation des chemins disque (pas de
..,/,\dans les slugs).
Checklist pre-merge finale
- Gate 1 :
cargo fmt --all -- --check→ 0 diff - Gate 2 :
cargo clippy --workspace -- -D warnings→ 0 warning - Gate 3 :
cargo test --workspace→ 0 échec - Gate 4 :
npm run test:e2e→ 0 échec (si UI modifiée) - Gate 5 : CSS rebuild commité (si templates modifiés)
- Gate 6 :
cargo audit→ 0 vulnérabilité - Gate 7 :
cargo deny check→ 0 violation - Aucun secret en clair dans le diff (
git diff --staged | grep -i "password\|secret\|token\|key") - Aucune modification dans
crates/rustwarden-core/(framework read-only) - Checklist sécurité §ci-dessus validée (si applicable)
Vérification des secrets dans le diff
Avant tout commit, scannez votre diff :
git diff --staged | grep -iE "(password|secret|token|api_key|private_key)\s*=" | grep -v "test\|example\|dummy\|placeholder"
Si la commande retourne des lignes, examinez-les une par une pour confirmer qu’il ne s’agit pas de vraies valeurs sensibles.
Voir aussi
Référence
La référence est une documentation exhaustive et précise. Elle décrit le comportement exact des API, des schémas, et des services. Consultez-la quand vous avez besoin de savoir ce qu’une fonction accepte, ce qu’une table contient, ou ce qu’un endpoint retourne.
Diátaxis — mode information : la référence ne vous apprend pas — elle vous informe. Chaque entrée est complète, précise, et sans contexte superflu.
Documents de référence
Services API interne
Signatures complètes de tous les services Rust disponibles dans gitrust-core et rustwarden-core : AuthService, UserService, RepositoryService, IssueService, LabelService, TotpService, ResourceService, et plus.
Schéma base de données
ERD Mermaid complet, définition de chaque table avec types Rust/SeaORM, contraintes, index, et règles de clé étrangère. Ordre des migrations inclus.
API REST v1
Tous les endpoints /api/v1/ : authentification, utilisateurs, dépôts, issues, pull requests, pipelines CI. Format requête/réponse JSON, codes d’erreur, pagination, rate limiting, exemples curl.
Hooks git post-receive
Implémentation du trait RustwardenHooks dans gitrust. Événements on_user_registered, on_user_deleted, on_resource_shared, on_resource_unshared avec leurs effets de bord. Séquence receive-pack, dispatch webhook/CI, tests locaux.
API rustwarden-core
Extracteurs Axum (AuthUser, OptionalUser, AuthenticatedSession), middleware JWT/CSRF/rate-limit, JwtService, trait RustwardenHooks, i18n, EmailQueueService. Règles d’extension du framework.
Conventions de lecture
- Les signatures de méthode suivent la syntaxe Rust :
fn nom(arg: Type) -> Result<ReturnType, Error>. - Les types
Option<T>indiquent un champ nullable en base ou un paramètre optionnel. - Les colonnes marquées
PKsont des clés primaires UUID générées côté base (gen_random_uuid()). TIMESTAMPTZ= timestamp avec fuseau horaire, stocké en UTC.
Navigation croisée
- Vous cherchez comment utiliser un service dans votre code ? → Guides pratiques
- Vous voulez comprendre pourquoi ces services sont structurés ainsi ? → Explications
Architecture par crates
Référence structurelle de gitrust : crates, modules, routes, dépendances et composants CI.
Dépendances entre crates
graph LR
RW["rustwarden-core"]
Git["gitrust-git"]
Core["gitrust-core"]
Hooks["gitrust-hooks"]
SSH["gitrust-ssh"]
SshGuard["gitrust-ssh-guard"]
Web["gitrust-web"]
Bin["gitrust (binaire)"]
Core --> RW
Git -.->|"indépendant<br/>(git2, tokio)"| RW
Hooks --> Core
Hooks --> RW
SSH --> Core
SSH --> Git
SSH --> SshGuard
SshGuard -.->|"SeaORM bans/ACL/events"| Core
Web --> Core
Web --> Git
Web -.->|"CI pages"| Git
Web -.->|"admin ACL/ban via Extension"| SshGuard
Bin --> Core
Bin --> Git
Bin --> SSH
Bin --> SshGuard
Bin --> Web
Bin --> Hooks
Bin --> RW
subgraph "Externes CI (Phase 5b)"
Dagger["Dagger CLI"]
CiEng["ci-engine (Python)"]
Syft["Syft"]
DTrack["Dependency-Track"]
end
Core -.->|"CiWorker<br/>sous-processus"| Dagger
Dagger -.-> CiEng
Core -.-> Syft
Syft -.-> DTrack
Crates — Responsabilités
gitrust-core
Cœur métier. Ne dépend que de rustwarden-core et des crates Rust standards.
| Module | Rôle |
|---|---|
config | GitrustConfig::from_env() — ports SSH, chemin repos, limites |
error | GitrustError — erreurs domaine Git + IntoResponse HTTP |
roles | enum Role { Reader, Developer, Maintainer, Owner } avec Ord |
types | Newtypes validés : RepoSlug, TeamSlug, Fingerprint, TokenHash |
models/ | Entités SeaORM : repository, ssh_key, team, team_member, team_repository_access, ci_pipeline, ci_job, ci_log, ci_config, ci_variable |
migrations/ | Migrations SeaORM (tables gitrust) combinées avec les migrations core |
services/ | SshKeyService, RepositoryService, TeamService, AccessService, CiService, CiVariableService, CiDetectionService, CiWorker, NotificationService |
dto/ | Structures d’entrée/sortie pour les services |
gitrust-git
Opérations Git sur le système de fichiers. Utilise git2 (libgit2). Pas de dépendance vers la base de données.
| Module | Rôle |
|---|---|
errors | enum GitError (Git, RepoNotFound, RefNotFound, PathTraversal, …) |
bare_repo | Init, open, delete, exists + validation chemin canonique sous base |
branch / tag | Listing branches (BranchInfo) et tags (TagInfo, annotated/lightweight) |
reference | resolve_ref (branch -> tag -> SHA -> revparse) |
tree_browser | list_tree (dirs first, sorted), TreeEntry, EntryKind |
blob_reader | read_blob -> Text/Binary, détection binaire (null bytes) |
commit_log | list_commits (paginé), find_commit, CommitInfo |
readme | find_readme (README.md > README > readme.md > …) |
pack_protocol | advertise_refs, serve_pack (async, git-upload-pack/git-receive-pack) |
gitrust-ssh
Serveur SSH basé sur russh. Authentification par clé publique.
| Module (prévu) | Rôle |
|---|---|
server | Démarrage, génération clé hôte Ed25519, wrapper du TcpListener par SecureListener (ssh-guard) |
auth | Authentification par fingerprint -> SshKeyService, appel à AuthTracker.record_auth_attempt après chaque tentative |
session | Handler SSH (exec, shell), porte ClientIdentity du listener jusqu’aux décisions auth |
command_handler | Parsing git-upload-pack/git-receive-pack |
gitrust-ssh-guard
Couche de durcissement SSH intercalée entre le TcpListener et russh. Voir la page dédiée Crate gitrust-ssh-guard pour le détail des modules et l’API publique. Synthèse :
| Module | Rôle |
|---|---|
config | GuardConfig::from_env() — lecture SSH_GUARD_*, sélection DeploymentProfile, validation |
runtime | GuardHandles::build(db) — assemblage partageable entre serveur SSH et routeur admin |
listener | SecureListener wrappe TcpListener : extraction IP réelle (PROXY v1/v2), ACL, flood, retour AcceptOutcome |
proxy | Parseur PROXY protocol v1/v2 avec timeout |
identity | ClientIdentity — IP + session_id + user/fingerprint enrichis pendant l’auth |
tracker | AuthTracker — persiste, émet, dispatche aux détecteurs |
ban | BanManager + EffectiveStatus — priorité deny > auto_ban > allow > default |
events | GuardEvent — schéma JSON stable (tag = “event”) pour fail2ban / SIEM |
sinks | TracingSink, FileSink, MultiSink — destination des événements |
detector/{brute_force,user_enumeration,key_scanning,connection_flood} | 4 motifs d’attaque, push-based |
store/{memory,postgres,hybrid} | Backend bans/ACL/events — hybrid (RAM + write-through) par défaut |
gitrust-web
Interface web SSR (Server-Side Rendering) avec Askama (templates compilés) + HTMX (interactions dynamiques). Fonctionne en mode headless() : les pages UI du framework (rustwarden-ui) ne sont pas montées, gitrust-web réimplémente toutes les pages avec un design spécifique (sidebar contextuelle, navigation Git, DaisyUI).
Architecture des routes :
gitrust (port 4000)
├── /api/v1/auth/* ← Routes API du framework (JSON)
│ ├── POST /login AuthService::authenticate_user
│ ├── POST /register UserService::create_user
│ ├── POST /refresh RefreshTokenService (rotation JWT)
│ ├── POST /logout JwtBlacklistService
│ ├── GET /me Claims extraction
│ ├── POST /forgot-password PasswordResetService::request_reset
│ ├── POST /reset-password PasswordResetService::reset_password
│ ├── GET /verify-email/{t} EmailValidationService::verify_email
│ └── POST /resend-verif EmailValidationService::resend
│
├── /api/v1/* ← API REST Gitrust (JSON, auth JWT/PAT)
│ ├── /user, /user/repos Profil utilisateur
│ ├── /repos/{o}/{r} Détail dépôt, branches, tags, commits
│ ├── /repos/{o}/{r}/issues CRUD issues + commentaires
│ ├── /repos/{o}/{r}/pulls CRUD PRs + merge
│ └── /repos/{o}/{r}/ci/* Pipelines CI (list, trigger, cancel, logs)
│
├── /* ← Pages SSR gitrust (HTML)
│ ├── /login, /register Pages formulaires (design gitrust)
│ ├── /dashboard, /teams Pages utilisateur
│ ├── /settings/* Profile, SSH keys, sessions, notifications
│ ├── /notifications Liste notifications + SSE stream
│ ├── /admin/* Administration (+ admin CI)
│ ├── /{owner}/{repo}/* Navigation dépôt Git
│ └── /{owner}/{repo}/ci/* Pages CI (pipelines, logs, config, variables)
│
└── /static/* ← Assets locaux (CSS, JS, HTMX)
Les routes API du framework sont montées via framework_api_routes() dans routes.rs. Elles sont utilisées par :
- Les liens dans les emails (vérification, reset password)
- Le refresh token (rotation JWT, session longue durée)
- L’accès programmatique (GET /me, POST /logout)
Les pages SSR et les routes API coexistent : les formulaires HTML soumettent aux handlers gitrust (SSR), tandis que les services framework génèrent des liens vers les routes API.
| Module | Rôle |
|---|---|
routes.rs | Routeur principal : framework API + API REST Gitrust + pages SSR + fichiers statiques |
handlers/ | Handlers Axum SSR (un fichier par domaine) + handlers API JSON |
templates.rs | Structs Askama + SidebarContext + RepoNav |
helpers.rs | require_auth, require_admin, sidebar_*, resolve_repo_access |
templates/ | Templates HTML (Askama, extends base.html) |
static_files.rs | ServeDir pour /static |
gitrust-hooks
Implémentation de RustwardenHooks pour réagir aux événements du framework. Les hooks sont injectés dans le routeur Axum via Arc<dyn RustwardenHooks> + axum::Extension (dans main.rs).
| Hook | Action |
|---|---|
on_user_registered | Crée le répertoire utilisateur sur le FS (anti path-traversal) |
on_user_deleted | Supprime le répertoire utilisateur + bare repos sur le FS |
on_resource_shared | Audit log si resource_type = “repository” |
on_resource_unshared | Audit log si resource_type = “repository” |
Notifications
Système de notification à deux canaux : in-app (SSE temps réel + page) et email (worker SMTP du framework).
Événement (pipeline échoue, PR commentée, etc.)
│
▼
NotificationService::notify(user_id, event_type, title, body, link)
│
├── INSERT INTO notifications (in-app)
│ → broadcast::Sender → SSE /notifications/stream
│ → Badge header mis à jour en temps réel (HTMX)
│
└── if user.email_on_{event} = true:
EmailQueueService::enqueue() (framework SMTP worker)
Les préférences de notification sont par utilisateur (email on/off par type d’événement).
CI/CD — Modes et composants
Système CI hybride à deux niveaux, activé via CI_ENABLED=true.
Modes
| Mode | Détection | Exécution |
|---|---|---|
| Easy | .gitrust-ci.yml à la racine du repo | Module Dagger Python générique (ci-engine) interprète le YAML |
| Power | .dagger/ à la racine du repo | dagger call -m .dagger/ exécute le module utilisateur directement |
| Aucun | Ni l’un ni l’autre | Pas de CI pour ce repo |
Architecture CI
git push (HTTP ou SSH)
│
▼
receive-pack handler
│
├── CiDetectionService::detect(bare_repo, commit_sha)
│ → Easy / Power / None
│
├── CiService::create_pipeline(repo_id, sha, mode)
│
└── mpsc::Sender.send(job) ──► CiWorker (tokio::spawn)
│
├── 1. Validation statique
│ Easy: schéma YAML, whitelist images/packages
│ Power: analyse code .dagger/ (si activé)
│
├── 2. SBOM Gate (si CI_SBOM_ENABLED)
│ syft scan → sbom.cdx.json
│ → Dependency-Track (si CI_DTRACK_ENABLED)
│ → PASS/FAIL selon politique
│
├── 3. Résolution variables (héritage team → repo)
│ CiVariableService::resolve_for_pipeline()
│
├── 4. Exécution Dagger (isolé, timeout, cgroups)
│ Easy: dagger call -m ci-engine test --profile=...
│ Power: dagger call -m .dagger/ <function>
│
└── 5. Rapport + audit
Status, logs (broadcast SSE), ci_audit_log
Composants externes (tous optionnels sauf Dagger)
| Composant | Rôle | Variable de contrôle |
|---|---|---|
| Dagger Engine | Exécution des pipelines dans des containers isolés | CI_ENABLED |
| ci-engine (Python) | Moteur générique pour Easy Mode | CI_DAGGER_MODULE_PATH |
| Syft | Génération SBOM CycloneDX | CI_SBOM_ENABLED |
| Dependency-Track | Analyse de vulnérabilités sur les SBOM | CI_DTRACK_ENABLED |
Variables et secrets CI
Héritage team → repo : les variables définies au niveau team sont partagées entre tous les repos de l’équipe. Le repo peut surcharger une clé team avec sa propre valeur. Les secrets sont chiffrés en base (AES-256-GCM) et masqués dans les logs (***).
Sécurité (ANSSI PA-074)
| Directive | Scope |
|---|---|
#![forbid(unsafe_code)] | gitrust-core, gitrust-web, gitrust-hooks, gitrust-ssh-guard |
#![deny(unsafe_code)] | gitrust-git, gitrust-ssh (FFI libgit2/russh) |
deny(clippy::unwrap_used, expect_used, panic) | Tous les crates |
deny(clippy::indexing_slicing) | Tous les crates |
deny(clippy::mem_forget) | Tous les crates |
Zeroize on drop | TokenHash (ANSSI R23) |
| RSA minimum 4096 bits | SshKeyService |
| Path traversal validation | RepositoryService, gitrust-git |
| Slug validation + noms réservés | RepoSlug, TeamSlug |
Crate gitrust-ssh-guard
Référence structurelle de la crate de durcissement SSH. Pour la motivation et le rationale de design, voir Conception de ssh-guard. Pour l’exploitation côté admin, voir Configurer ssh-guard.
1. Position dans l’arborescence
crates/
├── gitrust-ssh-guard/
│ ├── Cargo.toml
│ └── src/
│ ├── lib.rs (re-exports publics)
│ ├── config.rs (SSH_GUARD_*, profils, validation)
│ ├── runtime.rs (GuardHandles : assemblage)
│ ├── listener.rs (SecureListener, AcceptOutcome)
│ ├── proxy.rs (parseur PROXY protocol v1/v2)
│ ├── identity.rs (ClientIdentity)
│ ├── tracker.rs (AuthTracker, dispatch détecteurs)
│ ├── ban.rs (BanManager, EffectiveStatus)
│ ├── events.rs (GuardEvent, GuardEventSink)
│ ├── sinks.rs (TracingSink, FileSink, MultiSink)
│ ├── detector/
│ │ ├── mod.rs
│ │ ├── brute_force.rs
│ │ ├── user_enumeration.rs
│ │ ├── key_scanning.rs
│ │ └── connection_flood.rs
│ └── store/
│ ├── mod.rs (trait GuardStore)
│ ├── entities.rs (modèles SeaORM)
│ ├── memory.rs (MemoryStore — DashMap)
│ ├── postgres.rs (PostgresStore — write-through)
│ └── hybrid.rs (HybridStore — RAM chaude + DB)
2. Tableau des modules
| Module | Rôle | Types principaux |
|---|---|---|
config | Lecture des env vars SSH_GUARD_*, sélection du profil de déploiement, validation de cohérence (ex. : un profil proxifié sans trusted_proxies est rejeté au démarrage). | GuardConfig, DeploymentProfile, ProxyProtocol, StoreBackend, LogFormat, LogTarget, DetectorThreshold, ConfigError |
runtime | Assemble tous les composants à partir de GuardConfig + connexion DatabaseConnection. Le binaire en construit une seule instance et la partage entre le serveur SSH et le routeur Axum admin. | GuardHandles, BuildError |
listener | Wrappe un tokio::net::TcpListener : extraction d’IP réelle (PROXY ou peer_addr), consultation de l’ACL/ban, rate-limit flood, puis remise du TcpStream au handler russh. | SecureListener, AcceptOutcome, AcceptError, ProxyListenerConfig |
proxy | Parseur PROXY protocol v1 (texte HAProxy legacy) et v2 (binaire nginx stream / HAProxy moderne) avec timeout dédié. | parse_header, ParsedHeader, ProxyError |
identity | Identité d’un client SSH au fil de la session : IP, session_id (UUID), nom d’utilisateur et fingerprint de clé renseignés au moment de l’auth. | ClientIdentity |
tracker | Reçoit les événements d’auth émis par gitrust-ssh, les persiste dans le store, les diffuse au sink, puis appelle on_event sur chaque détecteur actif. | AuthTracker, AuthOutcome |
ban | Compose ACL admin (allow/deny) et bans actifs en un statut effectif. Pose les bans auto (TTL) et manuels (CIDR), gère le unban. | BanManager, EffectiveStatus |
events | Définition stable du schéma JSON émis. Toute évolution incompatible doit créer un nouveau variant plutôt que renommer un champ existant. | GuardEvent, GuardEventSink, AuthMethod, BanReason, DropReason |
sinks | Implémentations de GuardEventSink : tracing (stderr / journald), fichier en append (ligne JSON), fan-out vers plusieurs sinks. | TracingSink, FileSink, MultiSink, build_sink |
detector::brute_force | Compte les AuthFailed par IP dans la fenêtre, déclenche auto_ban(BruteForce) au seuil. | BruteForceDetector |
detector::user_enumeration | Compte les usernames distincts essayés par IP dans la fenêtre. | UserEnumerationDetector |
detector::key_scanning | Compte les fingerprints de clé distincts essayés par IP dans la fenêtre. | KeyScanningDetector |
detector::connection_flood | Token bucket GCRA (governor) keyé par IP. Pas de ban persistant : drop immédiat au listener. | ConnectionFloodDetector |
store | Trait abstrait pour bans, ACL et événements d’auth. Trois implémentations interchangeables. | GuardStore, MemoryStore, PostgresStore, HybridStore, BanEntry, AclEntry, AclKind, RehydrateStats, StoreError |
3. Surface API publique (lib.rs)
#![allow(unused)]
fn main() {
// Assemblage et runtime
pub use runtime::{BuildError, GuardHandles};
// Configuration
pub use config::{
ConfigError, DeploymentProfile, DetectorThreshold, GuardConfig,
LogFormat, LogTarget, ProxyProtocol, StoreBackend,
};
// Listener
pub use listener::{AcceptError, AcceptOutcome, ProxyListenerConfig, SecureListener};
// Identité et tracking
pub use identity::ClientIdentity;
pub use tracker::{AuthOutcome, AuthTracker};
// Décision de ban
pub use ban::{BanManager, EffectiveStatus};
// Détecteurs
pub use detector::{
BruteForceDetector, ConnectionFloodDetector,
KeyScanningDetector, UserEnumerationDetector,
};
// Événements et sinks
pub use events::{AuthMethod, BanReason, DropReason, GuardEvent, GuardEventSink};
pub use sinks::{build_sink, FileSink, MultiSink, TracingSink};
// Stockage
pub use store::{
AclEntry, AclKind, BanEntry, GuardStore, HybridStore,
MemoryStore, PostgresStore, RehydrateStats, StoreError,
};
}
Lints actifs sur la crate (alignés ANSSI PA-074) :
#![allow(unused)]
#![forbid(unsafe_code)]
#![deny(
fn main() {
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing,
clippy::mem_forget
)]
}
4. GuardHandles — point d’entrée du runtime
GuardHandles est l’objet construit une fois au démarrage du binaire, puis cloné (tous les champs sont Arc<…>) là où il est nécessaire :
- vers
gitrust_ssh::server::start_serverpour queSecureListeneretAuthTrackerpartagent les mêmesBanManager/GuardStore; - vers le routeur Axum via
axum::Extension, pour que les actions admin (ajouter une IP en denylist, lever un ban) soient immédiatement visibles par le listener — sans attendre unrehydrateau prochain redémarrage.
#![allow(unused)]
fn main() {
#[derive(Clone)]
pub struct GuardHandles {
pub config: GuardConfig,
pub store: Arc<dyn GuardStore>,
pub sink: Arc<dyn GuardEventSink>,
pub ban_manager: Arc<BanManager>,
pub tracker: Arc<AuthTracker>,
pub flood: Option<Arc<ConnectionFloodDetector>>,
pub proxy_config: Option<ProxyListenerConfig>,
}
impl GuardHandles {
pub async fn build(db: DatabaseConnection) -> Result<Self, BuildError>;
}
}
build enchaîne :
GuardConfig::from_env()(lecture + validation).build_sink(&config)selonLogTarget.- Construction du store selon
SSH_GUARD_STORE_BACKEND:memory→MemoryStore(rien en DB) ;postgres→PostgresStore(chaque écriture en DB) ;hybrid→HybridStore(RAM chaude + flush write-through périodique +rehydrateau boot pour repeupler la mémoire depuis la DB).
- Instanciation du
BanManager. - Instanciation des détecteurs (chaque détecteur dont le seuil est désactivé —
u32::MAX— n’est pas instancié). - Construction de l’
AuthTrackeravec les détecteurs actifs. - Construction de
ProxyListenerConfigsiproxy_protocol != Disabled.
5. SecureListener — séquence d’accept
#![allow(unused)]
fn main() {
pub enum AcceptOutcome {
Accepted { stream: TcpStream, identity: ClientIdentity },
Dropped { ip: IpAddr, reason: DropReason },
}
impl SecureListener {
pub async fn accept(&self) -> Result<AcceptOutcome, AcceptError>;
}
}
Étapes appliquées à chaque connexion entrante :
tcp.accept()→(stream, peer).resolve_real_ip(stream, peer.ip()):- profil direct (pas de
proxy_config) → IP =peer.ip(); - profil proxifié → vérifie que
peer.ip()∈trusted_proxies, puis parse l’en-tête PROXY pour extraire l’IP cliente réelle ; - en mode
strict, un parsing en échec retourneErr(DropReason::ProxyHeaderInvalid|Missing|UntrustedProxy); en mode souple, fallback surpeer.ip()avec warn.
- profil direct (pas de
ban_manager.effective_status(ip):DeniedByAcl→Dropped { reason: Banned };BannedAuto(_)→Dropped { reason: Banned };AllowListed→Acceptedimmédiat (bypass flood) ;Normal→ étape 4.
- Si
floodactif :flood.check(ip). Sur false →Dropped { reason: FloodLimit }(les events sont déjà émis parflood.check). - Sinon : construction du
ClientIdentity, émission deConnectionAccepted, retourAccepted.
Toute erreur du store est traitée en fail-open (warn + on laisse passer), pour ne pas couper le service quand PostgreSQL est indisponible.
6. BanManager — priorités d’ACL
#![allow(unused)]
fn main() {
pub enum EffectiveStatus {
Normal,
AllowListed,
DeniedByAcl,
BannedAuto(Box<BanEntry>),
}
}
Priorité de résolution dans effective_status(ip) :
acl_match(ip) == Some(Deny)→DeniedByAcl.bans_covering(ip)non vide →BannedAuto(ban).acl_match(ip) == Some(Allow)→AllowListed.- Sinon →
Normal.
Un appel auto_ban(ip, reason) est no-op dans trois cas :
dry_runactif (un événementIpBannedest tout de même émis pour fail2ban / observabilité) ;- l’IP est allowlistée ;
- un ban auto actif couvre déjà l’IP (évite le spam d’événements
IpBanned).
Un manual_ban(cidr, reason, ttl) accepte un CIDR (pas seulement un /32) et est permanent si ttl=None. unban(ban_id, by) retire le ban et émet IpUnbanned.
7. AuthTracker — pivot de la chaîne d’événements
#![allow(unused)]
fn main() {
pub enum AuthOutcome { Success, Failure }
impl AuthTracker {
pub async fn record_auth_attempt(
&self,
identity: &ClientIdentity,
method: AuthMethod,
outcome: AuthOutcome,
);
}
}
Pour chaque appel :
- Construit
GuardEvent::AuthSucceededouAuthFailedselonoutcome. store.record_event(&event)— alimente la table consultée par les détecteurs.sink.emit(&event)— alimente fail2ban / observabilité.- Pour chaque détecteur configuré :
detector.on_event(&event).await.
Côté gitrust-ssh, le handler russh appelle record_auth_attempt après chaque tentative d’authentification (clé publique, password, keyboard-interactive).
8. Schéma JSON des événements
Tous les GuardEvent se sérialisent avec #[serde(tag = "event", rename_all = "snake_case")]. La forme stable est :
{"event":"<nom_variant>","ts":"...","ip":"...", ...}
Pour le détail des champs par variant et leur usage côté admin (fail2ban, SIEM), voir Événements ssh-guard (JSON).
Convention de stabilité :
- ajouter un nouveau variant n’est pas un breaking change pour les consommateurs ;
- ajouter un champ optionnel à un variant existant ne l’est pas non plus ;
- renommer un champ ou un variant est un breaking change et nécessite un nouveau nom.
9. Backends de stockage (GuardStore)
| Backend | Vie des bans/ACL | Latence lecture | Cas d’usage |
|---|---|---|---|
MemoryStore | Perdues au restart | DashMap, lock-free | Tests, profil private, dev |
PostgresStore | Persistantes | Round-trip DB par lecture | Audits stricts, instances multi-noeuds (sans cache RAM) |
HybridStore | RAM chaude + write-through DB, rehydrate au boot | DashMap | Défaut recommandé : performance + persistance |
Le trait GuardStore expose : acl_match, bans_covering, is_banned, insert_ban, unban, list_active_bans, insert_acl, record_event, count_auth_failures, count_distinct_users, count_distinct_keys.
10. Intégration côté gitrust-ssh
Pseudo-code condensé du wiring effectif (crates/gitrust-ssh/src/server.rs) :
#![allow(unused)]
fn main() {
let secure = SecureListener::new_with_proxy(
tcp_listener,
guard.ban_manager.clone(),
guard.flood.clone(),
guard.sink.clone(),
guard.proxy_config.clone(),
);
loop {
match secure.accept().await? {
AcceptOutcome::Accepted { stream, identity } => {
let session = GitSession::new_with_guard(
db.clone(), ssh_config.clone(), ci_tx.clone(),
identity, guard.tracker.clone(),
);
tokio::spawn(async move { russh::server::run_stream(/* ... */).await });
}
AcceptOutcome::Dropped { .. } => {
// L'événement ConnectionDropped a déjà été émis par le listener.
}
}
}
}
Côté GitSession (handler russh) : à chaque tentative d’auth, l’identité est enrichie (set_username, set_fingerprint) puis tracker.record_auth_attempt(&identity, method, outcome) est appelé.
11. Tests
Toute la crate utilise des tests unitaires #[tokio::test] avec un MemoryStore et un RecorderSink qui capture les GuardEvent. Ordre de grandeur : 80+ tests couvrant chaque détecteur, la priorité d’ACL, le mode dry_run, l’idempotence du ban, l’expiration TTL, les cas d’erreur du parseur PROXY, la séparation des budgets de flood par IP.
Les tests d’intégration de bout en bout (PROXY v2 binaire émis par un client de test, déclenchement réel du seuil brute-force depuis gitrust-ssh) vivent dans crates/gitrust-ssh/tests/.
12. Voir aussi
- Conception de ssh-guard — pourquoi cette crate, modèle de menace, design async
- Architecture des crates — position de ssh-guard dans le graphe de dépendances
- Configurer ssh-guard — recettes par profil de déploiement
- Variables d’environnement — section SSH_GUARD_*
- Événements ssh-guard (JSON)
Services et API interne — Référence
Ce document liste tous les services publics de gitrust (rustwarden-core + gitrust-core), leurs signatures Rust, les erreurs possibles, et quand les utiliser.
Architecture générale
Handler Axum → Service (logique métier) → SeaORM (base de données)
Conventions communes :
- Les services sont des structs sans état avec des méthodes associées
async fn. - Premier paramètre toujours
db: &DatabaseConnection. - Type de retour toujours
Result<T, AppError>(rustwarden-core) ouResult<T, GitrustError>(gitrust-core). - Aucun
unwrap()/expect()en production. - Aucun SQL brut — tout passe par SeaORM.
Services rustwarden-core (framework)
Importés via use rustwarden_core::prelude::* pour les exports du prélude, ou via leur chemin complet.
AuthService
Module : rustwarden_core::services::auth_service
| Méthode | Signature | Description |
|---|---|---|
verify_credentials | (db, identifier: &str, password: &str) → Result<user::Model> | Vérifie les identifiants. identifier accepte un username ou un email. Utilise un dummy hash si l’utilisateur n’existe pas (protection timing attack SEC-C1). |
authenticate_user | (db, identifier: &str, password: &str) → Result<user::Model> | Authentification de haut niveau. Appelle verify_credentials et logue must_change_password. |
UserService
Module : rustwarden_core::services::user_service — Export : prélude
| Méthode | Signature | Description |
|---|---|---|
create_user | (db, username, email, password_hash, must_change_password) → Result<user::Model> | Crée un utilisateur. Le mot de passe doit être pré-haché via PasswordService::hash_password. |
get_user_by_id | (db, user_id: Uuid) → Result<Option<user::Model>> | Recherche par ID. |
get_user_by_username | (db, username: &str) → Result<Option<user::Model>> | Recherche par nom d’utilisateur. |
get_user_by_email | (db, email: &str) → Result<Option<user::Model>> | Recherche par email. |
list_all_users | (db) → Result<Vec<user::Model>> | Liste tous les utilisateurs. |
get_user_roles | (db, user_id: Uuid) → Result<Vec<String>> | Retourne les noms des rôles. |
assign_role_to_user | (db, user_id, role_id) → Result<user_role::Model> | Assigne un rôle. |
update_profile | (db, user_id, username: Option<String>, email: Option<String>) → Result<user::Model> | Met à jour le profil. Valide l’unicité. |
change_password | (db, user_id, old_password, new_password, current_token_id) → Result<(Uuid, String)> | Initie un changement de mot de passe (demande pendante). Retourne (request_id, plain_token). |
PasswordService
Module : rustwarden_core::services::password_service
| Méthode | Signature | Description |
|---|---|---|
hash_password | async (password: &str) → Result<String> | Hash bcrypt via spawn_blocking. Coût configurable via BCRYPT_COST (défaut : 12). |
verify_password | async (password: &str, hash: &str) → Result<bool> | Vérification bcrypt via spawn_blocking. |
generate_secure_password | () → String | Génère un mot de passe de 20 caractères avec mix garanti. |
TotpService
Module : rustwarden_core::services::totp_service
| Méthode | Description |
|---|---|
setup_totp(db, user_id, username, issuer) | Génère un secret TOTP + QR code URI. |
verify_and_enable(db, user_id, code) | Vérifie un code et active le 2FA. Retourne les 10 codes de secours. |
verify_code(db, user_id, code) | Vérifie un code TOTP (6 chiffres). |
verify_backup_code(db, user_id, code) | Vérifie un code de secours (8 chars, usage unique). |
disable_totp(db, user_id) | Désactive le 2FA. |
create_challenge(db, user_id) | Crée un challenge 2FA (token temporaire, 5 min, max 5 tentatives). |
validate_challenge(db, challenge_token) | Valide un challenge. |
consume_challenge(db, challenge_token) | Marque un challenge comme consommé. |
RoleService / PermissionService
Module : rustwarden_core::services::role_service — Export : prélude
| Méthode | Description |
|---|---|
RoleService::create_role(db, name, description) | Crée un rôle. |
RoleService::get_role_by_name(db, name) | Recherche par nom. |
RoleService::list_all_roles(db) | Liste tous les rôles. |
RoleService::initialize_default_roles(db) | Seed admin, user, app_manager. Idempotent. |
PermissionService::get_permissions_for_user(db, user_id) | Union des permissions de tous les rôles. |
PermissionService::set_role_permissions(db, role_id, perm_ids) | Remplace atomiquement les permissions d’un rôle. |
PermissionService::initialize_default_permissions(db) | Seed les 12 permissions par défaut. Idempotent. |
ResourceService
Module : rustwarden_core::services::resource_service — Export : prélude
Système générique de registre de ressources et de partage.
Hiérarchie des permissions : read < write < admin
| Méthode | Description |
|---|---|
register(db, resource_type, resource_id, owner_id) | Enregistre une ressource dans le registre. |
set_public(db, resource_type, resource_id, is_public) | Bascule la visibilité publique. |
unregister(db, resource_type, resource_id) | Supprime du registre (cascade sur les partages). |
share(db, resource_type, resource_id, shared_with_user_id, permission_level, shared_by_user_id) | Partage une ressource. Met à jour si déjà partagé. |
revoke_share(db, share_id) | Révoque un partage par ID. |
user_can_access(db, user_id, resource_type, resource_id, required_level) | Vérifie l’accès : owner → public+read → partagé. |
effective_permission(db, user_id, resource_type, resource_id) | Retourne "owner", le niveau de partage, "read" (public), ou None. |
accessible_by_user(db, user_id, resource_type) | Toutes les ressources accessibles (possédées + partagées + publiques). |
RefreshTokenService
Module : rustwarden_core::services::refresh_token_service
| Méthode | Description |
|---|---|
create_refresh_token(db, user_id, expiration_days) | Crée un refresh token. Révoque les plus anciens si > 10 actifs (SEC-M4). |
validate_refresh_token(db, token) | Valide un token (hash SHA-256, existence, non révoqué, non expiré). |
revoke_all_tokens_for_user(db, user_id) | Révoque toutes les sessions (logout, changement de mot de passe). |
list_user_sessions(db, user_id) | Liste les sessions actives. |
revoke_session(db, token_id) | Révoque une session spécifique. |
AppSettingsService
Module : rustwarden_core::services::app_settings_service
| Méthode | Description |
|---|---|
get_setting(db, key) | Lire un paramètre. |
set_setting(db, key, value, updated_by) | Créer ou mettre à jour un paramètre. |
get_bool(db, key, default) | Lire un booléen avec valeur par défaut. |
initialize_default_settings(db) | Seed ~30 paramètres par défaut. Idempotent. |
Services gitrust-core
RepositoryService
Module : gitrust_core::services::repository_service
| Méthode | Signature | Description |
|---|---|---|
create | (db, owner_id, slug, description, is_public) → Result<repository::Model> | Crée un dépôt + répertoire bare sur disque. Enregistre dans ResourceService. |
find_by_owner_and_slug | (db, owner_username, slug) → Result<Option<repository::Model>> | Recherche par owner/slug (chemin URL). |
list_by_owner | (db, owner_id) → Result<Vec<repository::Model>> | Dépôts d’un propriétaire. |
update | (db, repo_id, owner_id, description, default_branch) → Result<repository::Model> | Met à jour les métadonnées. Anti-IDOR. |
delete | (db, repo_id, owner_id) → Result<()> | Supprime DB + bare repo sur disque. Anti-IDOR. |
mark_non_empty | (db, repo_id) → Result<()> | Passe is_empty = false après le premier push. |
SshKeyService
Module : gitrust_core::services::ssh_key_service
| Méthode | Signature | Description |
|---|---|---|
create | (db, user_id, title, key_data) → Result<ssh_key::Model> | Parse la clé, calcule le fingerprint SHA256, valide le type (ed25519, rsa ≥ 4096 bits, ecdsa-p256/p384). |
list_by_user | (db, user_id) → Result<Vec<ssh_key::Model>> | Liste les clés d’un utilisateur. |
find_by_fingerprint | (db, fingerprint) → Result<Option<ssh_key::Model>> | Recherche par empreinte (utilisé par le serveur SSH). |
delete | (db, key_id, user_id) → Result<()> | Suppression avec vérification d’ownership. |
update_last_used | (db, key_id) → Result<()> | Met à jour last_used_at à chaque authentification SSH. |
IssueService
Module : gitrust_core::services::issue_service
| Méthode | Description |
|---|---|
create(db, repo_id, author_id, title, body, label_ids, subject_names) | Numéro auto-incrémenté par dépôt. |
list(db, repo_id, state_filter, label_filter, page, per_page) | Liste paginée avec filtres. |
find_by_number(db, repo_id, number) | Par numéro d’issue. |
close(db, issue_id, closed_by) | Ferme l’issue + audit. |
reopen(db, issue_id) | Rouvre l’issue + audit. |
enrich_issue(db, issue) | Enrichit avec username auteur, labels, nombre de commentaires. |
LabelService
Module : gitrust_core::services::label_service
| Méthode | Description |
|---|---|
create_classification(db, owner_id, name, color, description) | Label scope owner (tous ses dépôts). |
create_subject(db, repo_id, name, color) | Label scope dépôt. Couleur défaut #6b7280. |
find_or_create_subject(db, repo_id, name) | Trouve ou crée (pour création inline dans les issues). |
list_classification_by_owner(db, owner_id) | Labels classification de l’owner. |
list_subject_by_repo(db, repo_id) | Labels subject du dépôt. |
search_subject(db, repo_id, query, limit) | Recherche préfixe (autocomplete HTMX). |
assign_to_issue(db, issue_id, label_id) | Association issue ↔ label. |
TeamService
Module : gitrust_core::services::team_service
| Méthode | Description |
|---|---|
create(db, owner_id, slug, description) | Crée une équipe. |
add_member(db, team_id, user_id, role) | Ajoute un membre. |
remove_member(db, team_id, user_id) | Retire un membre. |
grant_repo_access(db, team_id, repo_id, permission) | Accorde l’accès à un dépôt (read/write/admin). |
revoke_repo_access(db, team_id, repo_id) | Révoque l’accès. |
ImportService
Module : gitrust_core::services::import_service
| Méthode | Description |
|---|---|
create_job(db, owner_id, source_url, target_slug) | Crée un job pending dans import_jobs. |
mark_running(db, job_id) | Passe en running + started_at. |
update_progress(db, job_id, received, total, bytes) | Met à jour les compteurs de progression. |
mark_success(db, job_id, repo_id) | Lie le job au dépôt créé. |
mark_failed(db, job_id, error_message) | Enregistre l’échec. |
mark_cancelled(db, job_id) | Annulation. |
Gestion des erreurs
AppError (rustwarden-core)
| Variante | HTTP | Usage |
|---|---|---|
AppError::NotFound(msg) | 404 | Ressource introuvable |
AppError::Validation(msg) | 400 | Données invalides |
AppError::Forbidden | 403 | Accès refusé (RBAC) |
AppError::Unauthorized | 401 | Non authentifié |
AppError::TokenExpired | 401 | JWT expiré |
AppError::TokenReused | 401 | Refresh token réutilisé (attaque détectée) |
AppError::Conflict(msg) | 409 | Contrainte d’unicité violée |
AppError::Internal(msg) | 500 | Erreur interne |
GitrustError (gitrust-core)
Mêmes variantes que AppError avec GitrustError::Database(sea_orm::DbErr) en plus pour les erreurs SeaORM non mappées.
Patterns communs
Accès à un service depuis un handler
#![allow(unused)]
fn main() {
use gitrust_core::services::issue_service::IssueService;
async fn list_issues_handler(
State(db): State<DatabaseConnection>,
user: AuthUser,
) -> Result<impl IntoResponse, AppError> {
let issues = IssueService::list(&db, repo_id, "open", None, 1, 30).await?;
// ...
}
}
Vérification d’ownership systématique (anti-IDOR)
#![allow(unused)]
fn main() {
// Toujours passer owner_id au service — le service vérifie en interne
let repo = RepositoryService::find_by_owner_and_slug(&db, &owner, &repo_slug).await?;
// Puis vérifier que l'utilisateur courant peut accéder
if !ResourceService::user_can_access(&db, user.user_id, "repository", repo.id, "read").await? {
return Err(AppError::Forbidden);
}
}
Voir aussi
Schéma de la base de données — Référence développeur
Ce document décrit le schéma complet de la base de données gitrust, orienté développeur : types SeaORM côté Rust, enums, contraintes de modèle, et ordre des migrations.
Pour la vue admin (sauvegardes, supervision), consultez le manuel d’administration.
ERD — Diagramme entité-relation
erDiagram
%% ===== Tables rustwarden-core =====
users {
uuid id PK
string username UK
string email UK
string password_hash
boolean must_change_password
boolean email_verified
timestamptz last_login_at
timestamptz created_at
timestamptz updated_at
}
resources {
uuid id PK
string resource_type
uuid resource_id
uuid owner_id FK
boolean is_public
timestamptz created_at
}
resource_shares {
uuid id PK
uuid resource_id FK
uuid shared_with_user_id FK
string permission_level
uuid shared_by_user_id FK
timestamptz created_at
}
user_totp {
uuid id PK
uuid user_id FK
text encrypted_secret
boolean enabled
text backup_codes_json
timestamptz enabled_at
timestamptz created_at
timestamptz updated_at
}
%% ===== Tables gitrust-core =====
repositories {
uuid id PK
uuid owner_id FK
string slug
string description
string disk_path
string default_branch
boolean is_empty
timestamptz created_at
timestamptz updated_at
}
ssh_keys {
uuid id PK
uuid user_id FK
string title
string fingerprint UK
string key_type
text key_data
timestamptz last_used_at
timestamptz created_at
}
teams {
uuid id PK
uuid owner_id FK
string slug
string description
timestamptz created_at
timestamptz updated_at
}
team_members {
uuid id PK
uuid team_id FK
uuid user_id FK
string role
timestamptz created_at
}
team_repository_access {
uuid id PK
uuid team_id FK
uuid repository_id FK
string permission
timestamptz created_at
}
personal_access_tokens {
uuid id PK
uuid user_id FK
string name
string token_hash UK
string scopes
timestamptz expires_at
timestamptz last_used_at
timestamptz created_at
}
issues {
uuid id PK
uuid repository_id FK
integer number
uuid author_id FK
string title
text body
string state
uuid closed_by FK
timestamptz created_at
timestamptz updated_at
}
issue_comments {
uuid id PK
uuid issue_id FK
uuid author_id FK
text body
timestamptz created_at
timestamptz updated_at
}
labels {
uuid id PK
uuid owner_id FK
uuid repository_id FK
string name
string color
string description
string label_type
timestamptz created_at
}
issue_labels {
uuid id PK
uuid issue_id FK
uuid label_id FK
timestamptz created_at
}
pull_requests {
uuid id PK
uuid repository_id FK
integer number
uuid author_id FK
string title
text body
string source_branch
string target_branch
string state
timestamptz merged_at
uuid merged_by FK
string merge_commit_sha
timestamptz created_at
timestamptz updated_at
}
pr_comments {
uuid id PK
uuid pull_request_id FK
uuid author_id FK
text body
timestamptz created_at
}
import_jobs {
uuid id PK
uuid repository_id FK
uuid owner_id FK
text source_url
text target_slug
string status
string phase
int received_objects
int total_objects
bigint received_bytes
text error_message
timestamptz started_at
timestamptz finished_at
int duration_ms
timestamptz created_at
timestamptz updated_at
}
%% Relations
users ||--o{ repositories : "possède"
users ||--o{ ssh_keys : "enregistre"
users ||--o{ teams : "crée"
users ||--o{ resources : "possède"
users ||--o| user_totp : "configure"
repositories ||--o| resources : "enregistre dans"
resources ||--o{ resource_shares : "partage"
teams ||--o{ team_members : "contient"
teams ||--o{ team_repository_access : "accède à"
repositories ||--o{ team_repository_access : "accessible par"
repositories ||--o{ issues : "contient"
issues ||--o{ issue_comments : "a"
issues ||--o{ issue_labels : "étiquetée"
labels ||--o{ issue_labels : "appliquée"
repositories ||--o{ pull_requests : "contient"
pull_requests ||--o{ pr_comments : "a"
users ||--o{ personal_access_tokens : "crée"
users ||--o{ import_jobs : "lance"
Tables rustwarden-core (framework — read-only pour gitrust)
Ces tables sont gérées exclusivement par le framework rustwarden-core. Ne les modifiez jamais directement depuis gitrust.
| Table | Rôle |
|---|---|
users | Comptes utilisateurs |
roles / permissions / role_permissions / user_roles | RBAC global |
resources / resource_shares | Registre générique de ressources et partages |
refresh_tokens / jwt_blacklist | Gestion JWT |
app_settings | Configuration applicative clé-valeur |
audit_logs | Journal d’audit |
user_totp / totp_challenges | Authentification 2FA TOTP |
email_queue / email_delivery_status | File d’emails asynchrone |
password_reset_tokens / password_change_requests | Flux de changement de mot de passe |
Tables gitrust-core
repositories
| Colonne | Type Rust (SeaORM) | Contrainte PG | Notes |
|---|---|---|---|
id | Uuid | PK | |
owner_id | Uuid | FK → users ON DELETE CASCADE | |
slug | String | VARCHAR(64) NOT NULL | Newtype RepoSlug à la frontière |
description | Option<String> | VARCHAR(500) NULL | |
disk_path | String | VARCHAR(512) NOT NULL | Chemin absolu du bare repo |
default_branch | String | VARCHAR(255) NOT NULL DEFAULT ‘main’ | |
is_empty | bool | BOOLEAN NOT NULL DEFAULT true | Passe à false après le premier push |
created_at | DateTimeUtc | TIMESTAMPTZ NOT NULL | |
updated_at | DateTimeUtc | TIMESTAMPTZ NOT NULL |
Index : UNIQUE(owner_id, slug), INDEX(owner_id)
Lien ResourceService : chaque dépôt est enregistré dans resources avec resource_type = "repository" et resource_id = repositories.id.
ssh_keys
| Colonne | Type Rust | Contrainte PG | Notes |
|---|---|---|---|
id | Uuid | PK | |
user_id | Uuid | FK → users CASCADE | |
title | String | VARCHAR(255) NOT NULL | |
fingerprint | String | VARCHAR(128) UNIQUE NOT NULL | SHA256, format SHA256:xxx |
key_type | String | VARCHAR(32) NOT NULL | ssh-ed25519, ssh-rsa, ecdsa-sha2-nistp256/384 |
key_data | String | TEXT NOT NULL | Format authorized_keys complet |
last_used_at | Option<DateTimeUtc> | TIMESTAMPTZ NULL | Mis à jour par le serveur SSH |
created_at | DateTimeUtc | TIMESTAMPTZ NOT NULL |
Validation : RSA ≥ 4096 bits obligatoire. Fingerprint calculé à l’insertion.
teams et team_members
| Colonne | Type Rust | Notes |
|---|---|---|
teams.slug | String | Newtype TeamSlug ; UNIQUE(owner_id, slug) |
team_members.role | String | "read" (défaut), "write", "admin" |
team_repository_access.permission | String | "read", "write", "admin" |
personal_access_tokens
| Colonne | Type Rust | Notes |
|---|---|---|
token_hash | String | SHA-256 du token en clair. Le token en clair n’est retourné qu’à la création. |
scopes | String | Espace-séparé : "repo:read repo:write issues:write" |
expires_at | Option<DateTimeUtc> | NULL = pas d’expiration |
issues
| Colonne | Type Rust | Notes |
|---|---|---|
number | i32 | Auto-incrémenté par dépôt (pas global). UNIQUE(repository_id, number) |
state | String | "open" (défaut) ou "closed" |
body | String | Markdown brut |
labels
| Colonne | Type Rust | Notes |
|---|---|---|
label_type | String | "classification" (scope owner) ou "subject" (scope repo) |
owner_id | Option<Uuid> | Non-null pour classification, null pour subject |
repository_id | Option<Uuid> | Non-null pour subject, null pour classification |
color | String | Hex #RRGGBB, validé à l’insertion |
Index : UNIQUE(owner_id, repository_id, name, label_type)
pull_requests
| Colonne | Type Rust | Notes |
|---|---|---|
number | i32 | Auto-incrémenté par dépôt. UNIQUE(repository_id, number) |
state | String | "open", "closed", "merged" |
merge_commit_sha | Option<String> | SHA40, null avant merge |
import_jobs
| Colonne | Type Rust | Notes |
|---|---|---|
status | String | State machine : pending → running → success/failed/cancelled |
phase | Option<String> | "cloning", "resolving", "finalizing" |
repository_id | Option<Uuid> | Null pendant le clonage, rempli au succès uniquement |
Ordre des migrations
| # | Nom | Tables créées |
|---|---|---|
| 1 | m20260305_000001_initial_schema | users, refresh_tokens, jwt_blacklist, audit_logs (core) |
| 2 | m20260306_000002_create_app_settings_table | app_settings (core) |
| 3 | m20260309_000003_create_permissions_tables | roles, permissions, role_permissions, user_roles (core) |
| 4 | m20260309_000004_create_resources_tables | resources, resource_shares (core) |
| 5 | m20260310_000005_create_oauth_accounts_table | oauth_accounts (core) |
| 6 | m20260325_000001_create_repositories | repositories |
| 7 | m20260325_000002_create_ssh_keys | ssh_keys |
| 8 | m20260325_000003_create_teams | teams |
| 9 | m20260325_000004_create_team_members | team_members |
| 10 | m20260325_000005_create_team_repository_access | team_repository_access |
| 11 | m20260327_000006_create_personal_access_tokens | personal_access_tokens |
| 12 | m20260327_000007_create_issues | issues |
| 13 | m20260327_000008_create_issue_comments | issue_comments |
| 14 | m20260327_000009_create_labels | labels, issue_labels |
| 15 | m20260327_000010_add_label_type | ALTER labels (label_type) |
| 16 | m20260327_000011_labels_owner_scope | ALTER labels (owner_id, repository_id nullable) |
| 17 | m20260327_000012_create_pull_requests | pull_requests |
| 18 | m20260327_000013_create_pr_comments | pr_comments |
| 19 | m20260409_000006_create_totp_tables | user_totp, totp_challenges (core) |
| 20+ | m20260416_000022_create_import_jobs | import_jobs |
Toutes les migrations sont exécutées par AppMigrator (combine core + gitrust) dans gitrust_core::migrations::run_migrations().
Voir aussi
Règles QA & conformité ANSSI
Ce document définit les règles de qualité et de sécurité applicables à tout le projet gitrust. Chaque lot doit satisfaire ces règles avant merge.
1. Gates de compilation obligatoires
Toute modification doit passer ces 4 gates sans erreur :
| Gate | Commande | Critère |
|---|---|---|
| Formatage | cargo fmt --all -- --check | Zéro diff |
| Linting | cargo clippy --workspace -- -D warnings | Zéro warning |
| Tests unitaires | cargo test --workspace | 100% pass |
| Build CSS | npx tailwindcss -i static/css/input.css -o static/css/style.css --minify | Si templates modifiés |
Gates supplémentaires (à mettre en place)
| Gate | Outil | Rôle |
|---|---|---|
| Audit dépendances | cargo audit | Détection CVE dans les deps |
| Licences & bans | cargo deny check | Licences compatibles, pas de crate bannie |
| Secrets dans le code | Recherche de patterns sensibles | Pas de token/mot de passe en dur |
2. Règles de sécurité (ANSSI PA-074)
2.1 Lints Rust obligatoires (déjà en place)
#![allow(unused)]
#![forbid(unsafe_code)] // core, web, hooks
#![deny(unsafe_code)] // git, ssh (FFI nécessaire)
#![deny(clippy::unwrap_used)]
#![deny(clippy::expect_used)]
#![deny(clippy::panic)]
#![deny(clippy::indexing_slicing)]
#![deny(clippy::mem_forget)]
fn main() {
}
2.2 Checklist sécurité par feature
Avant toute implémentation touchant l’authentification, les secrets ou les permissions, vérifier :
- Secrets jamais en clair en base — hash SHA-256 ou bcrypt
- Secrets jamais loggés — vérifier
tracing::info/debug/warn,DisplayetDebugimpls ne révèlent pas le secret - Comparaison constant-time —
subtle::ConstantTimeEqpour toute validation de secret (pas de==sur des hash) - Rate-limiting — endpoint d’authentification protégé
- CSRF — token valide sur toute action mutante (POST/PUT/DELETE)
- Ownership vérifiée (anti-IDOR) — le user ne peut agir que sur ses propres ressources (vérifier
user_idcôté serveur) - Expiration obligatoire — tout token/session a une durée de vie max
- Audit log — création, révocation, utilisation suspecte tracées
- Zeroize — types sensibles implémentent
Zeroize/ZeroizeOnDrop - Path traversal — validation des chemins disque (pas de
..,/,\)
2.3 Marqueurs SEC-
Le code utilise des commentaires // SEC-XX pour tracer les décisions de sécurité. Tout nouveau contrôle doit être marqué avec le prochain numéro disponible dans sa catégorie :
| Préfixe | Catégorie | Exemples |
|---|---|---|
| SEC-C | Cryptographie | Timing attack, CSRF, PKCE |
| SEC-H | HTTP/Headers | X-Forwarded-For, cookies, nonce |
| SEC-L | Logique métier | Hashing, validation, defaults |
| SEC-M | Mémoire/sessions | Rate-limit DoS, refresh tokens |
3. Tests requis par catégorie
3.1 Matrice de couverture
| Catégorie | Quand appliquer | Exemples |
|---|---|---|
| Unitaire | Logique pure (validation, parsing, conversion) | RepoSlug::new("../evil") → erreur |
| Intégration | Service avec DB (CRUD, contraintes, transactions) | PatService::validate token expiré → None |
| Handler | Endpoint HTTP (status, redirect, CSRF, auth) | POST sans CSRF → 403 |
| E2E Playwright | Flow utilisateur complet | Créer token → copier → cloner un dépôt |
| Sécurité négatif | Tout bypass imaginable | Token user A sur ressource user B → 401 |
3.2 Règles de tests
- Tests d’intégration sur vraie DB — pas de mocks pour la couche persistence (les mocks masquent les bugs de migration)
- Tests négatifs obligatoires — pour chaque chemin heureux, tester au moins : input invalide, non-authentifié, non-autorisé, expiré, révoqué
- Tests E2E en français — cohérent avec l’UI (locale
fr-FR) - Pas de
sleep()dans les tests — utiliser des retry/poll avec timeout - Données de test isolées — chaque test crée ses propres données (pas de dépendance sur l’ordre d’exécution)
4. Règles de code
4.1 Framework boundary
- Ne jamais modifier
crates/rustwarden-core/ - Réutiliser les services framework avant d’implémenter (auth, users, sessions, ResourceService, i18n, middleware)
- Si un service manque, l’étendre côté gitrust (wrappers, trait impls)
4.2 Assets
- Zéro CDN — tous les CSS/JS servis depuis
static/ - La CSP du framework bloque les domaines externes
4.3 Gestion d’erreurs
GitrustErroravecIntoResponsepour le mapping HTTP- Pas de
.unwrap()/.expect()/panic!()/[index] - Errors utilisateur : messages génériques (pas de leak d’info interne)
4.4 Validation aux frontières
- Valider les inputs utilisateur (formulaires, query params, headers)
- Ne pas re-valider entre services internes
- Newtypes avec validation à la construction (
RepoSlug,TeamSlug,Fingerprint,TokenHash)
5. Checklist pré-merge
Avant chaque merge de lot :
-
cargo fmt --all -- --checkpasse -
cargo clippy --workspace -- -D warningspasse -
cargo test --workspace— tous les tests passent - Tests E2E Playwright passent (
npm run test:e2e) - CSS rebuild si templates modifiés
- Aucun secret en clair dans le code ou les logs
- Checklist sécurité §2.2 validée (si applicable)
- Pas de modification dans
crates/rustwarden-core/
API REST v1 — Référence
L’API REST gitrust est accessible sous le préfixe /api/v1/. Elle accepte et retourne du JSON. Elle partage les mêmes services que l’interface web SSR.
Documentation interactive : GET /api/docs sur toute instance gitrust.
Authentification
Deux méthodes sont acceptées sur tous les endpoints authentifiés :
Cookie JWT (navigateur) — défini automatiquement par le serveur après login :
Cookie: jwt_token=<JWT>
Bearer PAT (clients API, CI, scripts) — Personal Access Token créé depuis /settings/tokens :
Authorization: Bearer <PAT>
Les PAT supportent des scopes : repo:read, repo:write, issues:read, issues:write, pulls:read, pulls:write, ci:read, ci:write.
Un endpoint qui requiert repo:write rejette un PAT avec seulement repo:read avec 403 Forbidden.
Format des erreurs
Toutes les erreurs retournent ce format JSON :
{
"error": "not_found",
"message": "Le dépôt 'alice/myrepo' est introuvable",
"status": 404
}
Codes d’erreur standards :
error | HTTP | Cause |
|---|---|---|
unauthorized | 401 | Token absent, expiré ou invalide |
forbidden | 403 | Permissions insuffisantes |
not_found | 404 | Ressource inexistante |
validation_error | 400 | Corps de requête invalide |
conflict | 409 | Contrainte d’unicité violée |
rate_limited | 429 | Trop de requêtes |
internal_error | 500 | Erreur serveur |
Pagination
Tous les endpoints de liste acceptent ?page=N&per_page=N (défaut : page=1, per_page=30, max per_page=100).
Headers de réponse :
X-Total-Count: 142
X-Page: 2
X-Per-Page: 30
Rate limiting
En cas de dépassement : 429 Too Many Requests avec Retry-After: <secondes>.
Endpoints
Authentification
POST /api/v1/auth/login
// Requête
{ "username": "alice", "password": "SecurePass123!" }
// Réponse 200 (sans 2FA)
{
"access_token": "<JWT>",
"refresh_token": "<token>",
"token_type": "Bearer",
"user": { "id": "...", "username": "alice", "email": "alice@example.com" }
}
// Réponse 200 (avec 2FA activé)
{ "requires_2fa": true, "challenge_token": "abc123..." }
POST /api/v1/auth/2fa/verify
// Requête
{ "challenge_token": "abc123...", "code": "123456" }
// Réponse 200
{ "access_token": "...", "refresh_token": "...", "user": { ... } }
POST /api/v1/auth/refresh
// Requête
{ "refresh_token": "<token>" }
// Réponse 200
{ "access_token": "<nouveau JWT>", "refresh_token": "<nouveau token>" }
GET /api/v1/auth/me
// Réponse 200
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"username": "alice",
"email": "alice@example.com",
"email_verified": true,
"roles": ["user"]
}
Utilisateur courant
GET /api/v1/user
Profil de l’utilisateur authentifié. Identique à GET /api/v1/auth/me.
GET /api/v1/user/repos
Liste les dépôts accessibles par l’utilisateur courant (possédés + partagés).
// Réponse 200
{
"items": [
{
"id": "...",
"owner": "alice",
"slug": "myrepo",
"description": "Mon premier dépôt",
"default_branch": "main",
"is_empty": false,
"is_public": false,
"created_at": "2026-03-25T10:00:00Z"
}
],
"total": 1,
"page": 1,
"per_page": 30
}
Dépôts
GET /api/v1/repos/{owner}/{repo}
// Réponse 200
{
"id": "...",
"owner": "alice",
"slug": "myrepo",
"description": "Mon dépôt",
"default_branch": "main",
"is_empty": false,
"is_public": true,
"created_at": "2026-03-25T10:00:00Z",
"updated_at": "2026-04-01T12:00:00Z"
}
GET /api/v1/repos/{owner}/{repo}/branches
// Réponse 200
{
"items": [
{ "name": "main", "is_default": true, "commit_sha": "abc123..." },
{ "name": "feat/42-fix", "is_default": false, "commit_sha": "def456..." }
],
"total": 2, "page": 1, "per_page": 30
}
GET /api/v1/repos/{owner}/{repo}/commits
Paramètres optionnels : ?ref=main&page=1&per_page=30
// Réponse 200
{
"items": [
{
"sha": "abc123...",
"message": "feat: ajouter la signature HMAC",
"author": { "name": "Alice", "email": "alice@example.com" },
"committed_at": "2026-04-01T09:00:00Z"
}
],
"total": 42, "page": 1, "per_page": 30
}
Issues
GET /api/v1/repos/{owner}/{repo}/issues
Paramètres : ?state=open&page=1&per_page=30
// Réponse 200
{
"items": [
{
"id": "...",
"number": 1,
"title": "Bug dans le parser",
"state": "open",
"author": "alice",
"labels": [
{ "name": "bug", "color": "#d73a4a", "type": "classification" }
],
"comment_count": 3,
"created_at": "2026-03-27T08:00:00Z"
}
],
"total": 5, "page": 1, "per_page": 30
}
POST /api/v1/repos/{owner}/{repo}/issues
// Requête
{ "title": "Nouveau bug", "body": "Description en Markdown..." }
// Réponse 201
{ "id": "...", "number": 6, "title": "Nouveau bug", "state": "open", ... }
PATCH /api/v1/repos/{owner}/{repo}/issues/{num}
// Requête (tous les champs optionnels)
{ "state": "closed" }
// Réponse 200
{ "id": "...", "number": 1, "state": "closed", ... }
Pull Requests
GET /api/v1/repos/{owner}/{repo}/pulls
Paramètres : ?state=open&page=1&per_page=30
POST /api/v1/repos/{owner}/{repo}/pulls
// Requête
{
"title": "feat: ajouter la signature HMAC",
"body": "Closes #42",
"source_branch": "feat/42-hmac",
"target_branch": "main"
}
// Réponse 201
{ "id": "...", "number": 3, "state": "open", ... }
POST /api/v1/repos/{owner}/{repo}/pulls/{num}/merge
// Requête
{ "merge_method": "merge" }
// merge_method: "merge" | "squash" | "rebase"
// Réponse 200
{ "merged": true, "merge_commit_sha": "abc123..." }
CI Pipelines
GET /api/v1/repos/{owner}/{repo}/ci/pipelines
Paramètres : ?page=1&per_page=30
// Réponse 200
{
"items": [
{
"id": "...",
"status": "success",
"trigger": "push",
"ref": "main",
"commit_sha": "abc123...",
"duration_ms": 45000,
"created_at": "2026-04-01T09:00:00Z"
}
],
"total": 12, "page": 1, "per_page": 30
}
POST /api/v1/repos/{owner}/{repo}/ci/pipelines/trigger
Déclenche un pipeline manuellement.
// Requête
{ "ref": "main" }
// Réponse 201
{ "id": "...", "status": "pending", ... }
GET /api/v1/repos/{owner}/{repo}/ci/pipelines/{id}/logs
// Réponse 200
{
"items": [
{ "line": 1, "content": "$ cargo build --release", "stream": "stdout", "timestamp": "..." },
{ "line": 2, "content": " Compiling gitrust v0.1.0", "stream": "stdout", "timestamp": "..." }
],
"total": 248, "page": 1, "per_page": 100
}
Exemples clients
Lister les issues avec curl
curl -s \
-H "Authorization: Bearer ${GITRUST_PAT}" \
"https://demo.gitrust.eu/api/v1/repos/alice/myrepo/issues?state=open" \
| jq '.items[].title'
Créer une issue avec Python
import httpx
client = httpx.Client(
base_url="https://demo.gitrust.eu",
headers={"Authorization": f"Bearer {PAT}"},
)
resp = client.post(
"/api/v1/repos/alice/myrepo/issues",
json={"title": "Bug trouvé", "body": "Description..."},
)
resp.raise_for_status()
print(resp.json()["number"])
Créer une issue avec Rust
#![allow(unused)]
fn main() {
use reqwest::Client;
use serde_json::json;
let client = Client::new();
let resp = client
.post("https://demo.gitrust.eu/api/v1/repos/alice/myrepo/issues")
.bearer_auth(&pat)
.json(&json!({"title": "Bug", "body": "..."}))
.send()
.await?;
println!("Issue #{}", resp.json::<serde_json::Value>().await?["number"]);
}
Voir aussi
Hooks gitrust — Référence
Ce document décrit les hooks exposés par crates/gitrust-hooks/, quand ils sont appelés, leur payload, leurs effets de bord, et comment les tester localement.
Vue d’ensemble
gitrust-hooks implémente le trait RustwardenHooks de rustwarden-core. Ce trait est un mécanisme de lifecycle : le framework appelle les hooks aux moments clés du cycle de vie des ressources (inscription utilisateur, suppression, partage).
#![allow(unused)]
fn main() {
// crates/gitrust-hooks/src/lib.rs
#[async_trait]
impl RustwardenHooks for GitrustHooks {
async fn on_user_registered(...) -> anyhow::Result<()>;
async fn on_resource_shared(...) -> anyhow::Result<()>;
async fn on_user_deleted(...) -> anyhow::Result<()>;
async fn on_resource_unshared(...) -> anyhow::Result<()>;
}
}
Les hooks Git serveur (receive-pack, upload-pack) sont implémentés directement dans gitrust-web/src/handlers/git_http.rs et dans le serveur SSH (gitrust-ssh).
Hooks de lifecycle (RustwardenHooks)
on_user_registered
Déclencheur : après la création réussie d’un compte utilisateur (via /register ou l’API /api/v1/auth/register).
Payload :
#![allow(unused)]
fn main() {
async fn on_user_registered(
db: &DatabaseConnection,
user_id: Uuid,
username: &str,
) -> anyhow::Result<()>
}
Effet de bord : crée le répertoire de dépôts de l’utilisateur sur le système de fichiers :
{REPOS_BASE_PATH}/{username}/
Validation de sécurité : le username est validé contre la traversal de chemin (.., /, \) avant toute opération disque. Un username suspect déclenche un log WARN et retourne une erreur — le compte n’est pas créé.
Idempotence : si le répertoire existe déjà, l’appel réussit silencieusement.
on_user_deleted
Déclencheur : juste avant la suppression d’un utilisateur depuis le panel admin.
Payload :
#![allow(unused)]
fn main() {
async fn on_user_deleted(
db: &DatabaseConnection,
user_id: Uuid,
username: &str,
) -> anyhow::Result<()>
}
Effets de bord :
- Supprime le répertoire disque
{REPOS_BASE_PATH}/{username}/et tous les bare repos qu’il contient. - Enregistre un événement
user_deleteddansaudit_logs(niveau WARN).
Note : la cascade DB (suppression des lignes repositories) est gérée par la contrainte FK ON DELETE CASCADE. Ce hook gère uniquement le système de fichiers.
En cas d’erreur disque (permissions, NFS indisponible), l’erreur est loggée en WARN mais ne bloque pas la suppression en base.
on_resource_shared
Déclencheur : après un appel réussi à ResourceService::share() — quand un dépôt est partagé avec un collaborateur.
Payload :
#![allow(unused)]
fn main() {
async fn on_resource_shared(
db: &DatabaseConnection,
resource_type: &str,
resource_id: Uuid,
shared_with_user_id: Uuid,
permission_level: &str,
shared_by_user_id: Uuid,
) -> anyhow::Result<()>
}
Effet de bord : si resource_type == "repository", enregistre l’événement REPO_SHARED dans audit_logs avec les détails du partage.
on_resource_unshared
Déclencheur : après un appel réussi à ResourceService::revoke_share() ou revoke_user_access().
Payload :
#![allow(unused)]
fn main() {
async fn on_resource_unshared(
db: &DatabaseConnection,
resource_type: &str,
resource_id: Uuid,
user_id: Uuid,
) -> anyhow::Result<()>
}
Effet de bord : si resource_type == "repository", enregistre l’événement REPO_UNSHARED dans audit_logs.
Hooks Git serveur (receive-pack / upload-pack)
Ces hooks ne sont pas dans gitrust-hooks mais dans les handlers HTTP et SSH.
Receive-pack (après un git push)
Déclencheur : POST /{owner}/{repo}/git-receive-pack (HTTP) ou connexion SSH suivie d’un git-receive-pack.
Séquence d’effets :
sequenceDiagram
participant Client as git push
participant Handler as receive_pack handler
participant DB as Base de données
participant CI as CI Worker
participant WH as Webhook Dispatcher
Client->>Handler: objets packés
Handler->>DB: RepositoryService::mark_non_empty() si premier push
Handler->>DB: CiService::create_pipeline() si .gitrust-ci.yml détecté
Handler->>CI: mpsc::send(CiTask)
Handler->>DB: WebhookService::list_by_repo_and_event("push")
Handler->>WH: tokio::spawn → deliver(webhook, "push", payload)
Handler-->>Client: 200 OK (pack-protocol)
Payload webhook push :
{
"repository": {
"id": "...",
"slug": "myrepo",
"owner": "alice"
},
"pusher": { "username": "alice" },
"ref": "refs/heads/main",
"before": "0000000000000000000000000000000000000000",
"after": "abc123...",
"commits": [
{
"sha": "abc123...",
"message": "feat: ajouter la signature HMAC",
"author": { "name": "Alice", "email": "alice@example.com" }
}
]
}
Upload-pack (après un git fetch / git clone)
Déclencheur : GET /{owner}/{repo}/info/refs?service=git-upload-pack + POST /{owner}/{repo}/git-upload-pack.
Effets de bord : aucun effet de bord métier. Vérification des permissions d’accès (public → lecture libre ; privé → authentification requise).
Hooks SBOM (post-push)
Après chaque push sur un dépôt avec SBOM_ENABLED=true, un job SBOM est déclenché via SbomService :
SbomService::trigger_scan(db, repo_id, commit_sha)crée un job dansci_jobs.- Le worker CI exécute
syftsur le répertoire de travail. - Le résultat est uploadé vers Dependency-Track via HTTP.
- Les findings critiques peuvent bloquer la CI selon la configuration
sbom_block_on_violation.
Tester les hooks localement
Tester on_user_registered
#![allow(unused)]
fn main() {
// tests/hooks_test.rs
#[tokio::test]
async fn on_user_registered_creates_dir() {
let tmp = tempfile::TempDir::new().unwrap();
let hooks = GitrustHooks::new(tmp.path().to_path_buf());
let db = sea_orm::Database::connect("sqlite::memory:").await.unwrap();
hooks
.on_user_registered(&db, Uuid::new_v4(), "alice")
.await
.unwrap();
assert!(tmp.path().join("alice").exists());
}
#[tokio::test]
async fn on_user_registered_rejects_traversal() {
let tmp = tempfile::TempDir::new().unwrap();
let hooks = GitrustHooks::new(tmp.path().to_path_buf());
let db = sea_orm::Database::connect("sqlite::memory:").await.unwrap();
assert!(hooks
.on_user_registered(&db, Uuid::new_v4(), "../etc/passwd")
.await
.is_err());
}
}
Simuler un push en local
Pour tester le cycle complet receive-pack → CI → webhook sans client réel :
# 1. Démarrer gitrust localement
cargo run
# 2. Créer un dépôt via l'UI, cloner en SSH ou HTTP
git clone http://localhost:4000/alice/myrepo.git
# 3. Ajouter un fichier et pusher
cd myrepo
echo "hello" > README.md
git add README.md && git commit -m "test push"
git push origin main
# 4. Vérifier les effets de bord dans les logs
# → Log "Created pipeline" si .gitrust-ci.yml présent
# → Log "Delivered webhook" si webhook configuré
Ajouter un nouveau hook
Pour réagir à un nouvel événement de lifecycle :
- Définissez la méthode dans le trait
RustwardenHooks(dansrustwarden-core/src/hooks.rs). - Implémentez la méthode dans
GitrustHooks(dansgitrust-hooks/src/lib.rs). - Appelez le hook depuis le service rustwarden-core approprié.
- Écrivez un test unitaire avec
tempfile::TempDiretsea_orm::Database::connect("sqlite::memory:").
Règle : les hooks ne doivent jamais bloquer un flux utilisateur critique. En cas d’erreur non fatale (écriture disque, audit log), loggez en WARN et retournez Ok(()).
Voir aussi
API rustwarden-core — Référence
Ce document décrit les helpers, extracteurs Axum, middleware, et APIs d’extension fournis par le framework rustwarden-core que gitrust consomme.
Extracteurs Axum
AuthUser
Module : rustwarden_core::auth::extractors
Export : prélude
Extracteur qui valide le JWT depuis le cookie jwt_token ou le header Authorization: Bearer <token>. Retourne 401 Unauthorized si le token est absent, expiré, invalide, ou blacklisté.
#![allow(unused)]
fn main() {
use rustwarden_core::prelude::*;
async fn handler(user: AuthUser) -> impl IntoResponse {
// Champs disponibles :
let _id: Uuid = user.user_id;
let _name: &str = &user.username;
let _roles: &[String] = &user.roles;
let _perms: &[String] = &user.permissions;
}
}
Méthodes de vérification :
| Méthode | Description |
|---|---|
has_role(role: &str) → bool | L’utilisateur a-t-il ce rôle ? |
has_any_role(roles: &[&str]) → bool | A-t-il au moins un des rôles ? |
has_all_roles(roles: &[&str]) → bool | A-t-il tous les rôles ? |
has_permission(permission: &str) → bool | A-t-il cette permission ? Le rôle admin retourne toujours true. |
AuthenticatedSession
Module : rustwarden_core::auth::session
Extracteur similaire à AuthUser mais inclut les informations de session (device info, IP). Utilisé pour la page /settings/sessions.
#![allow(unused)]
fn main() {
async fn sessions_handler(
State(db): State<DatabaseConnection>,
session: AuthenticatedSession,
) -> impl IntoResponse {
let current_token_id = session.token_id; // UUID du refresh token courant
// Utilisé pour marquer "Current session" dans l'UI
}
}
OptionalUser
Extracteur qui ne rejette pas les requêtes non authentifiées — retourne Option<AuthUser>. Utilisé pour les pages publiques (dépôts publics, explore).
#![allow(unused)]
fn main() {
async fn public_page(user: OptionalUser) -> impl IntoResponse {
match user.0 {
Some(u) => format!("Bonjour {}", u.username),
None => "Visiteur anonyme".to_owned(),
}
}
}
Middleware
Middleware d’authentification JWT
Intégré automatiquement dans RustwardenApp::build(). Gère :
- Lecture du JWT depuis
Cookie: jwt_token=<JWT>ouAuthorization: Bearer <JWT>. - Vérification de la signature HMAC-SHA256 avec la clé
JWT_SECRET. - Vérification de l’expiration (
exp). - Vérification de la blacklist (table
jwt_blacklist).
Ce middleware ne bloque pas les requêtes anonymes — c’est l’extracteur AuthUser dans le handler qui décide si l’authentification est requise.
Middleware CSRF
Protège toutes les requêtes mutantes (POST, PUT, DELETE, PATCH) émises par des formulaires HTML. Le token CSRF est :
- Généré à la connexion et stocké dans un cookie
csrf_token(HttpOnly, SameSite=Strict). - Attendu dans le corps du formulaire sous le champ
_csrf. - Absent ou invalide →
403 Forbidden.
Pour les endpoints API JSON (header Content-Type: application/json), la vérification CSRF est désactivée — l’authentification Bearer PAT/JWT suffit.
<!-- Dans tout formulaire POST -->
<input type="hidden" name="_csrf" value="{{ csrf_token }}">
Middleware de rate limiting
Basé sur une fenêtre glissante en mémoire (par IP ou par user_id). Configurable via les variables d’environnement :
RATE_LIMIT_LOGIN=10 # tentatives de login / 15 min / IP
RATE_LIMIT_API_READ=1000 # requêtes lecture / heure / user
RATE_LIMIT_API_WRITE=200 # requêtes écriture / heure / user
Claims — contenu du JWT
#![allow(unused)]
fn main() {
pub struct Claims {
pub sub: Uuid, // user_id
pub username: String,
pub roles: Vec<String>,
pub permissions: Vec<String>,
pub exp: i64, // timestamp d'expiration
pub iat: i64, // timestamp d'émission
pub iss: String, // issuer (app_domain)
pub jti: Uuid, // JWT ID unique (pour blacklist)
}
}
JwtService
Module : rustwarden_core::auth::jwt
Export : prélude
| Méthode | Signature | Description |
|---|---|---|
generate_token | (config, user_id, username, roles, permissions) → Result<String> | Génère un JWT signé HS256. |
validate_token | (config, token: &str) → Result<Claims> | Valide et décode. Retourne TokenExpired si expiré. |
extract_token_from_header | (header: &str) → Result<&str> | Extrait le token du header Authorization: Bearer <token>. |
Trait RustwardenHooks
Interface à implémenter dans votre projet consommateur pour réagir aux événements du framework.
#![allow(unused)]
fn main() {
#[async_trait]
pub trait RustwardenHooks: Send + Sync {
async fn on_user_registered(
&self,
db: &DatabaseConnection,
user_id: Uuid,
username: &str,
) -> anyhow::Result<()> {
Ok(()) // défaut no-op
}
async fn on_user_deleted(
&self,
db: &DatabaseConnection,
user_id: Uuid,
username: &str,
) -> anyhow::Result<()> {
Ok(())
}
async fn on_resource_shared(
&self,
db: &DatabaseConnection,
resource_type: &str,
resource_id: Uuid,
shared_with_user_id: Uuid,
permission_level: &str,
shared_by_user_id: Uuid,
) -> anyhow::Result<()> {
Ok(())
}
async fn on_resource_unshared(
&self,
db: &DatabaseConnection,
resource_type: &str,
resource_id: Uuid,
user_id: Uuid,
) -> anyhow::Result<()> {
Ok(())
}
}
}
Enregistrement dans main.rs :
#![allow(unused)]
fn main() {
let hooks = Arc::new(GitrustHooks::new(repos_base_path.clone()));
let app = RustwardenApp::builder()
.from_env()
.hooks(hooks)
.build()
.await?;
}
RBAC API
Vérification des permissions dans un handler
#![allow(unused)]
fn main() {
async fn admin_handler(user: AuthUser) -> Result<impl IntoResponse, AppError> {
if !user.has_role("admin") {
return Err(AppError::Forbidden);
}
// ...
}
}
Vérification des permissions sur une ressource
#![allow(unused)]
fn main() {
use rustwarden_core::prelude::*;
async fn repo_handler(
State(db): State<DatabaseConnection>,
user: AuthUser,
// ... autres extracteurs
) -> Result<impl IntoResponse, AppError> {
// Vérifier que l'utilisateur peut accéder en lecture
if !ResourceService::user_can_access(
&db, user.user_id, "repository", repo.id, "read"
).await? {
return Err(AppError::Forbidden);
}
// Déterminer le niveau d'accès effectif
match ResourceService::effective_permission(
&db, user.user_id, "repository", repo.id
).await? {
Some(ref p) if p == "owner" => { /* accès total */ }
Some(ref p) if p == "write" => { /* écriture permise */ }
Some(ref p) if p == "read" => { /* lecture seule */ }
_ => return Err(AppError::Forbidden),
}
}
}
i18n — Internationalisation
Module : rustwarden_core::i18n
Gitrust supporte 6 langues : fr, en, de, es, it, pt.
Les fichiers de traduction sont dans crates/rustwarden-core/locales/{lang}.yml et crates/gitrust-web/locales/{lang}.yml.
Dans un template Askama :
<!-- Clé de traduction avec paramètre -->
<h1>{{ t!("repository.title", repo_name = repo.slug) }}</h1>
<!-- Clé simple -->
<button>{{ t!("actions.save") }}</button>
Dans un handler (pour les messages d’erreur i18n) :
#![allow(unused)]
fn main() {
use rustwarden_core::i18n::I18n;
let message = I18n::translate(&user_lang, "errors.forbidden", &[]);
}
La langue est déterminée dans l’ordre : préférences utilisateur (table user_preferences) → cookie lang → header Accept-Language.
Email templating
Module : rustwarden_core::services::email_service
Pour envoyer un email depuis gitrust, utilisez EmailQueueService::enqueue (envoi asynchrone) :
#![allow(unused)]
fn main() {
use rustwarden_core::services::email_queue_service::EmailQueueService;
EmailQueueService::enqueue(
&db,
&to_email,
Some(&to_name),
&from_email,
Some(&from_name),
"Votre dépôt a été importé",
&html_body,
&text_body,
"import_success",
).await?;
}
Les templates d’email sont dans crates/rustwarden-core/templates/emails/ (format HTML + texte brut).
Étendre rustwarden-core
Règle absolue : ne modifiez jamais le code de crates/rustwarden-core/. C’est un framework partagé entre plusieurs projets (gitrust, PasterMan, etc.).
Pour étendre une fonctionnalité :
- Wrappers dans gitrust-core : créez un service wrapper dans
crates/gitrust-core/src/services/qui appelle le service rustwarden-core et ajoute la logique spécifique gitrust. - Trait impls dans gitrust-hooks : implémentez
RustwardenHookspour réagir aux événements du framework. - Feature flags : certaines fonctionnalités rustwarden-core sont derrière des features Cargo (
oauth,totp, etc.) — activez-les dansCargo.toml.
Voir aussi
Explications
Les explications répondent à la question « pourquoi ? ». Elles décrivent les décisions d’architecture, les modèles conceptuels, et les compromis qui ont façonné gitrust. Consultez-les quand vous avez besoin de comprendre un système avant de le modifier.
Diátaxis — mode compréhension : les explications ne vous apprennent pas à faire quelque chose de précis. Elles vous donnent un modèle mental juste, qui rend les autres actions plus sûres et plus cohérentes.
Les explications disponibles
Sécurité et accès
| Page | Ce qu’elle explique |
|---|---|
| Modèle de permissions RBAC | Les 4 rôles, les trois sources d’accès, le calcul du rôle effectif par maximum |
| Fonctionnement du 2FA TOTP | RFC 6238, chiffrement AES-256-GCM du secret, codes de secours bcrypt, challenges temporaires |
Interface et architecture
| Page | Ce qu’elle explique |
|---|---|
| Décisions UI : SSR, HTMX, DaisyUI | Pourquoi pas React, comment HTMX remplace des SPA pour 90 % des cas, budget bundle |
| Patron worker async | mpsc + Semaphore + SSE, durabilité via table SQL, récupération au redémarrage |
Données et organisation
| Page | Ce qu’elle explique |
|---|---|
| Hiérarchie des labels à deux niveaux | Classification (scope owner) vs subject (scope dépôt), filtrage cross-dépôt, schéma DB |
CI et outillage
| Page | Ce qu’elle explique |
|---|---|
| CI Dagger : Easy Mode vs Power Mode | Détection .gitrust-ci.yml / .dagger/, quand basculer, philosophie anti-YAML de Dagger |
Quand lire une explication
Lisez une explication quand :
- Vous vous apprêtez à modifier un système existant et vous voulez éviter de casser un invariant non documenté dans le code.
- Vous rencontrez une décision qui vous semble étrange (ex : pourquoi le rôle effectif est-il le maximum et non le minimum ?) et vous voulez comprendre le raisonnement.
- Vous voulez proposer une alternative architecturale et vous avez besoin du contexte des compromis déjà évalués.
Navigation croisée
- Pour appliquer ces concepts en pratique → Guides pratiques
- Pour consulter les API exactes → Référence
- Pour apprendre par l’exemple → Tutoriels
Vue d’ensemble de l’architecture
Gitrust est une plateforme d’hébergement Git auto-hébergée construite sur le framework rustwarden-core. L’application expose deux points d’entrée réseau (HTTP + SSH) et persiste ses données dans PostgreSQL.
Vue d’ensemble
graph TB
subgraph Clients
Browser["Navigateur Web"]
GitCLI["Git CLI (ssh/https)"]
end
subgraph "Gitrust Binary (src/main.rs)"
Main["main()"]
end
subgraph "Crates Gitrust"
Web["gitrust-web<br/><i>Routes HTTP, templates,<br/>handlers Axum</i>"]
SSH["gitrust-ssh<br/><i>Serveur SSH (russh),<br/>auth par clé, sessions</i>"]
SshGuard["gitrust-ssh-guard<br/><i>SecureListener, détecteurs,<br/>BanManager, AuthTracker</i>"]
Hooks["gitrust-hooks<br/><i>impl RustwardenHooks<br/>(on_user_registered, ...)</i>"]
Core["gitrust-core<br/><i>Models, services, migrations,<br/>rôles, types, DTOs</i>"]
Git["gitrust-git<br/><i>Bare repos, tree browser,<br/>pack protocol (git2)</i>"]
end
subgraph "Framework"
RW["rustwarden-core<br/><i>Auth, users, JWT, sessions,<br/>ResourceService, i18n</i>"]
end
subgraph "Stockage"
PG[(PostgreSQL)]
FS[("Système de fichiers<br/>bare repos .git")]
end
subgraph "CI/CD (optionnel)"
CiWorker["CiWorker<br/><i>tokio::spawn, mpsc,<br/>sous-processus Dagger</i>"]
Dagger["Dagger Engine<br/><i>Containers isolés,<br/>cache, exécution</i>"]
CiEngine["ci-engine<br/><i>Module Dagger Python<br/>(Easy Mode)</i>"]
Syft["Syft (optionnel)<br/><i>Génération SBOM<br/>CycloneDX</i>"]
DTrack["Dependency-Track<br/>(optionnel)<br/><i>Analyse vulnérabilités</i>"]
end
Browser -->|HTTP :4000| Web
GitCLI -->|SSH :2222| SshGuard
SshGuard -->|"AcceptOutcome::Accepted"| SSH
GitCLI -->|HTTPS :4000| Web
Main --> Web
Main --> SSH
Main --> SshGuard
Main --> Hooks
Main -.->|"si CI_ENABLED"| CiWorker
Web --> Core
Web --> Git
Web -.->|"admin ACL/ban"| SshGuard
SSH --> Core
SSH --> Git
SSH --> SshGuard
SshGuard --> Core
Hooks --> Core
Core --> RW
Core --> PG
Git --> FS
CiWorker --> Core
CiWorker -->|"dagger call"| Dagger
Dagger -->|"Easy Mode"| CiEngine
Dagger -->|"Power Mode"| FS
CiWorker -.->|"si CI_SBOM_ENABLED"| Syft
Syft -.->|"si CI_DTRACK_ENABLED"| DTrack
Démarrage de l’application
Le diagramme suivant détaille la séquence d’initialisation depuis main() jusqu’au démarrage des serveurs HTTP et SSH.
sequenceDiagram
participant Main as main()
participant Builder as RustwardenBuilder
participant App as RustwardenApp
participant Mig as AppMigrator
participant DB as PostgreSQL
participant SMTP as Serveur SMTP
Main->>Main: GitrustConfig::from_env()
Main->>Main: Arc<GitrustHooks> (hooks FS)
Main->>Builder: builder().from_env()
Note over Builder: Charge .env, init tracing
Main->>Builder: .headless().auto_migrate(false)
Main->>Builder: .merge_routes(app_routes(hooks))
Note over Builder: Routes = framework API<br/>+ pages SSR gitrust<br/>+ Extension(hooks)
Main->>Builder: .build().await
Builder->>DB: Connexion pool
Builder->>DB: Migrations rustwarden-core
Builder->>SMTP: EmailQueueProcessor.start()
Note over SMTP: Worker de fond : dépile<br/>email_queue toutes les 30s
Builder-->>Main: RustwardenApp
Main->>Mig: run_migrations(app.database())
Mig->>DB: core_migrations() + gitrust_migrations()
Note over DB: Tables: users, resources,<br/>repositories, ssh_keys,<br/>teams, team_members,<br/>team_repository_access
Mig-->>Main: Ok
Main->>Main: tokio::spawn(SSH server :2222)
alt CI_ENABLED=true
Main->>Main: CiWorker::start(config.ci)
Note over Main: Vérifie dagger dans PATH<br/>Si CI_SBOM_ENABLED: vérifie syft<br/>Si CI_DTRACK_ENABLED: vérifie API DT
Main->>Main: tokio::spawn(CiWorker loop)
Note over Main: Écoute mpsc channel<br/>pour les jobs CI
end
Main->>App: app.run().await
Note over App: HTTP :4000 démarre
Principes de conception
Séparation framework / métier gitrust
Gitrust est construit au-dessus de rustwarden-core, un framework Rust d’authentification et de gestion d’utilisateurs. Cette séparation est intentionnelle :
- rustwarden-core gère : auth JWT, sessions, users, roles génériques, i18n, SMTP, OAuth, audit log générique.
- gitrust-core gère : tout ce qui est spécifique à la forge Git — dépôts, clés SSH, équipes, permissions RBAC à 4 niveaux, CI, import de dépôts.
La règle fondamentale : ne jamais modifier crates/rustwarden-core/. Toute extension passe par des wrappers, des hooks (RustwardenHooks) ou des traits implémentés côté gitrust.
Mode headless
gitrust-web opère en mode headless() : les pages UI standard de rustwarden (login, register, settings) sont désactivées et réimplémentées entièrement dans gitrust-web/templates/. Cela permet un design cohérent (sidebar Git, DaisyUI) sans compromis avec l’UI générique du framework.
SSR + HTMX, zéro CDN
L’interface est rendue côté serveur via Askama (templates Rust compilés). HTMX gère les interactions dynamiques (mises à jour partielles de page, SSE pour les notifications et les logs CI). Aucun asset externe : tout CSS/JS est servi depuis static/ — imposé par la Content Security Policy du framework.
Bare repos sur le système de fichiers
gitrust-git est découplé de la base de données. Les opérations Git (navigation d’arbre, lecture de blobs, pack protocol) travaillent directement sur les bare repos ({GIT_REPOS_BASE_PATH}/{owner}/{slug}.git/) via libgit2. La base ne stocke que les métadonnées (nom, description, visibilité, ownership). Ce découplage permet de remplacer ou tester gitrust-git indépendamment.
CI hybride : Dagger comme abstraction d’exécution
Plutôt que d’implémenter un runner CI from scratch, gitrust délègue l’exécution à Dagger, qui garantit l’isolation (containers), la reproductibilité (cache) et la portabilité (local ou runner distant via SSH+rsync). Le CiWorker est une tâche Tokio dans le processus principal — pas un daemon séparé — ce qui simplifie le déploiement (un seul binaire).
Pour aller plus loin
- Référence structurelle complète (tables de modules, routes, dépendances) : Architecture des crates
- Diagrammes de séquence détaillés (clone SSH, push, permissions) : Flux de requêtes
- Conception de la couche de durcissement SSH : Conception de ssh-guard et Crate gitrust-ssh-guard
- Règles de code et gates QA : Règles QA et conformité ANSSI
Flux de requêtes
Diagrammes de séquence couvrant les opérations principales de gitrust : création de dépôt, clone et push SSH, enregistrement de clé SSH, résolution des permissions, navigation web, inscription utilisateur.
1. Création d’un dépôt (Web)
sequenceDiagram
actor User as Utilisateur
participant Web as gitrust-web
participant RepoSvc as RepositoryService
participant ResSvc as ResourceService
participant DB as PostgreSQL
participant Git as gitrust-git
participant FS as Système de fichiers
User->>Web: POST /new {name, description}
Web->>Web: Extraire user_id (session JWT)
Web->>RepoSvc: create(db, owner_id, username, base_path, input)
RepoSvc->>RepoSvc: RepoSlug::from_name(input.name)
RepoSvc->>DB: SELECT ... WHERE owner_id AND slug
DB-->>RepoSvc: None (pas de doublon)
RepoSvc->>RepoSvc: compute_disk_path(base, owner, slug)
Note over RepoSvc: Validation anti path-traversal
RepoSvc->>ResSvc: register(db, "repository", repo_id, owner_id)
ResSvc->>DB: INSERT INTO resources
DB-->>ResSvc: Ok
RepoSvc->>DB: INSERT INTO repositories
DB-->>RepoSvc: repository::Model
RepoSvc-->>Web: RepositoryOutput
Web->>Git: BareRepo::init(disk_path)
Git->>FS: git init --bare
FS-->>Git: Ok
Git-->>Web: Ok
Web-->>User: 302 Redirect /{owner}/{slug}
2. Clone SSH (git clone)
sequenceDiagram
actor Dev as Développeur
participant Client as Git CLI
participant SSH as gitrust-ssh
participant Auth as SshKeyService
participant Access as AccessService
participant Git as gitrust-git
participant DB as PostgreSQL
participant FS as Bare repo (.git)
Dev->>Client: git clone ssh://git@host:2222/alice/my-repo.git
Client->>SSH: Connexion TCP :2222
SSH-->>Client: Banner SSH + échange clés
Client->>SSH: Auth publickey (signature)
SSH->>Auth: find_by_fingerprint(db, fingerprint)
Auth->>DB: SELECT FROM ssh_keys WHERE fingerprint = ?
DB-->>Auth: ssh_key::Model {user_id, ...}
Auth-->>SSH: user_id
SSH->>Auth: update_last_used(db, key_id)
Auth->>DB: UPDATE ssh_keys SET last_used_at = now()
SSH-->>Client: Auth OK
Client->>SSH: exec "git-upload-pack 'alice/my-repo.git'"
SSH->>SSH: CommandHandler::parse("git-upload-pack alice/my-repo.git")
Note over SSH: Extraction owner=alice, repo=my-repo
SSH->>Access: effective_role(db, user_id, repo_id)
Access->>DB: Check ownership + shares + teams
DB-->>Access: Role::Developer
Access-->>SSH: can_read = true
SSH->>Git: pack_protocol::advertise_refs(repo_path)
Git->>FS: Lecture refs
FS-->>Git: refs/heads/main, ...
Git-->>SSH: advertise output
SSH-->>Client: refs advertisement
Client->>SSH: want/have negotiation
SSH->>Git: pack_protocol::serve_pack(repo_path, wants, haves)
Git->>FS: Pack objects
FS-->>Git: pack data
Git-->>SSH: pack stream
SSH-->>Client: pack data
Client-->>Dev: Dépôt cloné avec succès
3. Push SSH (git push)
sequenceDiagram
actor Dev as Développeur
participant Client as Git CLI
participant SSH as gitrust-ssh
participant Access as AccessService
participant Git as gitrust-git
participant Hooks as gitrust-hooks
participant DB as PostgreSQL
participant FS as Bare repo (.git)
Note over Client,SSH: Session SSH déjà authentifiée
Client->>SSH: exec "git-receive-pack 'alice/my-repo.git'"
SSH->>SSH: CommandHandler::parse(...)
Note over SSH: Extraction owner=alice, repo=my-repo
SSH->>Access: effective_role(db, user_id, repo_id)
Access->>DB: Check ownership + shares + teams
DB-->>Access: Role::Developer
Access-->>SSH: can_push = true
SSH->>Git: pack_protocol::advertise_refs(repo_path)
Git->>FS: Lecture refs
Git-->>SSH: refs advertisement
SSH-->>Client: refs
Client->>SSH: pack data + commands
SSH->>Git: pack_protocol::serve_pack(repo_path, receive-pack)
Git->>FS: Écriture objets + update refs
FS-->>Git: Ok
Git-->>SSH: Ok
SSH->>DB: UPDATE repositories SET is_empty = false, updated_at = now()
SSH-->>Client: Push OK
Client-->>Dev: Push réussi
4. Enregistrement d’une clé SSH
sequenceDiagram
actor User as Utilisateur
participant Web as gitrust-web
participant Svc as SshKeyService
participant DB as PostgreSQL
User->>Web: POST /settings/keys {title, key_data}
Web->>Web: Extraire user_id (session)
Web->>Svc: create(db, user_id, input)
Svc->>Svc: parse_public_key(key_data)
Note over Svc: Extraire type + décoder base64
Svc->>Svc: validate_key_type_and_size(type, bytes)
Note over Svc: ed25519 OK<br/>RSA >= 4096 bits<br/>ecdsa-p256/p384 OK
Svc->>Svc: compute_fingerprint(bytes)
Note over Svc: SHA256(key_bytes) -> base64-no-pad
Svc->>DB: SELECT FROM ssh_keys WHERE fingerprint = ?
DB-->>Svc: None (pas de doublon)
Svc->>DB: INSERT INTO ssh_keys
DB-->>Svc: ssh_key::Model
Svc-->>Web: SshKeyOutput
Web-->>User: 200 Clé ajoutée (fingerprint affiché)
5. Résolution des permissions (AccessService)
sequenceDiagram
participant Caller as Handler / SSH Session
participant AS as AccessService
participant RS as ResourceService
participant DB as PostgreSQL
Caller->>AS: effective_role(db, user_id, repo_id)
AS->>RS: effective_permission(db, user_id, "repository", repo_id)
RS->>DB: SELECT FROM resources WHERE type AND id
DB-->>RS: resource {owner_id, is_public}
alt user_id == owner_id
RS-->>AS: "owner"
else Share exists
RS->>DB: SELECT FROM resource_shares WHERE resource AND user
DB-->>RS: share {permission_level}
RS-->>AS: permission_level
else Resource is public
RS-->>AS: "read"
else No access
RS-->>AS: None
end
AS->>AS: Role::from_permission(individual_level)
AS->>DB: SELECT team_members + team_repository_access<br/>WHERE user_id AND repository_id
DB-->>AS: Vec<team_permission>
AS->>AS: team_role = max(team_permissions)
AS->>AS: effective = max(individual_role, team_role)
AS-->>Caller: Role (Reader / Developer / Maintainer / Owner)
6. Navigation dans un dépôt (Web)
sequenceDiagram
actor User as Utilisateur
participant Web as gitrust-web
participant Ext as RepoExtractor
participant Access as AccessService
participant Git as gitrust-git
participant DB as PostgreSQL
participant FS as Bare repo
User->>Web: GET /alice/my-repo/tree/main/src
Web->>Ext: RepoContext::extract(owner=alice, repo=my-repo, ref=main, path=src)
Ext->>DB: SELECT user WHERE username = 'alice'
DB-->>Ext: user {id}
Ext->>DB: SELECT repository WHERE owner_id AND slug
DB-->>Ext: repository::Model
Ext->>Access: effective_role(db, visitor_id, repo_id)
Access-->>Ext: Role::Reader
Ext-->>Web: RepoContext {repo, role, ref, path}
Web->>Git: tree_browser::list_tree(repo_path, "main", "src")
Git->>FS: Resolve ref main -> commit -> tree -> src/
FS-->>Git: Vec<TreeEntry>
Git-->>Web: entries (fichiers + dossiers)
Web->>Git: readme::find_readme(repo_path, "main", "src")
Git->>FS: Cherche README.md, README, readme.md
FS-->>Git: Option<BlobContent>
Git-->>Web: Option<readme_content>
Web->>Web: Render template tree.html
Web-->>User: HTML (arbre + README)
7. Inscription utilisateur + création répertoire
sequenceDiagram
actor User as Nouvel utilisateur
participant Web as rustwarden-core (auth)
participant Hook as GitrustHooks
participant DB as PostgreSQL
participant FS as Système de fichiers
User->>Web: POST /register {username, email, password}
Web->>DB: INSERT INTO users
DB-->>Web: user {id, username}
Web->>Hook: on_user_registered(db, user_id, username)
Hook->>Hook: Valider username (anti path-traversal)
Hook->>FS: create_dir_all(repos_base_path / username)
FS-->>Hook: Ok
Hook-->>Web: Ok
Web-->>User: 302 Redirect /login
Comprendre le modèle de permissions RBAC de gitrust
Ce que vous allez comprendre
- Analyser comment les 4 rôles (Reader, Developer, Maintainer, Owner) s’appliquent à un dépôt.
- Évaluer la différence entre accès direct et accès via équipe, et comment le rôle effectif est calculé.
- Comparer le modèle gitrust avec les ACL Unix (user/group/other).
Le problème concret
Vous avez un dépôt privé alice/myproject. Alice souhaite que :
- Bob puisse lire le code (pas pusher).
- L’équipe
backend-devspuisse pusher. - Charlie, membre de
backend-devs, soit quand même restreint en lecture seule.
Sans un modèle de permissions précis, impossible de savoir quel accès Charlie obtient réellement.
L’analogie
Le modèle gitrust ressemble aux ACL Unix, mais à deux dimensions.
Sur Unix, chaque fichier a trois niveaux : user (propriétaire), group, other. Le niveau effectif est le premier qui correspond, dans cet ordre.
Gitrust fonctionne de manière similaire, avec deux axes :
| Axe Unix | Axe gitrust |
|---|---|
user | accès individuel direct (resource_shares) |
group | accès via équipe (team_repository_access) |
other | accès public (resources.is_public) |
La différence clé : gitrust prend le maximum entre l’accès individuel et l’accès équipe, pas le premier correspondant.
Le modèle
Les 4 rôles et leur hiérarchie
graph LR
R[Reader] --> D[Developer]
D --> M[Maintainer]
M --> O[Owner]
| Rôle | Permissions cumulatives |
|---|---|
| Reader | Clone, browse (lecture seule) |
| Developer | Reader + push sur branches non protégées |
| Maintainer | Developer + gestion dépôt (settings, collaborateurs, labels, protection de branches) |
| Owner | Maintainer + suppression, transfert |
La hiérarchie est strictement cumulative : un Maintainer a toutes les permissions d’un Developer, qui a toutes celles d’un Reader.
Sources d’accès
Un utilisateur peut accéder à un dépôt par trois chemins indépendants :
flowchart TD
User[Utilisateur]
subgraph "Accès individuel"
Owner["Owner (resources.owner_id)"]
Share["resource_shares\n(read / write / admin)"]
Public["Accès public\n(resources.is_public = true)"]
end
subgraph "Accès équipe"
TeamMember["team_members\n(l'utilisateur est membre)"]
TeamAccess["team_repository_access\n(permission de l'équipe sur le dépôt)"]
end
Effective["Rôle effectif\n= max(individuel, équipe)"]
User --> Owner
User --> Share
User --> Public
User --> TeamMember
TeamMember --> TeamAccess
Owner -->|"owner (full)"| Effective
Share -->|"read/write/admin"| Effective
Public -->|"read only"| Effective
TeamAccess -->|"read/write/admin"| Effective
Calcul du rôle effectif
Le rôle effectif d’un utilisateur sur un dépôt est le maximum de tous ses accès :
rôle_effectif = max(
owner_directement ? owner : none,
resource_shares.permission_level,
is_public ? read : none,
max(team_repository_access.permission pour chaque équipe dont il est membre)
)
Exemple — réponse au problème concret :
| Utilisateur | Accès individuel | Accès via équipe | Rôle effectif |
|---|---|---|---|
| Alice | owner | — | owner |
| Bob | read (partagé directement) | — | read |
| Charlie | read (partagé directement) | write (via backend-devs) | write (max) |
| Dave (membre backend-devs) | — | write | write |
Charlie obtient write car le max(read, write) = write. Pour restreindre Charlie en lecture seule malgré son appartenance à backend-devs, il faudrait le retirer de l’équipe ou utiliser une protection de branche.
Arbre de décision
flowchart TD
Q1{L'utilisateur\nest-il owner ?}
Q1 -->|Oui| Full[Accès owner\n(toutes permissions)]
Q1 -->|Non| Q2{resource_shares\nexiste pour cet utilisateur ?}
Q2 -->|Oui| Q3{Dépôt public ?}
Q2 -->|Non| Q3
Q3 -->|Oui| Q4[Accès public = read]
Q3 -->|Non| Q4b[Pas d'accès public]
Q4 --> Q5{Membre d'une équipe\navec accès au dépôt ?}
Q4b --> Q5
Q5 -->|Oui| Calc["Rôle = max(share, public, team)"]
Q5 -->|Non| Calc2["Rôle = max(share, public)"]
Calc --> Result[Rôle effectif final]
Calc2 --> Result
Mapping vers les niveaux ResourceService
ResourceService (rustwarden-core) utilise les niveaux "read", "write", "admin". Le mapping gitrust est :
| Niveau ResourceService | Rôle gitrust |
|---|---|
"read" | Reader |
"write" | Developer |
"admin" | Maintainer |
owner (ownership directe) | Owner |
Alternatives et compromis
Pourquoi pas un modèle RBAC pur avec des rôles nommés par dépôt ? Gitea/Forgejo utilisent des rôles nommés (reporter, developer, maintainer). Gitrust utilise des niveaux numériques (read < write < admin) héritables, ce qui simplifie le calcul du maximum mais rend impossible l’attribution de permissions non hiérarchiques (ex : « peut créer des issues mais pas pusher »).
Pourquoi pas des ACL par fichier ? La granularité fichier (comme dans Perforce) crée une complexité de maintenance considérable pour des équipes de 3-20 personnes. Les protections de branches couvrent 90 % des besoins de contrôle granulaire.
Vérifier votre compréhension
-
Alice est owner de
myrepo. Elle partage le dépôt avec Bob enread. L’équipeopsa accès enadmin. Bob rejointops. Quel est le rôle effectif de Bob ? -
Un visiteur anonyme accède à un dépôt dont
resources.is_public = true. Peut-il pusher ? Pourquoi ?
Pour aller plus loin
- Référence ResourceService
- Schéma base de données — team_repository_access
- Gérer les équipes et permissions
Comprendre le fonctionnement du 2FA TOTP dans gitrust
Ce que vous allez comprendre
- Décrire le flux cryptographique TOTP (RFC 6238) de la génération du secret à la vérification du code.
- Analyser les choix de sécurité : chiffrement du secret, codes de secours, challenges temporaires.
- Évaluer pourquoi le 2FA ne protège pas contre le vol de session et comment gitrust atténue ce risque.
Le problème concret
Alice utilise un mot de passe fort. Mais si sa base de mots de passe est compromise, ou si un attaquant intercepte son mot de passe (phishing), il peut se connecter à son compte gitrust et pousser du code malveillant. Un second facteur garantit que la possession du mot de passe seul ne suffit pas.
L’analogie
TOTP ressemble à un générateur de codes de distributeur bancaire qui change toutes les 30 secondes. La banque et le distributeur partagent un secret commun (la clé de votre carte). À partir de ce secret et de l’heure actuelle, les deux parties calculent indépendamment le même code. Personne n’a besoin de transmettre le code sur le réseau.
La différence avec un SMS OTP : le secret n’est jamais transmis après la configuration initiale. Un attaquant qui intercepte votre réseau ne voit jamais le code à venir.
Le modèle
Génération du secret (RFC 6238)
sequenceDiagram
participant User as Utilisateur
participant Server as Serveur gitrust
participant App as App authenticator
User->>Server: POST /settings/security/2fa/setup
Server->>Server: Générer secret aléatoire 20 octets (CSPRNG)
Server->>Server: Chiffrer AES-256-GCM → stocker dans user_totp
Server-->>User: { secret_base32, otpauth_uri, qr_code_data_uri }
User->>App: Scanner le QR code
App->>App: Stocker le secret localement
User->>Server: POST /settings/security/2fa/verify { code: "123456" }
Server->>Server: Déchiffrer secret, calculer TOTP, vérifier fenêtre ±1
Server-->>User: { backup_codes: [...] } — activé
L’URI OTPAuth encode tout ce qu’une app authenticator a besoin :
otpauth://totp/gitrust:alice?secret=JBSWY3DPEHPK3PXP&issuer=gitrust&algorithm=SHA1&digits=6&period=30
Calcul du code TOTP
TOTP est HOTP (RFC 4226) avec un compteur basé sur le temps :
counter = floor(unix_timestamp / 30)
hmac = HMAC-SHA1(secret, counter_as_8_bytes_big_endian)
offset = hmac[19] & 0x0f
code = (hmac[offset..offset+4] & 0x7fffffff) % 1_000_000
Le code est valide pendant 30 secondes. Gitrust accepte une fenêtre de tolérance ±1 période (90 secondes totales) pour compenser les légères dérives d’horloge entre l’appareil de l’utilisateur et le serveur.
Flux de login avec 2FA activé
sequenceDiagram
participant C as Client
participant S as Serveur
C->>S: POST /api/v1/auth/login { username, password }
S->>S: verify_credentials() → OK
S->>S: is_totp_enabled(user_id) → true
S->>S: create_challenge(user_id) → challenge_token (5 min, max 5 tentatives)
S-->>C: { requires_2fa: true, challenge_token: "abc..." }
C->>S: POST /api/v1/auth/2fa/verify { challenge_token, code }
S->>S: validate_challenge() → non expiré, < 5 tentatives
S->>S: verify_code() ou verify_backup_code()
S->>S: consume_challenge()
S-->>C: { access_token, refresh_token, user }
Le challenge expire après 5 minutes. Maximum 5 tentatives par challenge. Un challenge échoué 5 fois force une nouvelle authentification complète.
Stockage sécurisé du secret
Le secret TOTP n’est jamais stocké en clair en base de données. Il est chiffré avec AES-256-GCM via CryptoService avant insertion dans user_totp.encrypted_secret.
#![allow(unused)]
fn main() {
// À l'activation
let encrypted = CryptoService::encrypt(&secret_base32)?;
// → stocké : "enc:base64(nonce||ciphertext)"
// À la vérification
let secret = CryptoService::decrypt(&user_totp.encrypted_secret)?;
}
La clé de chiffrement est ENCRYPTION_KEY (32 octets, variable d’environnement). Si elle est absente, le TOTP ne peut pas être activé.
Codes de secours
À l’activation du 2FA, 10 codes de secours de 8 caractères alphanumériques sont générés. Ils sont stockés hachés bcrypt dans user_totp.backup_codes_json (JSON chiffré).
Chaque code est à usage unique : une fois utilisé, il est marqué consommé. L’utilisateur peut régénérer un nouveau jeu de 10 codes depuis /settings/security.
Format : XXXXXXXX (ex : A3B7K2M9) — reconnaissable par sa longueur (8 chars vs 6 chiffres pour TOTP).
La détection est automatique dans verify_backup_code : si le code contient des lettres ou fait 8 caractères, c’est un code de secours ; sinon c’est un code TOTP.
Force 2FA par l’administrateur
Un administrateur peut forcer le 2FA sur toute l’instance via AppSettingsService::set_setting("force_2fa_enabled", "true"). Quand ce flag est actif :
- Les utilisateurs sans 2FA sont redirigés vers la page de configuration à chaque login.
- Les endpoints API retournent
403avec{"error":"totp_required"}tant que le 2FA n’est pas configuré.
Alternatives et compromis
Pourquoi TOTP et pas WebAuthn/passkeys ? WebAuthn offre une protection supérieure contre le phishing (le domaine est lié cryptographiquement). Gitrust prévoit de l’ajouter en phase future. TOTP a été choisi pour sa compatibilité universelle (toutes les apps authenticator, hors connexion, pas de dépendance matérielle).
Pourquoi pas SMS OTP ? Les SMS OTP sont vulnérables au SIM swapping et à l’interception SS7. Gitrust ne les implémente pas.
La fenêtre de ±1 période réduit-elle la sécurité ? Marginalement. Elle augmente la fenêtre d’attaque de 30 à 90 secondes, mais un code TOTP reste à 6 chiffres (1/1 000 000 de probabilité de deviner), et les 5 tentatives max par challenge rendent le brute-force infaisable.
Vérifier votre compréhension
-
Un utilisateur est dans un fuseau horaire décalé de 45 secondes par rapport au serveur. Sa tentative de connexion TOTP échoue systématiquement. Pourquoi, et comment diagnostiquer ?
-
L’
ENCRYPTION_KEYdu serveur change (rotation). Que se passe-t-il pour les utilisateurs ayant déjà activé le 2FA ? Quelle procédure de migration faudrait-il mettre en place ?
Pour aller plus loin
Comprendre la hiérarchie des labels à deux niveaux
Ce que vous allez comprendre
- Analyser pourquoi gitrust utilise deux types de labels indépendants plutôt qu’un seul système plat.
- Évaluer l’impact du filtrage combinatoire sur la recherche d’issues.
- Décrire comment l’UI différencie visuellement les deux types.
Le problème concret
Sur une forge Git classique, les labels forment une liste plate par dépôt. Quand vous avez 50 dépôts avec chacun leurs propres labels bug, feature, documentation, vous ne pouvez pas filtrer « toutes les issues de type bug dans tous mes dépôts » sans dupliquer les labels à la main dans chaque dépôt.
De plus, deux équipes qui utilisent le même dépôt veulent des tags libres pour annoter leurs domaines (auth, ci, parser) sans que ces tags exigent une validation préalable par un mainteneur.
L’analogie
Pensez aux tags Flickr à deux niveaux : un album appartient à une catégorie fixe définie par l’organisateur (Voyage, Famille, Travail), et peut recevoir des tags libres ajoutés par quiconque (paris, 2026, coucher-de-soleil). Les deux systèmes coexistent sans se mélanger. La catégorie donne la structure ; les tags libres donnent la granularité.
Le modèle
Deux types, deux scopes
graph TB
subgraph "Classification labels (scope owner)"
C1[bug]
C2[feature]
C3[documentation]
C4[security]
end
subgraph "Subject labels (scope dépôt)"
S1[auth]
S2[parser]
S3[ci-dagger]
S4[performance]
end
subgraph "Issue #42"
I1["bug (classification)"]
I2["auth (subject)"]
end
C1 --> I1
S1 --> I2
| Caractéristique | Classification | Subject |
|---|---|---|
| Scope | Owner (tous ses dépôts) | Dépôt unique |
| Qui crée | Owner, Maintainer uniquement | Tout utilisateur connecté |
| Création inline | Non (CRUD dédié /labels) | Oui (champ autocomplete dans l’issue) |
| Style visuel | Badge plein (fond coloré) | Badge outline (contour coloré) |
| Couleur | Définie par l’auteur | Défaut #6b7280, personnalisable |
| Colonne DB | owner_id non-null, repository_id null | repository_id non-null, owner_id null |
Filtrage combinatoire
L’intérêt principal des classification labels est le filtrage cross-dépôt. Sur le tableau de bord d’Alice (/alice) :
Filtrer par classification: bug
→ Affiche toutes les issues "bug" dans tous les dépôts d'Alice
(alice/myrepo, alice/api-client, alice/cli-tool)
Un subject label seul ne permet pas ce filtrage car il est scopé à un seul dépôt.
La combinaison des deux offre un filtrage à deux dimensions :
bug (classification) × auth (subject)
→ Toutes les issues bug liées à l'authentification
Assignation sur une issue
La page de détail d’une issue présente deux blocs distincts :
Bloc classification (géré par Owner/Maintainer uniquement) :
- Checkboxes pour les labels de classification de l’owner
- Bouton « Save labels »
- Envoyé dans
label_ids[]au POST/{o}/{r}/issues/{num}/labels
Bloc subject tags (géré par l’auteur ou Owner/Maintainer) :
- Champ texte avec autocomplete HTMX (déclenché après 3 caractères, délai 300ms)
- Tab sur une suggestion → sélectionne le label existant
- Tab sur du texte libre → crée le label subject à la soumission (
find_or_create_subject) - Badges removable (×)
- Envoyé dans
subject_names(comma-separated) au même endpoint
Schéma DB
-- Un seul champ label_type discrimine les deux cas
labels (
id UUID PK,
owner_id UUID NULL, -- non-null pour classification
repository_id UUID NULL, -- non-null pour subject
name VARCHAR(50),
color VARCHAR(7),
description VARCHAR(255) NULL,
label_type VARCHAR(20), -- 'classification' ou 'subject'
...
)
-- Index d'unicité composite
UNIQUE(owner_id, repository_id, name, label_type)
La contrainte d’unicité garantit qu’un owner ne peut pas avoir deux labels classification bug, et qu’un dépôt ne peut pas avoir deux subject tags auth.
Alternatives et compromis
Pourquoi pas un seul système de tags libres ? Les tags libres offrent une flexibilité maximale mais aucune standardisation cross-dépôt. Deux développeurs utilisant bug et Bug créent deux labels distincts.
Pourquoi pas des labels en arbre (hiérarchiques) ? Un arbre de labels (comme les tags JIRA avec sous-catégories) serait plus expressif mais beaucoup plus complexe à afficher dans des badges inline. La combinaison classification × subject couvre 95 % des besoins à coût d’implémentation minimal.
Compromis choisi : la classification donne la rigueur (contrôlée par les mainteneurs, partagée cross-dépôt) ; le subject donne la liberté (créé par n’importe qui, spécifique au dépôt). Chacun fait ce que l’autre ne fait pas.
Vérifier votre compréhension
-
Un développeur crée un subject tag
bugdans son dépôt. L’owner a aussi un classification labelbug. Comment l’UI les distingue-t-elle visuellement ? Peuvent-ils coexister sur la même issue ? -
Alice crée un classification label
securitysur son compte. Elle a 15 dépôts. Sans aucune action supplémentaire, dans combien de dépôts ce label est-il disponible ?
Pour aller plus loin
Comprendre la CI Dagger : Easy Mode vs Power Mode
Ce que vous allez comprendre
- Analyser pourquoi Dagger a été choisi comme moteur CI plutôt qu’une CI YAML classique.
- Comparer Easy Mode (
.gitrust-ci.yml) et Power Mode (.dagger/) et évaluer quand basculer de l’un à l’autre. - Décrire les garanties d’isolation et de reproductibilité offertes par Dagger.
Le problème concret
Une CI traditionnelle (GitHub Actions, GitLab CI) exécute des étapes dans un environnement partagé. Deux problèmes fréquents :
- Manque de reproductibilité : « ça passe sur ma machine mais pas en CI » parce que les versions d’outils diffèrent.
- Couplage fort : un pipeline GitLab CI est indissociable de GitLab. Migrer vers une autre plateforme = réécrire tous les pipelines.
L’analogie
Dagger, c’est comme Docker pour les pipelines CI : chaque étape s’exécute dans un container isolé, avec des dépendances déclarées et des caches portables. Le pipeline peut tourner identiquement sur votre machine, sur le serveur gitrust, ou sur n’importe quel autre runner qui a Docker installé.
L’analogie avec Docker/Kubernetes est directe :
- Docker résout « ça marche sur ma machine » pour les applications.
- Dagger résout « ça marche sur ma machine » pour les pipelines.
Le modèle
Architecture gitrust CI
flowchart LR
Push[git push] --> Hook[receive-pack\nhook]
Hook --> Detect{Détection\nfichier CI}
Detect -->|".gitrust-ci.yml"| Easy[Easy Mode\nDagger générique]
Detect -->|".dagger/"| Power[Power Mode\nModule utilisateur]
Detect -->|Aucun| None[Pas de CI]
Easy --> Worker[CI Worker\nSémaphore N]
Power --> Worker
Worker --> Dagger[dagger call ...]
Dagger --> Logs[Logs DB + SSE]
Logs --> UI[Page pipeline\n/{owner}/{repo}/ci]
La détection est prioritaire : si .dagger/ et .gitrust-ci.yml coexistent, le Power Mode prend la priorité.
Easy Mode — .gitrust-ci.yml
Un fichier YAML simple décrit le pipeline. Le moteur Dagger générique de la plateforme l’interprète.
# .gitrust-ci.yml — exemple pour un projet Rust
build:
command: cargo build --release
cache:
- path: ~/.cargo/registry
key: cargo-registry
test:
command: cargo test --workspace
requires_build: true
checks:
- name: fmt
command: cargo fmt --all -- --check
- name: clippy
command: cargo clippy --workspace -- -D warnings
Avantages :
- Accessible à tout utilisateur sans connaissance de Dagger.
- Standardisé : tous les projets Easy Mode ont la même structure.
- Zéro configuration côté plateforme.
Limites :
- Pas de logique conditionnelle complexe.
- Pas d’accès au Daggerverse (modules Dagger communautaires).
- Pas de tests unitaires du pipeline lui-même.
- Pas de composition (appeler un module depuis un autre).
Power Mode — .dagger/
Le dépôt contient un vrai module Dagger (Go, Python, ou TypeScript) :
.dagger/
├── dagger.json # manifest du module
└── main.go # fonctions Dagger
// .dagger/main.go — exemple Go
package main
import (
"dagger/gitrust-ci/internal/dagger"
)
type GitrustCi struct{}
func (m *GitrustCi) Test(ctx context.Context, source *dagger.Directory) (string, error) {
return dag.Container().
From("rust:1.77-slim").
WithMountedDirectory("/src", source).
WithWorkdir("/src").
WithExec([]string{"cargo", "test", "--workspace"}).
Stdout(ctx)
}
Gitrust exécute : dagger call -m .dagger/ test --source=.
Avantages :
- Logique conditionnelle arbitraire (Go/Python/TS complet).
- Accès au Daggerverse : réutiliser des modules existants (Helm, SBOM, Trivy…).
- Tests unitaires du pipeline possible (fonctions Dagger sont du vrai code).
- Composition : un module peut appeler un autre module.
- Isolation totale garantie par les containers Dagger.
- Type-safety : le compilateur Go/TS vérifie les appels.
Limites :
- Courbe d’apprentissage : il faut écrire du code Go/Python/TS.
- Chaque équipe maintient son propre module.
Quand basculer de Easy Mode vers Power Mode ?
flowchart TD
Q1{Pipeline simple ?\nbuild + test + lint}
Q1 -->|Oui| Easy[Rester en Easy Mode]
Q1 -->|Non| Q2{Logique conditionnelle ?\nex: déployer seulement sur main}
Q2 -->|Oui| Power[Passer en Power Mode]
Q2 -->|Non| Q3{Composition avec\nd'autres modules Dagger ?}
Q3 -->|Oui| Power
Q3 -->|Non| Q4{Tests du pipeline\nlui-même ?}
Q4 -->|Oui| Power
Q4 -->|Non| Easy
Règle pratique : si votre .gitrust-ci.yml commence à ressembler à un programme avec des if/else imbriqués, c’est le moment de passer en Power Mode.
Tableau comparatif
| Critère | Easy Mode (YAML) | Power Mode (Dagger natif) |
|---|---|---|
| Accessibilité | Excellent | Courbe d’apprentissage |
| Puissance | Limitée | Illimitée |
| Testabilité du pipeline | Nulle | Totale |
| Composition (Daggerverse) | Non | Oui |
| Portabilité | Via le moteur gitrust | Native (tout runner Dagger) |
| Alignement philosophie Dagger | Partiel (YAML = anti-pattern) | Total |
| Maintenance plateforme | Le moteur évolue | Chaque repo est autonome |
Position philosophique de Dagger
Dagger a été créé explicitement contre le modèle YAML des CI traditionnelles par l’équipe fondatrice de Docker. Le YAML Easy Mode de gitrust est un compromis assumé : on sacrifie la pureté philosophique pour l’accessibilité des 90 % de cas simples. Le YAML Easy Mode ne doit jamais essayer de tout couvrir — dès que la complexité augmente, le Power Mode est la bonne réponse.
Alternatives et compromis
Pourquoi pas GitHub Actions ou GitLab CI directement ? Ces systèmes sont couplés à leur plateforme. Un pipeline GitHub Actions ne tourne pas localement sans outil tiers (act). Dagger tourne identiquement en local, sur le runner gitrust, et sur GitHub Actions si nécessaire.
Pourquoi pas Jenkins/Tekton ? L’objectif de gitrust est l’auto-hébergement simple pour des équipes de 3-20 personnes. Ces outils ajoutent une complexité d’administration disproportionnée.
Vérifier votre compréhension
-
Un développeur a un
.gitrust-ci.ymlavec 15 étapes et desskip_ifconditionnels simulés via des scripts shell. Doit-il rester en Easy Mode ? Justifiez. -
Quelle propriété de Dagger garantit qu’un pipeline Power Mode qui passe en local passera aussi sur le runner gitrust ? Comment cette propriété est-elle implémentée techniquement ?
Pour aller plus loin
Comprendre les décisions UI : SSR, HTMX et DaisyUI
Ce que vous allez comprendre
- Analyser pourquoi gitrust utilise le rendu côté serveur (SSR) plutôt qu’un framework JavaScript front-end.
- Évaluer comment HTMX ajoute de l’interactivité sans introduire de build front-end.
- Décrire les contraintes qui ont orienté le choix de DaisyUI comme bibliothèque de composants.
Le problème concret
Une forge Git moderne doit proposer des interfaces réactives : autocomplétion, chargement partiel, mises à jour en temps réel (logs CI, progression d’import). Les approches habituelles imposent soit un SPA React/Vue avec son pipeline de build, soit un back-end qui sert une API JSON distincte de ses vues.
Pour une équipe de 3 personnes maintenant un outil en Rust, les coûts de ces approches sont disproportionnés :
- Un SPA impose une deuxième codebase (TypeScript), deux langages de test, deux pipelines CI.
- Un back-end JSON-only perd les avantages du rendu serveur (SEO, accessibilité, temps de chargement initial).
L’analogie
SSR + HTMX fonctionne comme un serveur HTML des années 2000 avec superpouvoirs : le serveur produit du HTML complet que le navigateur affiche directement (SSR classique), mais HTMX permet au navigateur de remplacer des fragments de la page sans recharger le tout. C’est la différence entre remplacer une page entière de livre et coller un post-it sur un paragraphe.
DaisyUI est la couche de présentation : elle apporte des composants CSS cohérents (boutons, badges, modales) sans JavaScript, uniquement via des classes Tailwind.
Le modèle
Architecture UI de gitrust
flowchart LR
Browser[Navigateur]
Axum[Axum SSR\nAskama templates]
HTMX[htmx.js\n~14 KB gzippé]
DaisyUI[DaisyUI + Tailwind\nbundle CSS < 300 KB]
AlpineJS[Alpine.js\n~15 KB gzippé]
Browser -->|"GET /alice/myrepo"| Axum
Axum -->|"HTML complet"| Browser
Browser -->|"hx-get=/fragment"| Axum
Axum -->|"Fragment HTML"| Browser
Browser --- HTMX
Browser --- DaisyUI
Browser --- AlpineJS
Aucun CDN externe. Tous les assets sont servis localement depuis static/. Le navigateur ne fait jamais de requête vers un domaine tiers.
Rendu côté serveur — Askama
Gitrust utilise Askama, un moteur de templates Rust compilé. Les templates sont vérifiés au moment de la compilation : une clé manquante ou un type incompatible est une erreur de build, pas une erreur à l’exécution.
#![allow(unused)]
fn main() {
// crates/gitrust-web/src/templates.rs
#[derive(Template)]
#[template(path = "repository/show.html")]
pub struct RepositoryShowTemplate {
pub repo: RepositoryView,
pub branches: Vec<BranchSummary>,
pub current_user: Option<UserView>,
pub csrf_token: String,
}
}
Le handler construit la struct, passe-la au template, et Askama sérialise le HTML :
#![allow(unused)]
fn main() {
async fn repo_show(/* ... */) -> impl IntoResponse {
let tmpl = RepositoryShowTemplate {
repo: repo_view,
branches,
current_user: user.map(|u| u.into()),
csrf_token: generate_csrf(&session),
};
Html(tmpl.render().unwrap())
}
}
Avantage clé : le compilateur Rust garantit que chaque champ exposé au template existe et a le bon type. Impossible d’afficher un champ None non géré.
HTMX — interactivité sans JavaScript custom
HTMX étend HTML avec des attributs hx-*. Le navigateur envoie des requêtes HTTP et remplace des fragments DOM avec la réponse.
Autocomplétion des subject labels
<!-- templates/issues/partials/subject_tags.html -->
<input
type="text"
name="tag_query"
hx-get="/{{ owner }}/{{ repo }}/labels/search"
hx-trigger="input changed delay:300ms, keyup[key=='Tab']"
hx-target="#tag-suggestions"
hx-swap="innerHTML"
placeholder="Ajouter un tag..."
/>
<ul id="tag-suggestions"></ul>
Le handler correspondant retourne un fragment HTML, pas du JSON :
#![allow(unused)]
fn main() {
async fn search_subject_labels(
Path((owner, repo)): Path<(String, String)>,
Query(params): Query<HashMap<String, String>>,
State(db): State<DatabaseConnection>,
) -> impl IntoResponse {
let query = params.get("tag_query").map(|s| s.as_str()).unwrap_or("");
let labels = LabelService::search_subject_labels(&db, &repo_id, query).await?;
let tmpl = LabelSuggestionsTemplate { labels };
Html(tmpl.render().unwrap())
}
}
Logs CI en temps réel (SSE)
Pour les logs de pipeline qui s’affichent en continu, gitrust utilise les Server-Sent Events exposés par Axum, consommés via hx-ext="sse" :
<div
hx-ext="sse"
sse-connect="/{{ owner }}/{{ repo }}/ci/{{ pipeline_id }}/stream"
sse-swap="message"
hx-swap="beforeend"
id="log-output"
>
</div>
Le serveur envoie des événements data: <ligne HTML>\n\n. HTMX les append dans #log-output sans aucun JavaScript custom.
DaisyUI — composants CSS, zéro JavaScript
DaisyUI est une bibliothèque de composants construite sur Tailwind CSS. Ses composants (bouton, badge, dropdown, modal) s’activent uniquement via des classes CSS et des attributs HTML natifs (<details>, <input type="checkbox">).
<!-- Badge classification (fond plein) -->
<span class="badge badge-primary">{{ label.name }}</span>
<!-- Badge subject (contour) -->
<span class="badge badge-outline" style="border-color: {{ label.color }}">
{{ label.name }}
</span>
<!-- Dropdown natif DaisyUI -->
<details class="dropdown">
<summary class="btn btn-sm">Assignees</summary>
<ul class="dropdown-content menu bg-base-100 rounded-box z-[1] w-52 p-2 shadow">
{% for collaborator in collaborators %}
<li><a hx-post="..." hx-vals='{"user_id": "{{ collaborator.id }}"}'>
{{ collaborator.username }}
</a></li>
{% endfor %}
</ul>
</details>
Aucun composant DaisyUI ne nécessite d’initialisation JavaScript. Les modales utilisent <input type="checkbox" id="modal-toggle"> avec CSS :checked pour afficher/masquer.
Alpine.js — état local léger
Pour les rares cas nécessitant de l’état côté client (toggle d’un panneau, validation inline), gitrust utilise Alpine.js (~15 KB) plutôt que React ou Vue.
<!-- Toggle d'un panneau de settings -->
<div x-data="{ open: false }">
<button @click="open = !open">Paramètres avancés</button>
<div x-show="open" x-transition>
<!-- contenu du panneau -->
</div>
</div>
Alpine.js est réservé aux comportements purement visuels sans appel réseau. Dès qu’un appel serveur est nécessaire, c’est HTMX qui prend le relais.
Budget bundle et contraintes
| Asset | Taille gzippée |
|---|---|
| htmx.min.js | ~14 KB |
| alpine.min.js | ~15 KB |
| gitrust.css (DaisyUI + Tailwind purged) | ~280 KB |
| Total | < 310 KB |
Le CSS est généré lors du build via npx tailwindcss --input ... --output ... --minify. Le résultat est commité dans static/css/gitrust.css pour éviter une dépendance Node.js en production. La reconstruction est déclenchée uniquement quand les templates changent (gate QA : make css-rebuild).
Quand utiliser chaque outil
flowchart TD
Q1{Interaction nécessite\nun appel serveur ?}
Q1 -->|Oui| Q2{Contenu retourné ?}
Q1 -->|Non| Alpine[Alpine.js\nx-data, x-show, @click]
Q2 -->|Fragment HTML| HTMX[HTMX\nhx-get / hx-post]
Q2 -->|Flux continu| SSE[HTMX + SSE\nhx-ext='sse']
Q2 -->|JSON uniquement| Fetch[fetch() natif\n(rare, éviter)]
Alternatives et compromis
Pourquoi pas React/Vue/Svelte ? Ces frameworks nécessitent un pipeline de build (Vite, Webpack), une API JSON séparée, et des compétences TypeScript en plus de Rust. Pour une forge auto-hébergée ciblant 3-20 développeurs, ce coût n’est pas justifié.
Pourquoi pas Inertia.js (SSR + SPA hybride) ? Inertia demande un adaptateur côté serveur. Il n’existe pas d’adaptateur Rust/Axum stable. De plus, Inertia reste JavaScript-first.
Pourquoi pas Turbo (Hotwire) ? Turbo est excellent mais son modèle Frame/Stream est plus complexe que les attributs HTMX pour des équipes qui démarrent. HTMX est plus proche de HTML pur.
Compromis assumé : HTMX ne peut pas gérer des interactions très complexes (éditeur de code en ligne, diagrammes interactifs). Si gitrust devait ajouter un éditeur Monaco, un composant React isolé serait envisageable — mais ce n’est pas une décision prise aujourd’hui.
Vérifier votre compréhension
-
Un développeur propose d’ajouter une feature de « preview Markdown en temps réel » dans l’éditeur de description d’issue. Il suggère d’installer React juste pour ce composant. Quelles sont les alternatives possibles avec la stack existante ? Évaluez leurs compromis.
-
La page
/alice/myrepo/issuescharge 100 issues et est lente. Un développeur propose de migrer vers une SPA React avec pagination côté client. Quelle solution HTMX permettrait d’obtenir le même résultat sans changer de stack ?
Pour aller plus loin
Comprendre le patron worker async de gitrust
Ce que vous allez comprendre
- Analyser pourquoi les opérations longues (import, export, envoi de mails) ne peuvent pas s’exécuter dans le cycle requête/réponse HTTP.
- Décrire comment le patron worker async de gitrust combine
mpsc,Semaphore, et SSE pour traiter des tâches longues avec retour de progression. - Évaluer les garanties de durabilité offertes par le modèle « état en base + canal en mémoire ».
Le problème concret
L’import d’un dépôt GitHub peut prendre 30 secondes. Une requête HTTP a un timeout. Si le serveur redémarre pendant l’import, la tâche est perdue. Si 50 utilisateurs lancent un import simultanément sans limite, le serveur sature.
Le même problème se pose pour l’envoi de mails, la génération de SBOM, ou toute opération dont la durée dépasse quelques secondes.
L’analogie
Le patron ressemble à une cuisine de restaurant avec un tableau de commandes :
- Le serveur (handler HTTP) prend la commande, l’inscrit sur le tableau (DB), et dit au client « votre numéro est le 42, suivez l’avancement ici ».
- La cuisine (worker) lit le tableau en continu, choisit la prochaine commande, la prépare derrière un comptoir limité à N plans de travail (Semaphore).
- Le tableau lumineux (SSE) indique l’état en temps réel au client sans qu’il ait besoin de redemander.
Si la cuisine ferme brutalement (crash), les commandes « en cours » sur le tableau sont remises en attente au redémarrage — elles ne disparaissent pas.
Le modèle
Vue d’ensemble du patron
flowchart LR
Handler[Handler HTTP\nPOST /import]
DB[(Table\nimport_jobs)]
Channel[mpsc::channel\nJobId]
Worker[NotifyWorker\ntokio::spawn]
Semaphore[Semaphore\nmax N slots]
Executor[execute_job()\nlogique métier]
SSE[SSE Handler\nGET /import/:id/stream]
Client[Navigateur\nhx-ext='sse']
Handler -->|"INSERT status=Pending"| DB
Handler -->|"tx.send(job_id)"| Channel
Channel -->|"rx.recv()"| Worker
Worker -->|"acquire()"| Semaphore
Semaphore -->|"permit"| Executor
Executor -->|"UPDATE status=..."| DB
DB -->|"SELECT"| SSE
SSE -->|"data: <html>"| Client
La table de persistance
Chaque type de job a sa propre table. Le schéma canonique pour un import :
CREATE TABLE import_jobs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
repository_id UUID REFERENCES repositories(id),
status VARCHAR(20) NOT NULL DEFAULT 'pending',
-- 'pending' | 'running' | 'completed' | 'failed'
source_url TEXT NOT NULL,
error_message TEXT,
progress_pct SMALLINT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ
);
Invariant fondamental : le statut en base est la source de vérité. Si le processus redémarre, un job running sans process vivant est remis à pending au démarrage.
L’état en mémoire : le canal mpsc
#![allow(unused)]
fn main() {
// crates/gitrust-web/src/workers/import_worker.rs
use tokio::sync::mpsc;
#[derive(Clone)]
pub struct ImportJobSender(pub mpsc::Sender<Uuid>);
pub struct ImportWorker {
db: DatabaseConnection,
rx: mpsc::Receiver<Uuid>,
semaphore: Arc<Semaphore>,
}
impl ImportWorker {
pub fn new(db: DatabaseConnection) -> (Self, ImportJobSender) {
let (tx, rx) = mpsc::channel(100); // buffer de 100 IDs en attente
let semaphore = Arc::new(Semaphore::new(4)); // max 4 imports simultanés
let worker = Self { db, rx, semaphore };
(worker, ImportJobSender(tx))
}
pub async fn run(mut self) {
while let Some(job_id) = self.rx.recv().await {
let db = self.db.clone();
let permit = Arc::clone(&self.semaphore)
.acquire_owned()
.await
.expect("semaphore closed");
tokio::spawn(async move {
let _permit = permit; // libéré quand le spawn se termine
if let Err(e) = execute_import(&db, job_id).await {
tracing::error!(?job_id, error = %e, "import job failed");
let _ = ImportJobRepository::mark_failed(&db, job_id, &e.to_string()).await;
}
});
}
}
}
}
Le Semaphore borne la concurrence. Même si 100 IDs arrivent dans le canal, seuls 4 execute_import s’exécutent en parallèle. Les autres attendent leur tour dans le canal.
Le handler HTTP — découpler la réponse de l’exécution
#![allow(unused)]
fn main() {
// crates/gitrust-web/src/routes/import.rs
pub async fn post_import(
State(db): State<DatabaseConnection>,
State(sender): State<ImportJobSender>,
user: AuthUser,
Form(form): Form<ImportForm>,
) -> Result<impl IntoResponse, AppError> {
// 1. Valider et persister
let job = ImportJobRepository::create(&db, user.user_id, &form.source_url).await?;
// 2. Envoyer l'ID dans le canal (non-bloquant grâce au buffer)
sender.0.send(job.id).await.map_err(|_| AppError::Internal("worker unavailable".into()))?;
// 3. Répondre immédiatement avec la page de suivi
Ok(Redirect::to(&format!("/import/{}/progress", job.id)))
}
}
Le handler retourne avant que l’import ait commencé. Le navigateur est redirigé vers une page de progression.
Le worker — exécution avec transitions d’état
#![allow(unused)]
fn main() {
async fn execute_import(db: &DatabaseConnection, job_id: Uuid) -> anyhow::Result<()> {
// Transition : pending → running
ImportJobRepository::mark_running(db, job_id).await?;
// Récupérer le job
let job = ImportJobRepository::find(db, job_id).await?
.ok_or_else(|| anyhow!("job not found: {}", job_id))?;
// Étape 1/3 — cloner
ImportJobRepository::update_progress(db, job_id, 10).await?;
let temp_dir = git_clone(&job.source_url).await?;
// Étape 2/3 — créer le dépôt gitrust
ImportJobRepository::update_progress(db, job_id, 50).await?;
let repo = RepositoryService::create_from_import(db, &job, &temp_dir).await?;
// Étape 3/3 — finaliser
ImportJobRepository::update_progress(db, job_id, 90).await?;
finalize_import(db, job.id, repo.id).await?;
// Transition : running → completed
ImportJobRepository::mark_completed(db, job_id, repo.id).await?;
Ok(())
}
}
Chaque update_progress écrit en base. Si le process redémarre entre deux étapes, la progression est connue — mais le job doit être relancé depuis zéro (pas de reprise partielle dans ce modèle).
SSE — retour de progression vers le navigateur
#![allow(unused)]
fn main() {
// crates/gitrust-web/src/routes/import.rs
pub async fn stream_import_progress(
Path(job_id): Path<Uuid>,
State(db): State<DatabaseConnection>,
user: AuthUser,
) -> Sse<impl Stream<Item = Result<Event, Infallible>>> {
let stream = async_stream::stream! {
loop {
let job = ImportJobRepository::find(&db, job_id).await
.unwrap_or(None);
match job {
None => {
yield Ok(Event::default().data("<p>Job introuvable.</p>"));
break;
}
Some(ref j) if j.status == "completed" => {
yield Ok(Event::default()
.data(format!("<p>Import terminé — <a href='/{}/{}'>Voir le dépôt</a></p>",
j.owner_login, j.repo_slug)));
break;
}
Some(ref j) if j.status == "failed" => {
yield Ok(Event::default()
.data(format!("<p class='text-error'>Erreur : {}</p>",
j.error_message.as_deref().unwrap_or("inconnue"))));
break;
}
Some(ref j) => {
yield Ok(Event::default()
.data(format!("<progress class='progress' value='{}' max='100'></progress>",
j.progress_pct)));
tokio::time::sleep(Duration::from_millis(500)).await;
}
}
}
};
Sse::new(stream).keep_alive(KeepAlive::default())
}
}
Le template HTMX consomme ce flux :
<!-- templates/import/progress.html -->
<div
hx-ext="sse"
sse-connect="/import/{{ job_id }}/stream"
sse-swap="message"
hx-swap="innerHTML"
id="progress-container"
>
<progress class="progress" value="0" max="100"></progress>
</div>
Récupération au redémarrage
Au démarrage de l’application, gitrust remet en pending les jobs bloqués en running :
#![allow(unused)]
fn main() {
// crates/gitrust-web/src/main.rs (dans la fonction de démarrage)
async fn recover_stuck_jobs(db: &DatabaseConnection, sender: &ImportJobSender) {
let stuck = ImportJobRepository::find_by_status(db, "running").await
.unwrap_or_default();
for job in stuck {
tracing::warn!(?job.id, "recovering stuck import job");
let _ = ImportJobRepository::reset_to_pending(db, job.id).await;
let _ = sender.0.send(job.id).await;
}
}
}
Cette fonction s’exécute une fois au démarrage, avant que le serveur HTTP accepte des connexions.
Enregistrement dans main.rs
#![allow(unused)]
fn main() {
// crates/gitrust-web/src/main.rs
let (import_worker, import_sender) = ImportWorker::new(db.clone());
// Démarrer le worker dans un task dédié
tokio::spawn(async move { import_worker.run().await });
// Récupérer les jobs bloqués
recover_stuck_jobs(&db, &import_sender).await;
// Injecter le sender dans l'état Axum
let app = Router::new()
// ... routes ...
.with_state(AppState { db, import_sender, /* ... */ });
}
Décisions et compromis
Durabilité : base de données vs queue dédiée
| Approche | Durabilité | Complexité opérationnelle |
|---|---|---|
| Table SQL (gitrust) | Au niveau PostgreSQL | Nulle (déjà présent) |
| Redis + Bull/Sidekiq | Haute (AOF/RDB) | Redis à opérer |
| RabbitMQ / NATS | Très haute | Broker supplémentaire |
| In-memory seulement | Nulle (perdu au restart) | Nulle |
Gitrust choisit la table SQL : la durabilité est suffisante (perte maximale = 1 transaction), et PostgreSQL est déjà requis. Un broker externe serait disproportionné pour une forge auto-hébergée à petite échelle.
Concurrence : Semaphore vs pool de workers
Le Semaphore(N) est plus simple qu’un pool de threads ou de tâches fixes. Il permet au runtime Tokio de gérer l’ordonnancement. L’inconvénient : si un job prend 30 secondes et que N=4, les 5e et 6e jobs attendent dans le canal, pas dans un pool observable.
Pour des jobs très longs (> 5 minutes), un pool de workers avec heartbeat serait plus robuste — mais ce cas ne se présente pas dans gitrust aujourd’hui.
SSE vs WebSocket vs polling
| Méthode | Cas d’usage |
|---|---|
Polling HTMX (hx-trigger="every 2s") | Statut simple, peu de mises à jour |
| SSE (gitrust) | Flux unidirectionnel continu (logs, progression) |
| WebSocket | Bidirectionnel (chat, collaboration temps réel) |
SSE est suffisant pour tous les workers actuels de gitrust (import, CI logs). WebSocket ajouterait une complexité d’état bidirectionnel non nécessaire.
Vérifier votre compréhension
-
Le serveur gitrust redémarre pendant l’import d’un dépôt. Le job était en statut
runningavecprogress_pct = 60. Que se passe-t-il exactement au redémarrage ? L’utilisateur verra-t-il la progression reprendre à 60 % ou à 0 % ? -
Le
Semaphoreest initialisé à 4 slots. 10 utilisateurs lancent un import simultanément. Combien detokio::spawns’exécutent en parallèle ? Où attendent les 6 jobs restants ? Que se passe-t-il si le serveur redémarre à cet instant ?
Pour aller plus loin
Conception de ssh-guard
Ce que vous allez comprendre
- Identifier les défauts de sécurité SSH de gitrust avant l’introduction de ssh-guard et le périmètre exact que cette nouvelle crate couvre.
- Analyser le design async/event-driven choisi : pourquoi un listener wrapper, pourquoi des détecteurs push-based, pourquoi le store hybride.
- Évaluer les compromis (fail-open, dry-run, idempotence, priorité ACL) et savoir comment les ajuster.
Public : contributeurs au code de
gitrust-sshetgitrust-ssh-guard. Pour une recette d’exploitation, voir Configurer ssh-guard.
1. Le problème concret
Avant ssh-guard, le serveur SSH russh intégré à gitrust avait quatre angles morts opérationnels :
- IP cliente faussée derrière nginx stream. Le déploiement type (« nginx en frontal sur
:22,proxy_protocol on;vers127.0.0.1:2222») rendaitpeer_addr() = 127.0.0.1côté gitrust. Conséquence : tous les rate-limits par IP étaient inutilisables, les logs n’identifiaient personne, fail2ban était impossible à brancher. - Aucune détection de motifs d’attaque. La couche
russhrejette une auth invalide, mais ne corrèle rien : 1 000 tentatives en 30 secondes ressemblent à 1 000 lignes de log indépendantes. - Format de logs instable. Les messages d’erreur de
russhévoluent entre versions, ce qui rendait les filtres fail2ban fragiles (cassés à chaque mise à jour). - Pas de levier admin. Aucune ACL CIDR (allowlist d’un VPN, denylist d’un AS abusif) ne pouvait être appliquée à l’extérieur du firewall système, donc inopérante pour la majorité des admins.
ssh-guard adresse exactement ces quatre points — pas plus. Toute la couche TLS, OAuth, JWT, RBAC reste hors de son périmètre.
2. L’analogie
Imaginez une porte d’immeuble (le TcpListener) et derrière elle un agent d’accueil (russh) qui contrôle les badges. ssh-guard est le sas qui s’intercale entre les deux. Il fait trois choses, dans cet ordre :
- Lire la pièce d’identité correctement, qu’elle soit présentée directement ou tendue par un coursier (le proxy) — c’est la résolution d’IP réelle.
- Consulter une liste blanche/noire affichée à l’intérieur du sas — c’est l’ACL admin.
- Compter les visiteurs récents par identité et activer une alarme si quelqu’un sonne 5 fois en 5 minutes — c’est la détection.
Le sas n’inspecte pas les badges (c’est le rôle de russh). Il décide juste qui a le droit d’arriver jusqu’à l’agent d’accueil.
3. Le modèle
Vie d’une connexion
flowchart TD
A[Connexion TCP entrante] --> B[peer_addr du socket]
B --> C{proxy_config présent ?}
C -- Non, profil direct --> E[ip_réelle = peer_addr]
C -- Oui, profil proxifié --> D{peer_addr dans<br/>trusted_proxies ?}
D -- Non --> X1[Drop : UntrustedProxy]
D -- Oui --> P[Parsing PROXY v1/v2]
P -- OK --> E2[ip_réelle = ip_cliente du header]
P -- Échec strict --> X2[Drop : ProxyHeaderInvalid/Missing]
P -- Échec souple --> E3[fallback peer_addr + warn]
E --> F[BanManager.effective_status]
E2 --> F
E3 --> F
F --> G{ACL Deny ?}
G -- Oui --> X3[Drop : Banned]
G -- Non --> H{Ban auto actif ?}
H -- Oui --> X3
H -- Non --> I{ACL Allow ?}
I -- Oui --> J[Bypass flood,<br/>Accept]
I -- Non --> K{Flood actif ?}
K -- Oui : check_key OK --> L[Accept]
K -- Oui : refusé --> X4[Drop : FloodLimit]
K -- Non --> L
L --> M[ClientIdentity créé,<br/>handshake russh]
M --> N[Tentatives auth]
N --> O[AuthTracker.record_auth_attempt]
O --> O1[Persiste dans store]
O --> O2[Émet event JSON sur sink]
O --> O3[Notifie chaque détecteur]
O3 --> P1[BruteForce / UserEnum / KeyScan]
P1 --> Q{Seuil atteint ?}
Q -- Oui --> R[BanManager.auto_ban]
R --> S[Insert ban + event IpBanned]
Q -- Non --> Z[Fin]
Trois plans qui interagissent
| Plan | Composants | Caractéristique principale |
|---|---|---|
| Synchrone, hot path | SecureListener.accept, BanManager.effective_status, ConnectionFloodDetector.check | Lock-free (DashMap, GCRA), aucun appel DB tant que le store est memory ou hybrid |
| Asynchrone, push | AuthTracker.record_auth_attempt → détecteurs → BanManager.auto_ban | Tâches Tokio courtes, jamais bloquantes |
| Persistant | HybridStore, PostgresStore | Write-through périodique, rehydrate au boot |
L’invariant clé : le hot path ne touche jamais directement la DB. Si PostgreSQL tombe, le listener continue à servir avec sa vue mémoire des bans/ACL, et la prochaine écriture admin sera mise en attente (HybridStore) ou échouera proprement (Postgres direct).
4. Les décisions de conception
4.1 Listener wrapper plutôt que middleware russh
ssh-guard intercale SecureListener entre TcpListener et russh. Trois raisons :
- Beaucoup de drops (flood, ban) doivent se faire avant d’allouer un handler
russh(RAM, threads). Un middlewarerusshcoûterait des allocations inutiles à chaque scan de port. - L’extraction d’IP via PROXY protocol nécessite de peeker les premiers octets du
TcpStreamavant querusshne le voie — un middleware de niveau session ne peut pas faire ça. - Découplage strict : ssh-guard ne dépend pas de
russh(testable en standalone, réutilisable si on remplace la lib SSH).
4.2 Push-based detectors
Chaque détecteur expose async fn on_event(event: &GuardEvent). L’AuthTracker les appelle après chaque événement. Avantages :
- Pas de scheduler de fond à entretenir. Le détecteur se réveille uniquement quand il y a quelque chose à analyser.
- Idempotence facile : un second appel après franchissement du seuil retombe sur un
auto_banno-op (déjà banni). - Test déterministe : on injecte des événements synthétiques et on observe le sink — pas de
tokio::time::sleepà attendre.
Coût : chaque détecteur fait une lecture du store pour calculer son agrégat. C’est acceptable car on ne déclenche qu’aux échecs d’auth (rare) et que l’agrégat est borné par la fenêtre.
4.3 Priorité ACL : deny > auto_ban > allow > default
L’ordre est exécuté tel quel dans BanManager.effective_status. Conséquences :
denyadmin > tout : un opérateur peut brûler une IP même si elle a été allowlistée par erreur ailleurs.auto_ban>allow: un ban auto déjà posé ne saute pas si on allowliste après coup. Pour le lever, il faut explicitementunban. C’est un choix conservateur : un allow ne doit pas effacer la trace d’un comportement passé.allow>default: une IP allowlistée bypasse les détecteurs (mais ses événements restent loggés pour audit).
L’invariant auto_ban no-op si IP allowlistée est un garde-fou de défense en profondeur : si un détecteur oublie un jour de consulter l’ACL avant d’appeler auto_ban, le BanManager refuse quand même.
4.4 Fail-open sur erreur de store
Quand BanManager.effective_status reçoit Err(StoreError), le listener log warn et laisse passer. C’est un compromis :
- Pour : une panne PostgreSQL n’arrête pas le service Git en lecture/push (les opérateurs ne sont pas réveillés à 3h du matin pour une indisponibilité partielle).
- Contre : pendant la fenêtre de panne, un attaquant pourrait passer (mais l’auth
russhreste opérationnelle, donc l’attaquant ne devient utilisateur que s’il connaît une clé valide).
Le compromis a été pris sciemment : il est documenté, et on peut le renverser en remplaçant Ok(...) par return Ok(self.drop_reason(ip, DropReason::Banned).await) dans la branche Err si une instance préfère fail-closed.
4.5 Dry-run avec émission d’événements
SSH_GUARD_DRY_RUN=true désactive la persistance des bans mais continue à émettre les événements IpBanned. C’est le mode parfait pour :
- valider un nouveau seuil en prod sans risquer de bannir un utilisateur ;
- alimenter fail2ban (qui ferait le ban réel via UFW/iptables) tout en laissant ssh-guard observer ;
- répliquer le comportement d’un détecteur dans un environnement de pré-prod.
4.6 Stockage hybride par défaut
Trois backends, mais le défaut hybrid est presque toujours le bon :
- les lectures hot path passent par DashMap (latence µs) ;
- chaque écriture est synchrone côté mémoire + asynchrone vers PostgreSQL (write-through) ;
- au boot,
rehydraterepeuple la mémoire depuis la DB en une seule transaction. Si la DB est vide ou inaccessible, on démarre avec une mémoire vide et un warn (fail-open au boot aussi).
Le mode memory est réservé aux tests et au profil private (réseau interne, on accepte de perdre l’historique au restart). Le mode postgres direct existe pour les rares cas où une instance multi-noeuds n’a pas de cache RAM cohérent et veut tout passer par la DB.
4.7 Schéma JSON stable
#[serde(tag = "event", rename_all = "snake_case")] produit {"event":"<nom>","ts":"...","ip":"...",...}. La règle inviolable :
- ajouter un nouveau variant ou un nouveau champ optionnel : OK ;
- renommer un variant ou un champ existant : breaking change → nouveau nom + period de transition.
C’est ce qui rend les filtres fail2ban et les requêtes Loki stables dans le temps. Un test (event_name_matches_serde_tag) garantit que le nom Rust et le tag JSON ne divergent pas.
5. Ce que ssh-guard ne fait pas (volontairement)
| Hors périmètre | Pourquoi | Couvert par |
|---|---|---|
| Inspection des payloads SSH | Niveau TCP uniquement, ssh-guard n’a pas la clé d’hôte | russh lui-même |
| Authentification utilisateur | Domaine de gitrust-core::SshKeyService | gitrust-ssh |
| Limites de débit applicatives (push trop gros) | Concerne le pack-protocol, pas la connexion | gitrust-git |
| Distribution multi-noeuds | Pas d’algorithme de gossip, pas de Raft | À traiter par le store backend (Postgres partagé) |
| GeoIP / blocage par pays | Décision politique, pas technique | Reverse-proxy ou firewall en amont |
6. Implications pour les contributeurs
Quand ajouter un détecteur
Si un nouveau motif d’attaque émerge (ex. : « attaques par chronométrie sur les fingerprints »), suivre ce squelette :
- Ajouter un fichier
src/detector/<nom>.rscalqué surbrute_force.rs. - Implémenter
pub async fn on_event(&self, event: &GuardEvent)qui filtre les événements pertinents et lit l’agrégat viaGuardStore. - Ajouter une variante
BanReason::<Nom>et unGuardEvent::<Nom>Detected { ... }dansevents.rs. Mettre à jourevent_nameet le testevent_name_matches_serde_tag. - Câbler le détecteur dans
runtime.rs::build(instanciation conditionnelle sithreshold.is_disabled()). - Câbler dans
tracker.rs(ajout d’unOption<Arc<NouveauDetector>>+ appel dansrecord_auth_attempt). - Documenter le nouveau seuil dans
config.rs(env var + preset par profil) et dans la page admin de référence.
Quand modifier le format d’événement
Ne pas. Si un champ doit changer de type ou de sens, créer un nouveau variant et garder l’ancien jusqu’à période de transition explicite. Le test de stabilité du tag est là pour casser la PR si quelqu’un le tente sans s’en rendre compte.
Quand changer une priorité ACL
Pratiquement jamais. La priorité actuelle (deny > auto_ban > allow > default) est un consensus défensif. Si une PR la modifie, exiger un dossier d’opportunité avec attaques traitées et nouveaux compromis acceptés.
7. Vérifier votre compréhension
- Une connexion arrive avec un en-tête PROXY v2 valide depuis
192.168.10.5. La config estSSH_GUARD_PROFILE=nginx(donctrusted_proxies=127.0.0.1/32, ::1/128). Quel est l’AcceptOutcome? Pourquoi ? - Une IP est dans la denylist admin et vient d’être bannie automatiquement pour brute force. Quel
EffectiveStatusretourneeffective_status? L’ordre des deux lookups dans le code change-t-il quelque chose ? - PostgreSQL tombe pendant 30 minutes. Une attaque brute-force démarre depuis une IP nouvelle. Que se passe-t-il : (a) au moment de l’accept, (b) au moment où le détecteur lit
count_auth_failures?
8. Pour aller plus loin
- Crate gitrust-ssh-guard — référence structurelle (modules, types, API publique)
- Architecture des crates — position dans le graphe de dépendances
- Vue d’ensemble de l’architecture — où ssh-guard s’insère dans le runtime gitrust
- ssh-guard : détection d’attaques SSH — vue admin de la chaîne de détection
- Conformité ANSSI PA-074 — directives applicables au code de la crate
Bibliothèque de templates gitrust
Bienvenue dans l’atelier gitrust. Chaque fichier ici est standalone et copiable tel quel — un fichier, un cas d’usage. Les commentaires internes suffisent à l’adapter. Pour les explications pédagogiques, référez-vous aux manuels qui pointent vers ces templates.
Convention : copiez le fichier qui correspond à votre cas, adaptez les valeurs marquées
CHANGE_ME, puis supprimez les commentaires si vous le souhaitez. Ne modifiez pas les fichiers dans ce dépôt — travaillez toujours sur une copie locale.
Fichiers d’environnement (env/)
| Fichier | Cas d’usage |
|---|---|
env/dev-local.env | Instance de développement local sur localhost:4000 — PostgreSQL local, pas de TLS, debug activé |
env/production-mono-machine.env | Production mono-machine derrière reverse-proxy — PostgreSQL, SMTP, Redis sessions, TLS délégué |
env/production-ci-heavy.env | Production avec CI runners distants intensifs — tuning pool de connexions, rate limiting ajusté |
env/tests-e2e.env | Environnement E2E Playwright — base éphémère, seed de données, SMTP mock |
env/ssh-guard-direct.env | Bloc SSH_GUARD_* pour gitrust exposé Internet sans reverse-proxy SSH (profil direct) |
env/ssh-guard-nginx.env | Bloc SSH_GUARD_* pour gitrust derrière nginx stream avec PROXY protocol v2 (profil nginx) |
Stacks Docker (docker/)
| Fichier | Cas d’usage |
|---|---|
docker/docker-compose.minimal.yml | Stack minimale : gitrust + PostgreSQL uniquement — idéale pour l’évaluation et les tutoriels |
docker/docker-compose.production.yml | Stack production : gitrust + PostgreSQL + Redis sessions + relay SMTP Postfix |
docker/docker-compose.with-ci-runner.yml | Stack production + CI runner Dagger en sidecar — pour instances avec builds fréquents |
docker/podman-quadlet.container | Alternative Podman Quadlet — déploiement rootless systemd-native sans Docker daemon |
Reverse-proxy (reverse-proxy/)
| Fichier | Cas d’usage |
|---|---|
reverse-proxy/nginx-tls.conf | Nginx avec TLS Let’s Encrypt — virtual host gitrust, headers de sécurité, proxy WebSocket SSE |
reverse-proxy/caddy-Caddyfile | Caddyfile — TLS automatique, configuration minimale, idéal pour les petites instances |
reverse-proxy/traefik-dynamic.yml | Traefik config dynamique — intégration labels Docker, middlewares rate-limit et auth |
Systemd (systemd/)
| Fichier | Cas d’usage |
|---|---|
systemd/gitrust.service | Service systemd avec hardening AppArmor/seccomp — pour déploiement binaire natif sans Docker |
systemd/gitrust-backup.timer | Timer systemd de sauvegarde quotidienne — déclenche backup.sh à 2h00 avec journalisation |
Configurations CI (.gitrust-ci.yml) (ci/)
| Fichier | Cas d’usage |
|---|---|
ci/rust.gitrust-ci.yml | Pipeline CI pour projet Rust — fmt, clippy, test, audit de dépendances, SBOM |
ci/go.gitrust-ci.yml | Pipeline CI pour projet Go — vet, staticcheck, test, build multi-arch |
ci/python.gitrust-ci.yml | Pipeline CI pour projet Python — ruff, mypy, pytest, packaging wheel |
ci/node.gitrust-ci.yml | Pipeline CI pour projet Node.js — ESLint, Vitest, build, audit npm |
ci/dagger-power-mode/dagger.json | Squelette Dagger Power Mode — module Go minimal à étendre pour pipelines complexes |
ci/dagger-power-mode/main.go | Module Dagger Go — fonctions build/test/publish prêtes à adapter |
Sécurité (security/)
| Fichier | Cas d’usage |
|---|---|
security/fail2ban-gitrust-ssh-guard.conf | Jail + filtre fail2ban consommant le flux JSON stable de gitrust-ssh-guard (durcissement SSH) |
Scripts de sauvegarde et restauration (backup/)
| Fichier | Cas d’usage |
|---|---|
backup/backup.sh | Sauvegarde complète — pg_dump + rsync des dépôts bare + copie des clés SSH host |
backup/restore.sh | Restauration complète depuis une archive de sauvegarde — avec vérification d’intégrité |
Clients API d’exemple (api-clients/)
Ces exemples illustrent l’authentification par Personal Access Token (PAT) et les opérations CRUD de base sur l’API REST gitrust v1.
| Fichier | Cas d’usage |
|---|---|
api-clients/python-create-repo.py | Client Python — créer un dépôt via l’API v1 avec httpx et PAT |
api-clients/rust-list-repos.rs | Client Rust — lister les dépôts avec pagination via reqwest et PAT |
api-clients/curl-pat-auth.sh | Shell curl — exemples d’authentification PAT, GET/POST/DELETE sur les endpoints principaux |
Scripts de migration (migration/)
| Fichier | Cas d’usage |
|---|---|
migration/from-gitea.sh | Import en masse depuis Gitea — dépôts, membres, issues via le worker d’import gitrust |
migration/from-forgejo.sh | Import en masse depuis Forgejo — même mécanique, adaptée à l’API Forgejo |
Comment utiliser ces templates depuis les manuels
Les manuels narratifs pointent vers ces fichiers depuis leurs sections how-to. Par exemple :
administration_manual/how-to/configurer-smtp.md→ « Copieztemplate/env/production-mono-machine.envpuis… »administration_manual/tutorials/01-installation-docker.md→ « La stack s’appuie surtemplate/docker/docker-compose.minimal.yml»developer_manual/how-to/implementer-endpoint-api.md→ « Testez avectemplate/api-clients/curl-pat-auth.sh»
Les templates ne contiennent pas d’explication pédagogique — tout le contexte se trouve dans les manuels.