Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018BAUeCFpDkRD6tU5wGsc1C |
||
|---|---|---|
| .. | ||
| 01-prerequis.md | ||
| 02-provisioning-hetzner.md | ||
| 03-durcissement-serveurs.md | ||
| 04-noeud-donnees.md | ||
| 05-cluster-k3s.md | ||
| 06-secrets-sops.md | ||
| 07-stockage-objet-s3.md | ||
| 08-dns-tls-cloudflare.md | ||
| 09-deploiement-application.md | ||
| 10-cicd-github-actions.md | ||
| 11-observabilite.md | ||
| 12-sauvegardes-restauration.md | ||
| 13-securite-durcissement.md | ||
| 14-runbook-go-live.md | ||
| 15-exploitation-incidents.md | ||
| 16-rgpd-conformite.md | ||
| README.md | ||
Mise en production — Xpeditis sur Hetzner
Procédure complète, pas à pas, pour ouvrir Xpeditis au public de manière sûre.
Option retenue : Hetzner auto-hébergé (feuille « Hetzner (auto-hébergé) » du
fichier Xpeditis_Previsions_Couts.xlsx), k3s sur 2 serveurs, ≈ 66 € HT/mois
d'infrastructure.
Tous les fichiers évoqués ici vivent dans infra/prod/.
Lisez ceci en premier
L'analyse du dépôt a mis au jour quatre problèmes qui auraient compromis ou cassé la production. Ils sont traités dans les fichiers livrés, mais deux exigent une action de votre part.
0. Une migration créait un ADMIN au mot de passe public — corrigé
1730000000007-SeedTestUsers insérait admin@xpeditis.com (rôle ADMIN),
manager@ et user@, tous avec le mot de passe Password123! — écrit en clair
dans le dépôt. Sur une base de production neuve, appliquer les migrations créait
donc un administrateur dont les identifiants sont publics. C'était le point le
plus grave du parcours.
Trois mécanismes, tous automatiques :
| Migration | Rôle |
|---|---|
1730000000007-SeedTestUsers (modifiée) |
Ne s'exécute plus si NODE_ENV=production. Les comptes ne sont jamais créés. Dev et preprod gardent les leurs. |
1756000000000-NeutralizeSeedAccountsInProduction |
Filet de sécurité pour toute base où ils existeraient déjà : renommage, hash inauthentifiable, désactivation. Échoue si le nettoyage est incomplet. |
1756000000001-BootstrapAdminFromEnv |
Crée votre administrateur depuis BOOTSTRAP_ADMIN_EMAIL, sans aucun mot de passe stocké : vous définissez le vôtre via « mot de passe oublié ». |
Résultat : aucun secret n'existe nulle part — ni dans Git, ni dans le Secret
Kubernetes, ni dans l'historique du shell. preflight-check.sh vérifie ensuite
en conditions réelles que Password123! est bien refusé sur les trois adresses.
1. Les secrets de preprod sont dans l'historique Git — action requise
infra/preprod/docker-stack.preprod.yml est versionné et contient en clair :
mot de passe PostgreSQL, mot de passe Redis, JWT_SECRET, identifiants MinIO,
clé SMTP Brevo, clés Stripe de test, mot de passe administrateur Grafana.
Ils sont dans l'historique Git : les réécrire ne suffirait pas, il faut les
considérer comme compromis et les révoquer. La clé Brevo en particulier est
un identifiant tiers actif : quiconque a lu le dépôt peut envoyer des e-mails
au nom de noreply@xpeditis.com.
→ 13-securite-durcissement.md § Rotation obligatoire
2. L'image frontend ne peut pas être promue depuis la preprod — corrigé
next.config.js fige NEXT_PUBLIC_API_URL au moment du build. L'ancien
cd-main.yml re-taguait l'image de preprod vers la production : l'application
en production aurait appelé api.preprod.xpeditis.com. Le workflow réécrit
reconstruit le frontend avec les URLs de production et vérifie que l'URL de
preprod n'est pas présente dans le bundle avant de déployer.
3. Les migrations partaient en concurrence — corrigé
L'image backend lance les migrations à chaque démarrage de pod
(scripts/setup/startup.js). Avec deux replicas, deux processus migrent en même
temps ; TypeORM ne sérialise pas entre processus, l'un des deux part en
CrashLoopBackOff. Un Job Kubernetes (parallélisme 1) applique désormais les
migrations avant la mise à jour des images ; startup.js ne fait plus que
constater qu'il n'y a rien à migrer. Aucune modification du code applicatif.
Un quatrième point ne bloque pas le lancement mais dégrade une fonctionnalité : les notifications temps réel. Voir 15-exploitation-incidents.md § Points de vigilance.
Chronologie
| Quand | Étape | Durée | Document |
|---|---|---|---|
| J-14 | Comptes, domaine, outils, décisions | 2-3 h | 01 |
| J-10 | Serveurs, réseau, firewalls (Terraform) | 1 h | 02 |
| J-10 | Durcissement système des deux serveurs | 1 h | 03 |
| J-9 | PostgreSQL + Redis + sauvegardes | 2 h | 04 |
| J-8 | Cluster k3s | 1 h 30 | 05 |
| J-8 | Secrets SOPS | 1 h | 06 |
| J-7 | Stockage objet | 45 min | 07 |
| J-7 | DNS, Cloudflare, TLS | 1 h 30 | 08 |
| J-6 | Premier déploiement applicatif | 2 h | 09 |
| J-5 | CI/CD automatisée | 1 h 30 | 10 |
| J-4 | Supervision et alertes | 2 h | 11 |
| J-3 | Test de restauration réel | 2 h | 12 |
| J-2 | Revue de sécurité, rotation des secrets | 3 h | 13 |
| J-1 | Répétition générale, go / no-go | 2 h | 14 |
| J0 | Ouverture | — | 14 |
| après | Exploitation, incidents, montée en charge | — | 15 |
| après | RGPD et conformité | — | 16 |
Temps de travail cumulé : environ 25 heures. Étalez-les : plusieurs étapes comportent des délais incompressibles (propagation DNS, émission de certificat, première sauvegarde complète).
Les documents
| # | Fichier | Contenu |
|---|---|---|
| 01 | Prérequis | Comptes, outils, domaine, clés SSH, décisions à trancher |
| 02 | Provisioning Hetzner | Terraform, serveurs, réseau privé, firewalls, volume |
| 03 | Durcissement des serveurs | SSH, UFW, fail2ban, auditd, mises à jour automatiques |
| 04 | Nœud de données | PostgreSQL 15 + TLS, Redis 7, WAL-G, timers de sauvegarde |
| 05 | Cluster k3s | Installation durcie, Traefik, cert-manager, kubeconfig |
| 06 | Secrets SOPS | Clé age, chiffrement, application, rotation, sauvegarde de la clé |
| 07 | Stockage objet | Hetzner Object Storage, buckets, clés cloisonnées |
| 08 | DNS, TLS, Cloudflare | Enregistrements, proxy, WAF, certificat wildcard |
| 09 | Déploiement applicatif | Manifests, migrations, premier administrateur, données de référence |
| 10 | CI/CD | Secrets GitHub, environnement protégé, déploiement automatique |
| 11 | Observabilité | Loki, Prometheus, Grafana, alertes Discord, supervision externe |
| 12 | Sauvegardes et restauration | Stratégie 3-2-1, PITR, tests de restauration, RTO/RPO |
| 13 | Sécurité | Rotation des secrets compromis, checklist, revue externe |
| 14 | Runbook de mise en ligne | J-1, J0, go/no-go, plan de repli |
| 15 | Exploitation et incidents | Runbooks, points de vigilance, montée en charge, mises à jour |
| 16 | RGPD et conformité | Hébergement, sous-traitants, conservation, droits des personnes |
Architecture livrée
Internet → Cloudflare (WAF, anti-DDoS, cache)
│ le firewall Hetzner n'accepte 80/443 que depuis Cloudflare
▼
app-01 · CPX41 · fsn1 · k3s mono-nœud
Traefik ──► api.xpeditis.com → backend NestJS ×2 (HPA 2→4)
├► app / www / apex → frontend Next.js ×2
└► grafana.xpeditis.com (filtré par IP)
observabilité : Loki · Promtail · Prometheus · Alertmanager · Grafana
│
│ réseau privé 10.10.1.0/24, PostgreSQL en TLS obligatoire
▼
db-01 · CPX31 · fsn1 · Docker Compose
PostgreSQL 15 + WAL-G · Redis 7 · postgres-exporter
volume dédié 50 Go
│
├─► Hetzner Object Storage (documents applicatifs + WAL-G)
└─► Hetzner Storage Box (dumps logiques chiffrés age)
Principes appliqués partout
- Tout secret est chiffré dans Git (SOPS + age) ou n'y est pas du tout.
- La base n'est jamais joignable depuis Internet. Réseau privé,
hostssluniquement, UFW, aucun enregistrement DNS. - Personne ne contourne Cloudflare.
- SSH et l'API Kubernetes ne sont ouverts qu'à vos IP. La CI ouvre une fenêtre de deux minutes pour une seule IP, et la referme quoi qu'il arrive.
- Une sauvegarde non restaurée n'est pas une sauvegarde.
- Ce qui n'est pas surveillé n'existe pas. Chaque défaillance a une alerte, et le silence des sauvegardes est lui-même surveillé — de l'extérieur.
En cas de problème
| Situation | Aller directement à |
|---|---|
| Le site ne répond plus | 15 § Le site est hors ligne |
| Un déploiement a mal tourné | 15 § Retour arrière |
| Perte ou corruption de données | 12 § Restauration |
| Suspicion de compromission | 13 § Réponse à incident |
| Certificat expiré | 08 § Dépannage TLS |