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 |
GitRust