Metti in produzione su una macchina Linux con HTTPS Let's Encrypt¶
Contesto¶
Procedura di riferimento per la distribuzione di un'istanza gitrust su una macchina Linux con IP fisso, con tratta esterna instradata e HTTPS tramite Let's Encrypt. Riproducibile e testato in condizioni reali.
Adatta i seguenti valori al tuo ambiente:
| 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 |
I tre servizi 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 = dipendenza, contenitore di loopback Docker bind 127.0.0.1:5432.
Topologia di rete¶
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
Rischio critico: ordine di esecuzione¶
Lo spostamento del sistema sshd da :22 a :2022 deve essere effettuato prima di configurare nginx stream e deve essere convalidato da una seconda sessione SSH prima di chiudere la prima. Altrimenti: blocco totale (è necessario l'accesso fisico alla console).
Prerequisiti¶
Stazione di creazione (macchina locale con codice sorgente)¶
rustup show && command -v rsync envsubst npx
# Installer si besoin : sudo apt install rsync gettext-base
Scegli come target "".¶
- Debian/Ubuntu recenti
- Accesso SSH con "sudo".
- Docker + plugin di composizione ("versione docker compose")
- Snap o adatto per il plugin
certbot+ nginx - DNS
<tuo-dominio>→ IP pubblico del ramo esterno (selezionadig +short <tuo-dominio>) - Modulo Nginx
streamdisponibile (pacchettolibnginx-mod-streamsu Debian)
Fase 0: genera .env.production (stazione di costruzione, solo una volta)¶
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.
Fase 1: preparare l'obiettivo: spostare il sistema sshd su:2022¶
Da eseguire mantenendo due sessioni SSH simultanee.
1.pre. Diagnosi della condizione iniziale¶
1.pre.1: trova la porta SSH aperta dalla build station¶
# 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 — Connettersi alla porta rilevata¶
1.pre.3 — Una volta connesso, controlla lo stato attuale di sshd¶
# 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. Sessione 1: modifica la configurazione 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. Sessione 2: convalidare la nuova porta¶
Dalla stazione di costruzione, in un altro terminale:
1c. (Più tardi, dopo la Fase 5) Chiudere la porta 22 del sistema sshd¶
Una volta che nginx stream:22 è attivo e testato:
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.
Fase 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
Sostituisci ${PG_PWD} con il valore effettivo. Verifica che la porta non sia esposta pubblicamente:
Fase 3: creazione + distribuzione gitrust tramite deploy.sh¶
3a. Preparare 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. Validazione della sola build (facoltativa)¶
3c. Distribuzione¶
# Sans argument : utilise DEPLOY_TARGET du deploy.conf (gitrust-host via ~/.ssh/config, port 2022)
./deployment/deploy.sh
Cosa fa deploy.sh: cargo build --release, tailwindcss --minify, adatta gitrust.service + .env, rsync, crea l'utente gitrust, installa systemd, riavvia.
Alla fine: gitrust ascolta su 127.0.0.1:4000 (HTTP) e 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)
Fase 4: Nginx HTTP + Crittografiamo¶
4a. Installa 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. Distribuire la configurazione 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. Emetti il certificato Let's Encrypt¶
ssh gitrust-host 'sudo certbot --nginx \
-d <your-domain> \
--email <acme-email> \
--agree-tos \
--no-eff-email \
--redirect'
4d. Controlla il rinnovo automatico¶
ssh gitrust-host 'sudo systemctl list-timers | grep certbot'
ssh gitrust-host 'sudo certbot renew --dry-run'
4°. 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
Fase 5: esporre SSH Git su :22 tramite flusso Nginx¶
5a. Aggiungi il blocco del flusso¶
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. Disabilita sshd di sistema su:22 (Fase 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 del clone di 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
Fase 6 – Controlli complessivi¶
# 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
Fase 7 – Rafforzamento: Fail2ban¶
Voir admin/how-to/durcir-avec-fail2ban.md pour la configuration complète.
7a. Facilità¶
7b. Configurazione minima rapida¶
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
Crea i filtri gitrust-login e gitrust-ssh, quindi:
Fase 8: aggiornamento a un nuovo dominio (quando il DNS è pronto)¶
8a. Aggiungi SAN al certificato esistente¶
ssh gitrust-host 'sudo certbot --nginx --expand \
-d <your-domain> \
-d <new-domain> \
--email <acme-email> \
--agree-tos --no-eff-email'
8b. Aggiorna configurazione 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. Aggiorna .env.production e ridistribuisci¶
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
Fase 9 – Ulteriori aggiornamenti¶
Risoluzione dei problemi¶
| 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/) |
Riepilogo del porto¶
| 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 |