Cloudflare : configurer rapidement avec Terraform et Traefik

Cloudflare : configurer rapidement avec Terraform et Traefik

Depuis quelques semaines maintenant ce blog est derrière Cloudflare.

Dans ce billet, je vais vous expliquer pourquoi et comment j'ai configuré Cloudflare devant Traefik.

Cloudflare : Pourquoi utiliser un CDN?

Cloudflare est avant tout un CDN, un Content Delivery Network. Le rôle d'un CDN est de réduire autant que possible la latence de réponse pour l'utilisateur final, en exploitant plusieurs points.

Un maillage fort

La première partie d'un CDN est d'exploiter un réseau de serveur déployé partout dans une région ou dans le monde (en fonction des offres). Un utilisateur se connectant à mon site à donc une réponse qui lui parvient par le serveur le plus rapide pour sa localisation, très souvent le plus proche géographiquement.

Ainsi, un utilisateur aux États-Unis ne sera pas pénalisé pour afficher mon site, bien que ce dernier soit hébergé en France.

Du cache

Le second point qu'apporte un CDN est son cache. L'idée est que mon serveur est sollicité uniquement quand le cache n'a pas l'information nécessaire pour répondre. Cela permet donc de réduire drastiquement le volume de requêtes traitées par mon backend. De plus, un cache est énormément plus rapide à répondre, vu qu'il n'a pas besoin de "compiler" votre page.

Dans mon cas, j'ai réduit d'environ 70% le volume de requêtes que mon serveur traitait.

À noter qu'en fonction des CDN et des offres il existe deux "type" de cache :

  • Le "lazy loading" : dans ce modèle, le serveur attend qu'une requête soit effectuée pour mettre en cache sa réponse. Cela signifie donc que le backend reçoit au moins une requête avant sa mise en cache
  • Le "prefetch loading" : Ici, l'idée est de "préchauffer" le cache en chargeant en amont des données dans le cache, cette fonctionnalité est très exploitée sur les sites avec énormément de contenu (par exemple pour précharger les images).

Bien sûr, il est possible de faire cohabiter les deux modèles si nécessaire.

Dans son modèle gratuit, Cloudflare permet uniquement le lazy loading, largement suffisant pour mon besoin.

Masquer le serveur

Le dernier intérêt est bien sûr de masquer autant que possible son serveur final.

Pour ce point, il y a plusieurs sujets que cela adresse :

  • Un serveur moins visible (en termes de DNS) est donc moins attaqué. En effet, l'IP affichée en résolution de mon domaine est celle de Cloudflare.
  • Cela permet de restreindre les IPs pouvant se connecter au serveur, et de bloquer au niveau firewall si besoin

Il faut toutefois garder en tête que l'obfuscation n'est pas de la sécurité! Toutefois, cela ajoute une couche supplémentaire.

En option : un WAF

De par leur positionnement dans les requêtes HTTP, de nombreux CDN intègrent aussi nativement un WAF (Web Application Firewall). Le but de ce composant est de rajouter un niveau de sécurité supplémentaire, en protégeant de certaines attaques ou empêcher l'accès aux bots et autres IP malveillantes. J'avais déjà abordé ce sujet dans un de mes anciens billets que je vous invite à lire.

Sécuriser son application web sur AWS
Lorsque l’on déploie son application web sur AWS, il est nécessaire de sécuriserson utilisation. On pense souvent qu’il s’agit de déployer tout dans AWS et que notre applicationest directement en ligne et sécurisée. Ce qui est faux. Si la protection DDOS auniveau 4 (TCP/UDP) est effectivement ef…

Configurer Traefik

Vous l'aurez compris, l'idée est de filtrer autant que possible mon serveur web pour qu'il n'autorise que Cloudflare à accéder aux données, sinon, l'intérêt de ce dernier est limité.

Utilité de Cloudflare dans la configuration de base

Nous allons donc configurer nos IngressRoutes exposées par Traefik pour autoriser uniquement Cloudflare en tant que source autorisée.

En effet, dans sa configuration de base, il est complètement possible de contourner Cloudflare si l'on connait l'IP du serveur cible.

Comme on peut le voir, le serveur me répond bien un code 200 en appel direct

Pour ce faire, nous allons exploiter le middleware "ipWhiteList", ce dernier est assez basique : si l'adresse IP de l'appelant est dans la liste fournie, la requête passe, sinon, elle est rejetée.

Source : Documentation officielle

