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é¶
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 :
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
Le reste du plan utilise~/.ssh/configcôté poste dev :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 :
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)¶
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 admin/how-to/durcir-avec-fail2ban.md pour la configuration complète.
7a. Installation¶
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 :
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¶
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 |