# 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` | `` | | A | `www` | `` | | A | `app` | `` | | A | `api` | `` | | A | `grafana` | `` | **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 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@ \ > "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::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@ '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)**