La liste des plages d'IP de Cloudflare est publique, disponible sur cette page.

Nous allons donc créer un objet kubernetes associé que nous allons déployer.

# Whitelist CloudFlare
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
  name: cloudflare
  namespace: default
spec:
  ipWhiteList:
    sourceRange:
      - 103.21.244.0/22
      - 173.245.48.0/20
      - 103.21.244.0/22
      - 103.22.200.0/22
      - 103.31.4.0/22
      - 141.101.64.0/18
      - 108.162.192.0/18
      - 190.93.240.0/20
      - 188.114.96.0/20
      - 197.234.240.0/22
      - 198.41.128.0/17
      - 162.158.0.0/15
      - 172.64.0.0/13
      - 131.0.72.0/22
      - 104.16.0.0/13
      - 104.24.0.0/14
      - 2400:cb00::/32
      - 2606:4700::/32
      - 2803:f800::/32
      - 2405:b500::/32
      - 2405:8100::/32
      - 2a06:98c0::/29
      - 2c0f:f248::/32

Si vous exploitez Docker, docker-compose ou autre, je vous invite à regarder sur la documentation de Traefik pour l'équivalence.

Ensuite, nous allons indiquer à notre backend qu'il doit utiliser ce middleware.

Pour rappel, les middlewares se positionnent en "interception" entre votre point d'entrée et votre service, dans le "Router" de Traefik.

Source : Documentation officielle

Dans notre cas, cela permettra donc à Traefik de rejeter la requête si l'IP source n'est pas dans la liste.

La dernière étape consiste donc à configurer mon IngressRoute.

kind: IngressRoute
metadata:
  name: ghost-tfe-fr-tls
  namespace: default
spec:
  entryPoints:
    - websecure
  routes:
  - kind: Rule
    priority: 1
    match: (HostHeader(`tferdinand.net`)  || HostHeader(`www.tferdinand.net`) )                                                                                                                                                                                && PathPrefix(`/`)
    services:
    - name: ghost-tfe-fr
      port: 2368
      helthcheck:
        path: /
        host: tferdinand.net
        intervalSeconds: 10
        timeoutSeconds: 5
    middlewares:
      - name: security
      - name: cloudflare
  tls:
    options:
      name: mytlsoption
      namespace: default

Comme vous pouvez le voir, la configuration est aussi simple que l'ajout d'une ligne.

J'ai fait le choix de ne pas ajouter de règles de firewall en plus, car j'utilise aussi mon serveur pour d'autres projets personnels sur lesquels je ne suis pas forcément derrière Cloudflare (et je suis en IP dynamique, et je ne veux pas mettre en place un VPN pour ce besoin ;) )

Si je refais mon appel curl après cette modification, Traefik rejette bien ma requête.

J'ai bien une erreur 403 "Forbidden" maintenant.

Configurer Cloudflare en exploitant Terraform

Si vous suivez ce blog régulièrement, vous avez dû noter que je déteste faire les choses manuellement. Cela pour plusieurs raisons :

  • L'humain est faillible, une erreur de manipulation est vite arrivée
  • Quand je veux faire une modification, je repars de zéro
  • Je veux quelque chose d'aussi uniforme que possible et reproductible

C'est pour cela que j'ai choisi d'utiliser Terraform pour piloter ma configuration Cloudflare, une fois que j'avais fini de tâtonner avec ses nombreuses configurations.

Petit rappel sur Terraform

Terraform est un outil d'infrastructure as code très populaire pour déployer sur des infrastructures cloud. Néanmoins, il faut aussi garder en tête que Terraform est très bon lorsqu'il s'agit de piloter des ressources en se basant sur des états, il est donc souvent utilisé pour des actions qui ne sont pas de l'infrastructure directement.

L'intérêt premier est de pouvoir déployer en mode "delta" à chaque fois tout en ayant une "image" du déploiement actuellement en place.

Terraform repose sur des "providers", qui fournissent les encapsulations nécessaires. Certains sont maintenus directement par HashiCorp, d'autres sont au contraire "communautaires".

Dans notre cas, le provider Cloudflare est maintenu par ... Cloudflare!

Passons au code!

Sans plus attendre, comme d'habitude, vous trouverez une version "lite" de ma propre configuration. L'idée est une fois de plus que vous puissiez comprendre la logique pour vous l'approprier.

Comme je le répète souvent, ce code est avant tout une démonstration, dans un contexte de production en entreprise, il sera évidemment nécessaire de l'ajuster. De plus, je suis dans un schéma où je n'ai qu'un seul serveur, si vous en avez plusieurs, il sera aussi nécessaire de faire des modifications.

