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

9.4 KiB

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. 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

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

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é.

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 :

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 :

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 :

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 :

  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

# 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 sur app.xpeditis.com — visez A ou A+.


9. Dépannage

Le certificat ne s'émet pas

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 :

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.

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 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