Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 :

PlaceholderRemplacez 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

#ServiceBind interneExposition publiqueVariable
1Web HTTP (UI SSR + API)127.0.0.1:4000:443 HTTPS via NginxSERVER_PORT
2SSH Git (russh)127.0.0.1:2222:22 via Nginx streamSSH_PORT
3Worker 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érifier dig +short <your-domain>)
  • Module stream Nginx disponible (paquet libnginx-mod-stream sur 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_PASSWORD et POSTGRES_PASSWORD dans 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/config côté poste dev :

Host gitrust-host
    HostName <your-server-ip>
    User <admin-user>
    Port 2022

Le 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ômeDiagnosticFix
Lockout SSH après reload sshdConsole physique / KVM IPMI
Cert Let’s Encrypt échoue (Connection refused)dig +short <your-domain> ≠ IP publiqueCorriger 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é HTTPSgrep COOKIE_SECURE /opt/gitrust/.envDoit être true ; vérifier que Nginx forward bien X-Forwarded-Proto https
Push SSH Connection closedjournalctl -u gitrust | grep sshVé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 HTTPSTemplates qui hardcodent http://Vérifier EMAIL_BASE_URL=https://... et headers X-Forwarded-Proto
Push gros repo timeoutLogs nginx client intended to send too large bodyAugmenter client_max_body_size 2G (déjà fait pour .git/)

Récapitulatif des ports

PortProcessExpositionRôle
22nginx (stream)PublicSSH Git → forward vers :2222
80nginxPublicACME challenge + redirect HTTPS
443nginxPublicHTTPS → forward vers :4000
2022sshd systèmePublicAdmin SSH (à restreindre par firewall si possible)
2222gitrustLoopbackBackend SSH russh
4000gitrustLoopbackBackend HTTP axum
5432docker-proxyLoopbackPostgreSQL