Vous pouvez retrouver l'intégralité du code que je vais vous décrire sur GitHub :

teddy-ferdinand/cloudflare-terraform-lite
Sample project in order to deploy Cloudflare configuration using Terraform - teddy-ferdinand/cloudflare-terraform-lite

Commençons donc par notre fichier provider.tf. C'est ce dernier qui va indiquer à Terraform que l'on veut exploiter le provider CloudFlare.

terraform {
  required_providers {
    cloudflare = {
      source  = "cloudflare/cloudflare"
      version = "~> 2.0"
    }
  }
}

provider "cloudflare" {
  email   = var.cloudflare_email
  api_key = var.cloudflare_api_key
}

Le premier bloc indique la source du provider, le second est la configuration de base de ce provider.

Je reviendrais plus tard sur la manière de récupérer les informations nécessaires sur l'interface de Cloudflare.

Les enregistrement DNS

Ensuite, nous pouvons passer aux enregistrements DNS. Dans le fichier records.tf, vous retrouverez plusieurs types d'enregistrements :

Des enregistrements de type A classiques : ces derniers sont utilisés pour les requêtes DNS classiques vers votre domaine.

resource "cloudflare_record" "for_each_records" {
  for_each = var.record_list

  zone_id = var.cloudflare_configs["zone_id"]
  name    = each.value
  value   = var.cloudflare_target
  type    = "A"
  ttl     = 1
  proxied = true
}

Comme vous pouvez le voir, je me base sur une liste, ce qui me permet de boucler dessus et de ne pas dupliquer ce bloc de code à chaque nouvel enregistrement. Je n'aurais qu'à mettre à jour la liste associée.

L'identifiant de la zone est récupérable depuis l'interface cloudflare, je reviendrais dessus.

Ensuite, on peut voir le couple nom/valeur, indiquant le nom de votre enregistrement et la cible associée.

Le TTL est obligatoirement à 1 lorsque vous choisissez d'utiliser Cloudflare en mode proxy (Cloudflare masque votre IP et fait du cache).

Des enregistrements de type MX : Ils seront utilisés lorsque des mails vous sont envoyés. J'ai laissé volontairement ma propre configuration, qui utilise OVH.

resource "cloudflare_record" "for_each_records_mx" {
  for_each = var.mx_record_list

  zone_id  = var.cloudflare_configs["zone_id"]
  name     = var.cloudflare_configs["domain"]
  value    = each.value[0]
  type     = "MX"
  ttl      = 1
  priority = each.value[1]
}

On retrouve la même configuration que précédemment, mais avec un champ "priority" en surplus, qui permet de gérer la priorité des enregistrements MX. Très souvent, votre fournisseur de mail vous indiquera la configuration nécessaire à ce sujet afin de garantir une haute disponibilité de votre service de mail.

resource "cloudflare_record" "caa" {
  zone_id = var.cloudflare_configs["zone_id"]
  name    = var.cloudflare_configs["domain"]
  data = {
    flags = "0"
    tag   = "issue"
    value = "letsencrypt.org"
  }
  type = "CAA"
  ttl  = 1
}

Enfin, vous trouverez un enregistrement CAA, dont j'ai déjà parlé dans le passé, qui permet d'indiquer les autorités qui peuvent délivrer un certificat pour ce domaine.

La structure est un peu plus complexe ici, car il s'agit de décomposer la structure du champ CAA (conformément à la RFC associée).

Autoriser l'accès sans filtrage à certaines IP

Dans mon cas, j'utilise Ghost sur ce serveur. Toutefois, la configuration antibot de Cloudflare empêche mon serveur de générer ses prévisualisations de lien. En effet, la plage d'IP associée à OVH est bloquée, empêchant le serveur de se connecter à lui-même en mode "public".

Pour corriger ce point, il est possible de "whitelister" le serveur afin qu'il passe outre les protections de Cloudflare.

Attention : Une IP whitelistée ne sera plus contrôlée par Cloudflare, il faut utiliser ce paramètre en connaissance de cause.

Comme vous pouvez le retrouver dans le fichier "access_rule", la configuration est très basique.

resource "cloudflare_access_rule" "dedicated_server" {
  notes = "Dedicated server"
  mode  = "whitelist"
  configuration = {
    target = "ip"
    value  = var.cloudflare_target
  }
}

Gérer différemment certaines pages

