Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018BAUeCFpDkRD6tU5wGsc1C
175 lines
9.6 KiB
Markdown
175 lines
9.6 KiB
Markdown
# 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/`](../../infra/prod/README.md).
|
||
|
||
---
|
||
|
||
## 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.
|
||
|
||
→ **[09 — Déploiement applicatif](./09-deploiement-application.md#le-premier-administrateur)**
|
||
|
||
### 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](./13-securite-durcissement.md#rotation-obligatoire-avant-la-mise-en-production)**
|
||
|
||
### 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](./15-exploitation-incidents.md#points-de-vigilance-connus).
|
||
|
||
---
|
||
|
||
## Chronologie
|
||
|
||
| Quand | Étape | Durée | Document |
|
||
|---|---|---|---|
|
||
| **J-14** | Comptes, domaine, outils, décisions | 2-3 h | [01](./01-prerequis.md) |
|
||
| **J-10** | Serveurs, réseau, firewalls (Terraform) | 1 h | [02](./02-provisioning-hetzner.md) |
|
||
| **J-10** | Durcissement système des deux serveurs | 1 h | [03](./03-durcissement-serveurs.md) |
|
||
| **J-9** | PostgreSQL + Redis + sauvegardes | 2 h | [04](./04-noeud-donnees.md) |
|
||
| **J-8** | Cluster k3s | 1 h 30 | [05](./05-cluster-k3s.md) |
|
||
| **J-8** | Secrets SOPS | 1 h | [06](./06-secrets-sops.md) |
|
||
| **J-7** | Stockage objet | 45 min | [07](./07-stockage-objet-s3.md) |
|
||
| **J-7** | DNS, Cloudflare, TLS | 1 h 30 | [08](./08-dns-tls-cloudflare.md) |
|
||
| **J-6** | Premier déploiement applicatif | 2 h | [09](./09-deploiement-application.md) |
|
||
| **J-5** | CI/CD automatisée | 1 h 30 | [10](./10-cicd-github-actions.md) |
|
||
| **J-4** | Supervision et alertes | 2 h | [11](./11-observabilite.md) |
|
||
| **J-3** | **Test de restauration réel** | 2 h | [12](./12-sauvegardes-restauration.md) |
|
||
| **J-2** | Revue de sécurité, rotation des secrets | 3 h | [13](./13-securite-durcissement.md) |
|
||
| **J-1** | Répétition générale, go / no-go | 2 h | [14](./14-runbook-go-live.md) |
|
||
| **J0** | Ouverture | — | [14](./14-runbook-go-live.md) |
|
||
| **après** | Exploitation, incidents, montée en charge | — | [15](./15-exploitation-incidents.md) |
|
||
| **après** | RGPD et conformité | — | [16](./16-rgpd-conformite.md) |
|
||
|
||
**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](./01-prerequis.md) | Comptes, outils, domaine, clés SSH, décisions à trancher |
|
||
| 02 | [Provisioning Hetzner](./02-provisioning-hetzner.md) | Terraform, serveurs, réseau privé, firewalls, volume |
|
||
| 03 | [Durcissement des serveurs](./03-durcissement-serveurs.md) | SSH, UFW, fail2ban, auditd, mises à jour automatiques |
|
||
| 04 | [Nœud de données](./04-noeud-donnees.md) | PostgreSQL 15 + TLS, Redis 7, WAL-G, timers de sauvegarde |
|
||
| 05 | [Cluster k3s](./05-cluster-k3s.md) | Installation durcie, Traefik, cert-manager, kubeconfig |
|
||
| 06 | [Secrets SOPS](./06-secrets-sops.md) | Clé age, chiffrement, application, rotation, sauvegarde de la clé |
|
||
| 07 | [Stockage objet](./07-stockage-objet-s3.md) | Hetzner Object Storage, buckets, clés cloisonnées |
|
||
| 08 | [DNS, TLS, Cloudflare](./08-dns-tls-cloudflare.md) | Enregistrements, proxy, WAF, certificat wildcard |
|
||
| 09 | [Déploiement applicatif](./09-deploiement-application.md) | Manifests, migrations, premier administrateur, données de référence |
|
||
| 10 | [CI/CD](./10-cicd-github-actions.md) | Secrets GitHub, environnement protégé, déploiement automatique |
|
||
| 11 | [Observabilité](./11-observabilite.md) | Loki, Prometheus, Grafana, alertes Discord, supervision externe |
|
||
| 12 | [Sauvegardes et restauration](./12-sauvegardes-restauration.md) | Stratégie 3-2-1, PITR, tests de restauration, RTO/RPO |
|
||
| 13 | [Sécurité](./13-securite-durcissement.md) | Rotation des secrets compromis, checklist, revue externe |
|
||
| 14 | [Runbook de mise en ligne](./14-runbook-go-live.md) | J-1, J0, go/no-go, plan de repli |
|
||
| 15 | [Exploitation et incidents](./15-exploitation-incidents.md) | Runbooks, points de vigilance, montée en charge, mises à jour |
|
||
| 16 | [RGPD et conformité](./16-rgpd-conformite.md) | 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
|
||
|
||
1. **Tout secret est chiffré dans Git** (SOPS + age) ou n'y est pas du tout.
|
||
2. **La base n'est jamais joignable depuis Internet.** Réseau privé, `hostssl`
|
||
uniquement, UFW, aucun enregistrement DNS.
|
||
3. **Personne ne contourne Cloudflare.**
|
||
4. **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.
|
||
5. **Une sauvegarde non restaurée n'est pas une sauvegarde.**
|
||
6. **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](./15-exploitation-incidents.md#le-site-est-hors-ligne) |
|
||
| Un déploiement a mal tourné | [15 § Retour arrière](./15-exploitation-incidents.md#retour-arriere) |
|
||
| Perte ou corruption de données | [12 § Restauration](./12-sauvegardes-restauration.md#restauration) |
|
||
| Suspicion de compromission | [13 § Réponse à incident](./13-securite-durcissement.md#reponse-a-incident) |
|
||
| Certificat expiré | [08 § Dépannage TLS](./08-dns-tls-cloudflare.md#depannage) |
|