xpeditis2.0/docs/mise-en-prod/08-dns-tls-cloudflare.md
David b22f4e0b74 docs: procedure de mise en production pas a pas
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018BAUeCFpDkRD6tU5wGsc1C
2026-09-07 21:40:50 +02:00

295 lines
9.4 KiB
Markdown

# 08 — DNS, TLS et Cloudflare
**Durée : environ 1 h 30**, dont des délais d'attente incompressibles.
La référence complète des réglages Cloudflare est dans
[`infra/prod/cloudflare/README.md`](../../infra/prod/cloudflare/README.md).
Ce document donne l'ordre d'exécution et les vérifications.
---
## 1. Ordre à respecter
L'ordre compte : émettre un certificat avant que le DNS ne réponde échoue, et
Let's Encrypt limite la production à **5 échecs par heure**.
```
1. Enregistrements DNS chez Cloudflare (propagation : quelques minutes)
2. Réglages SSL/TLS Cloudflare
3. Certificat de TEST (staging) ← valide la chaîne DNS-01
4. Certificat de PRODUCTION
5. Vérifications
6. Règles WAF et cache
```
---
## 2. Enregistrements DNS
```bash
cd infra/prod/terraform && terraform output dns_records_to_create
```
Créez dans Cloudflare, **tous proxifiés (nuage orange)** :
| Type | Nom | Valeur |
|---|---|---|
| A | `xpeditis.com` | `<app_public_ipv4>` |
| A | `www` | `<app_public_ipv4>` |
| A | `app` | `<app_public_ipv4>` |
| A | `api` | `<app_public_ipv4>` |
| A | `grafana` | `<app_public_ipv4>` |
**Aucun enregistrement ne pointe vers db-01.**
> Le proxy n'est pas optionnel : le firewall Hetzner n'accepte 80/443 que depuis
> les rangs Cloudflare. Un enregistrement en nuage gris (DNS only) donnerait un
> domaine injoignable — comportement voulu, mais déroutant si on l'a oublié.
### E-mail — à faire maintenant
```
TXT xpeditis.com v=spf1 include:spf.brevo.com -all
TXT mail._domainkey <clé DKIM fournie par Brevo>
TXT _dmarc v=DMARC1; p=quarantine; rua=mailto:dmarc@xpeditis.com; pct=100
```
Sans SPF/DKIM/DMARC, les confirmations de réservation et les liens magiques
transporteurs partent en indésirables. C'est un défaut de production silencieux
et coûteux : personne ne se plaint, les clients pensent simplement que le
service ne marche pas.
### Vérifier
```bash
dig +short app.xpeditis.com # doit renvoyer des IP Cloudflare (104.x, 172.6x…)
dig +short TXT xpeditis.com
dig +short TXT _dmarc.xpeditis.com
```
---
## 3. Réglages SSL/TLS Cloudflare
**SSL/TLS → Overview**
| Réglage | Valeur |
|---|---|
| Mode de chiffrement | **Full (strict)** |
| Always Use HTTPS | Activé |
| Minimum TLS Version | 1.2 |
| TLS 1.3 | Activé |
| Automatic HTTPS Rewrites | Activé |
| HSTS | **Pas encore** — voir §6 |
> **« Full (strict) » et rien d'autre.** En mode *Flexible*, Cloudflare parle en
> clair à votre serveur : le cadenas s'affiche dans le navigateur alors que le
> trafic circule en clair sur Internet. C'est pire que pas de HTTPS du tout,
> puisque personne ne s'en aperçoit.
---
## 4. Certificat de test
On valide d'abord la chaîne DNS-01 avec l'émetteur *staging*, qui n'a pas de
quota serré.
```bash
export KUBECONFIG=~/.kube/xpeditis-prod.yaml
kubectl -n xpeditis-prod patch certificate xpeditis-wildcard --type=merge \
-p '{"spec":{"issuerRef":{"name":"letsencrypt-staging","kind":"ClusterIssuer"}}}'
kubectl -n xpeditis-prod delete secret xpeditis-wildcard-tls --ignore-not-found
# Suivre l'émission
kubectl -n xpeditis-prod get certificate -w
kubectl -n xpeditis-prod describe certificaterequest
kubectl -n cert-manager logs -l app=cert-manager --tail=50 -f
```
Deux à cinq minutes (propagation de l'enregistrement TXT `_acme-challenge`).
`READY: True` signifie que le jeton Cloudflare, les permissions et la zone sont
corrects. Si l'émission échoue :
| Message | Cause |
|---|---|
| `Cloudflare API error 10000` | Jeton invalide ou permissions insuffisantes (`Zone / DNS / Edit` requis). |
| `could not find zone` | Le jeton ne couvre pas `xpeditis.com`, ou la zone n'est pas active. |
| `propagation check failed` | Attendre. Si cela persiste au-delà de 10 min, vérifier qu'aucun autre DNS ne fait autorité. |
---
## 5. Certificat de production
Une fois le test au vert :
```bash
kubectl -n xpeditis-prod patch certificate xpeditis-wildcard --type=merge \
-p '{"spec":{"issuerRef":{"name":"letsencrypt-prod","kind":"ClusterIssuer"}}}'
kubectl -n xpeditis-prod delete secret xpeditis-wildcard-tls --ignore-not-found
kubectl -n monitoring patch certificate grafana-tls --type=merge \
-p '{"spec":{"issuerRef":{"name":"letsencrypt-prod","kind":"ClusterIssuer"}}}'
kubectl -n monitoring delete secret grafana-tls --ignore-not-found
kubectl get certificate -A -w
```
Vérifiez l'émetteur réel du certificat servi :
```bash
echo | openssl s_client -connect api.xpeditis.com:443 -servername api.xpeditis.com 2>/dev/null \
| openssl x509 -noout -issuer -subject -dates
```
Attendu : `issuer=C=US, O=Let's Encrypt, CN=R11` (ou équivalent).
Si vous lisez `(STAGING)`, le certificat de test est encore en place :
supprimez le `Secret` et relancez.
> Avec Cloudflare en proxy, le navigateur voit le certificat **Cloudflare**, pas
> le vôtre. La commande ci-dessus interroge Cloudflare. Pour vérifier le
> certificat d'origine, il faut se connecter depuis app-01 :
> ```bash
> ssh deploy@<app_ip> \
> "echo | openssl s_client -connect 127.0.0.1:443 -servername api.xpeditis.com 2>/dev/null | openssl x509 -noout -issuer -dates"
> ```
---
## 6. HSTS
**À faire seulement quand tous les sous-domaines servent du HTTPS valide.**
Traefik pose déjà l'en-tête (`k8s/base/08-traefik-middlewares.yaml`,
`stsSeconds: 31536000`). Activer HSTS **aussi** chez Cloudflare le fait
appliquer avant même que la requête n'atteigne le serveur.
**SSL/TLS → Edge Certificates → HTTP Strict Transport Security → Enable**
| Réglage | Valeur |
|---|---|
| Max Age | 12 mois |
| Include subdomains | Oui |
| Preload | Oui |
| No-Sniff | Oui |
> **C'est irréversible pendant un an.** Une fois HSTS envoyé, les navigateurs
> refuseront tout accès HTTP à `xpeditis.com` et à ses sous-domaines pendant la
> durée annoncée, même si vous désactivez le réglage. Un sous-domaine qui
> n'aurait pas de certificat valide deviendrait inaccessible sans recours.
>
> Commencez par `Max Age = 1 mois`, sans *preload*. Passez à 12 mois et
> *preload* après deux semaines sans incident.
---
## 7. Règles WAF et cache
Appliquez les règles décrites dans
[`infra/prod/cloudflare/README.md`](../../infra/prod/cloudflare/README.md) :
1. **Bypass cache sur `api.xpeditis.com/*`** — la plus importante. Une réponse
d'API mise en cache servirait les données d'un utilisateur à un autre.
2. Blocage de `/api/docs`.
3. Limitation de débit sur `/api/v1/auth/login` (10 requêtes/minute/IP).
4. Restriction du webhook Stripe aux IP de Stripe.
5. Cache long sur `/_next/static/*`.
---
## 8. Vérifications finales
```bash
# Les redirections
curl -sI http://xpeditis.com | head -3 # 301 → https
curl -sI https://www.xpeditis.com | head -1 # 200
curl -sI https://app.xpeditis.com | head -1 # 200
curl -sI https://api.xpeditis.com/api/v1/health | head -1 # 200
# Les en-têtes de sécurité
curl -sI https://app.xpeditis.com | grep -iE 'strict-transport|x-frame|x-content-type|referrer-policy'
# L'API n'est pas mise en cache
curl -sI https://api.xpeditis.com/api/v1/health | grep -i 'cf-cache-status'
# Attendu : BYPASS ou DYNAMIC, jamais HIT
# L'origine est-elle contournable ?
curl -sS --max-time 5 --connect-to app.xpeditis.com:443:<app_public_ipv4>:443 \
https://app.xpeditis.com/ 2>&1 | head -2
# Attendu : timeout — le firewall Hetzner bloque tout ce qui n'est pas Cloudflare
```
Analyse externe : [ssllabs.com/ssltest](https://www.ssllabs.com/ssltest/) sur
`app.xpeditis.com` — visez A ou A+.
---
## 9. Dépannage
### Le certificat ne s'émet pas
```bash
kubectl -n xpeditis-prod describe certificate xpeditis-wildcard
kubectl -n xpeditis-prod get certificaterequest,order,challenge
kubectl -n cert-manager logs -l app=cert-manager --tail=100
```
Le `Challenge` indique précisément à quelle étape cela bloque.
### Le certificat approche de l'expiration
cert-manager renouvelle 30 jours avant l'échéance. L'alerte
`CertificatBientotExpire` se déclenche à 15 jours — c'est-à-dire uniquement si
le renouvellement automatique a cessé de fonctionner.
Forcer un renouvellement :
```bash
kubectl -n xpeditis-prod delete secret xpeditis-wildcard-tls
# cert-manager réémet dans la minute
```
### « Too many certificates already issued »
Vous avez atteint la limite Let's Encrypt (50 par domaine et par semaine).
Elle se réinitialise glissant sur 7 jours. En attendant, utilisez
`letsencrypt-staging` — et c'est exactement pourquoi la validation se fait
d'abord en staging.
### Erreur 521 / 522 chez Cloudflare
Cloudflare n'atteint pas l'origine.
```bash
ssh deploy@<app_ip> 'sudo systemctl status k3s; sudo ss -tlnp | grep -E ":(80|443) "'
kubectl -n kube-system get pods -l app.kubernetes.io/name=traefik
```
Cause fréquente : les rangs d'IP Cloudflare ont changé.
```bash
bash infra/prod/scripts/refresh-cloudflare-ips.sh
```
---
## 10. Contrôle
```
[ ] 5 enregistrements A créés, tous proxifiés
[ ] Aucun enregistrement ne pointe vers db-01
[ ] SPF, DKIM, DMARC publiés
[ ] Mode SSL : Full (strict)
[ ] Certificat de test émis avec succès (chaîne DNS-01 validée)
[ ] Certificat de production émis, Ready: True, émetteur non-staging
[ ] HTTP redirige en 301 vers HTTPS
[ ] En-têtes de sécurité présents
[ ] cf-cache-status = BYPASS sur l'API
[ ] L'origine n'est pas joignable en contournant Cloudflare
[ ] Règles WAF appliquées
[ ] Note SSL Labs ≥ A
[ ] HSTS activé (après vérification, max-age court d'abord)
```
→ **Suite : [09 — Déploiement applicatif](./09-deploiement-application.md)**