Lorsque j'ai basculé sur Cloudflare, certains lecteurs m'ont indiqué que leur agrégateur de flux RSS, installé sur leur serveur (chez OVH par exemple) ne fonctionnait plus sur mon site, car bloqué par Cloudflare.

Même si je suis pour empêcher l'accès au site à des bots, les flux RSS sont pour moi un outil prévu justement pour des bots. J'ai donc décidé de désactiver toutes les sécurités sur la page associée.

De plus, la page du flux RSS est une page qui bouge peu dans mon cas (une publication par semaine), et qui sollicite beaucoup mon serveur, de par le fonctionnement de Ghost qui le reconstruit à chaque appel. Ainsi, j'ai ajouté un niveau de cache bien plus élevé et long au niveau de cloudflare, permettant ainsi de requêter mon serveur de manière bien plus ponctuelle.

C'est ce que vous pouvez retrouver dans le fichier "page_rule".

À noter que le niveau gratuit de Cloudflare ne vous permet de définir que 3 règles.

resource "cloudflare_page_rule" "cache_rss" {
  zone_id  = var.cloudflare_configs["zone_id"]
  target   = "https://${var.cloudflare_configs["domain"]}/rss/"
  priority = 1
  status   = "active"
  actions {
    disable_security  = true
    browser_cache_ttl = 43200
    cache_level       = "cache_everything"
    edge_cache_ttl    = 43200
  }
}

Les réglages plus généraux de Cloudflare

Pour beaucoup de réglages, il existe une ressource globale Terraform pour toutes les configurer. Personnellement, je ne suis pas fan de ce schéma qui crée donc une ressource avec énormément de paramétrage sur plusieurs sections différentes et j'aurais préféré un découpage plus fin.

Vous retrouverez les informations dans le fichier "cloudflare_zone_settings" que je ne mettrais pas sur ce billet, car il est long et n'apporte pas vraiment d'information pertinente.

Pour en savoir plus sur cette section, je vous invite à consulter la documentation associée.

Les variables en entrée

Vous pouvez voir qu'un certain nombre de variables est disponible dans le fichier "variables". Je ne reviendrais pas sur toutes les variables qui parlent assez d'elles-mêmes, mais simplement sur la partie "cloudflare_configs" qui indique les informations nécessaires pour votre configuration.

Concernant le mail, il s'agit bien d'entendant d'un mail déclaré sur l'interface de cloudflare, et le domaine est votre domaine à modifier (par exemple "tferdinand.net" pour moi).

Concernant l'API Key et la Zone ID, vous pouvez les récupérer depuis l'interface.

La zone ID est disponible sur le bandeau latéral sur la page d'accueil de votre domaine.

Pour l'API Key, rendez-vous sur ce lien. Puis cliquez sur "View" sur "Global API Key".

Pour aller plus loin : importer la configuration existante

Si, comme moi, vous aviez déjà configuré Cloudflare avant d'utiliser Terraform, il faut savoir que Cloudflare met aussi à disposition un binaire vous permettant d'importer le nécessaire depuis l'espace d'administration. Vous pouvez retrouver les informations nécessaires ici.

Toutefois, dans mon cas, le binaire ne permettait pas d'extraire les "cloudflare_zone_settings_override", sans doute à cause d'un bug sur la version que j'ai utilisée, car c'est la que le bât blesse : toutes les releases sont taguées en 0.1.0, il est donc difficile de se fier à se point. Pour ma part, j'ai donc pointé sur un commit précédent puis j'ai juste fait un "go build".

De plus, dans le cas de l'import des configurations, l'outil importe tout, y compris les informations en lecture seule lorsqu'on à un niveau gratuit. Le provider vous renvoie une erreur si vous essayez de les envoyer, même si vous indiquez la valeur actuelle, il est donc nécessaire de faire un peu de ménage.

Toutefois, je salue l'initiative de mettre à disposition un outil complet qui permet de simplifier la transition vers Terraform.

Pour terminer

Cloudflare permet d'augmenter le niveau de sécurité de votre site tout en réduisant le temps de réponse global. De plus, cela vous permet de réduire la charge de vos serveurs en aval.

L'utilisation de Terraform vous permet d'avoir un point d'entrée unique pour piloter vos ressources et simplifie la gestion des changements. De plus, cela limite les erreurs de manipulation dans la console web.

Traefik quant à lui peut gérer de manière très simple le fait d'accepter uniquement Cloudflare comme point d'entrée.