Tout le monde le sait, WordPress est aujourd’hui le CMS le plus utilisé : il est ainsi le plus ciblé par les attaques en tout genres (ddos, injection de code…). C’est pourquoi il est important de renforcer la sécurité du CMS par tous les moyens. Aujourd’hui nous allons déployer une configuration optimale à l’aide de Terraform pour sécuriser WordPress avec Cloudflare : règles firewall WAF, règles de cache, sécurité du domaine, enregistrements DNS.

bldwebagency-fr-securiser-wordpress-cloudflare-waf-firewall

Les pré-requis

  • Un compte Cloudflare (gratuit)
  • Un CMS WordPress pointant sur les NS Cloudflare
  • Terraform installé sur votre machine

Schéma de l’infrastructure cible

Pour ceux qui ne le savent pas, Cloudflare intervient ici comme un Proxy entre le visiteur et l’origine (le serveur Web). Il intervient comme Proxy Cache, mais également comme Firewall pour protéger des attaques avec des règles managées par Cloudflare, et des règles définies par l’utilisateur. Il joue également un rôle de CDN en mettant en cache les requêtes (pages HTML, CSS, JS, Images) demandées par les utilisateurs, afin de les servir plus rapidement aux prochaines requêtes. Nous allons dans cet article utiliser Cloudflare au maximum de ses capacités (pour un compte gratuit) : Proxy Firewall, CDN.

CleanShot 2023-09-30 at 13.43.54

Terraform, qu’est ce que c’est ?

Terraform, développé par HashiCorp, est un outil de codage d’infrastructure en tant que code (IaC) qui permet aux développeurs de définir et de fournir des infrastructures de datacenter automatisées à l’aide de fichiers de configuration déclaratifs. En favorisant une approche d’infrastructure immuable et versionnée, Terraform facilite la gestion et l’orchestration de ressources cloud multi-plateformes, garantissant ainsi une scalabilité, une flexibilité et une reproductibilité optimales.

terraform-architecture-diagram

Dans notre exemple aujourd’hui, Terraform va nous permettre de déployer sur Cloudflare en quelques clics :

  • Les enregistrements DNS : A, TXT, DMARC, SPF
  • Les règles WAF / Firewall
  • La configuration générale de notre zone sur Cloudflare
  • Les règles de cache

Préparer son environnement Terraform sur macOS

Pour pouvoir utiliser Terraform, deux solutions :

  • Installer Terraform depuis Homebrew
  • Installer tfenv, l’outil magique qui vous permet de basculer d’une version de Terraform à une autre

Nous utiliserons ici la méthode tfenv, qui simplifie la gestion des versions de Terraform :

brew install tfenv
mkdir -p ~/.local/bin/
. ~/.profile
ln -s ~/.tfenv/bin/* ~/.local/bin
echo 'export PATH="$HOME/.tfenv/bin:$HOME/.local/bin:$PATH"' >> ~/.zshrc (ou ~/.bashrc)
which tfenv

La dernière commande devrait vous afficher le chemin vers le binaire tfenv, vous pouvez désormais spécifier la version à installer et à utiliser : 

❯ tfenv install v1.3.3
Terraform v1.3.3 is already installed
❯ tfenv use v1.3.3
Switching default version to v1.3.3
❯ terraform --version
Terraform v1.3.3
on darwin_amd64

Vous êtes désormais prêt à déployer des configurations avec Terraform !

Préparation de Cloudflare

Au niveau de Cloudflare, vous aurez besoin :

  • De l’API Key et de l’email de votre compte disponible sur votre profil Cloudflare : CF_APIKEY & CF_EMAIL
  • l’ID de votre zone dans Cloudflare pour votre site : ZONE_ID
  • D’une liste d’IP qu’il faudra créer depuis l’interface Web ou via l’API (nous allons créer cette liste via l’API)
  • De l’identifiant de votre compte Cloudflare (nous allons récupérer cet identifiant via l’API) : CF_ACCOUNT
  • Aucune configuration ne doit être présente côté Cloudflare, pour ne pas qu’il y ai de conflits

Récupération de l’ID de votre compte Cloudflare :

curl -sX GET "https://api.cloudflare.com/client/v4/accounts/" -H 'Content-Type: application/json' -H 'X-Auth-Email: EMAIL_CLOUDFLARE' -H 'X-Auth-Key: APIKEY_CLOUDFLARE' | jq -r '.result[].id'

Cette commande vous permet de récupérer l’ID de votre compte Cloudflare, mettez sa valeur de côté (CF_ACCOUNT)

Pour créer une liste d’IP, rien de plus simple, munissez-vous de l’ID de votre compte, de l’email et de la clé d’API et exécutez la commande suivante :

curl -sX POST "https://api.cloudflare.com/client/v4/accounts/CF_ACCOUNT/rules/lists" -H 'Content-Type: application/json' -H 'X-Auth-Email: CF_EMAIL' -H 'X-Auth-Key: CF_APIKEY' --data '{"description": "IP Blacklist","kind": "ip","name": "ipblacklist"}'

Vous avez désormais créé une liste sur votre compte Cloudflare, nommée ipblacklist, dans laquelle nous ajouterons plus tard dans cet article toutes les IPs blacklistées par Fail2ban.

Préparation de notre configuration Terraform pour Cloudflare

Dans votre terminal, créez un répertoire et les fichiers suivants :

mkdir -p ~/Terraform/cloudflare-config/modules/cloudflare-zone
cd ~/Terraform/cloudflare-config
touch versions.tf
touch cloudflare.tf
touch modules/cloudflare-zone/versions.tf
touch modules/cloudflare-zone/main.tf

Le fichier versions.tf

Ce fichier défini les différents provider et la version à utiliser. Dans notre cas, nous n’utiliserons que le module Cloudflare en version 4.15.0 (disponible ici). Mais d’autres providers existent : ovh, gandi

required_providers { 
    cloudflare = { 
      source = "cloudflare/cloudflare" 
      version = "4.15.0" 
    } 
  } 
}

Le fichier cloudflare.tf

Ce fichier permet de définir les identifiants pour la connexion à Cloudflare et la définition des modules pour chaque site avec leurs variables. Ici, nous allons déployer le site mondomain.com, mais vous pouvez ajouter autant de sites que vous le souhaitez :

provider "cloudflare" {
  email   = "CF_EMAIL"
  api_key = "CF_APIKEY"
}
module "cloudflare_zone_mondomaincom" {
  source         = "./modules/cloudflare-zone"
  zone_name      = "mondomain.com"
  zone_id        = "ZONE_ID"
  record         = "IP_ORIGIN"
  main_domain    = "www.mondomain.com"
  alias_domain   = "mondomain.com"
  mon_ip_safe    = "MON_IP"
}

Le fichier main.tf du module

Ce fichier décrit toutes les actions à effectuer lors du run terraform : création des enregistrements DNS, des ruleset pour les règles Firewall, Cache … Nous pouvons découper ce fichier en plusieurs parties :

  • La définition des variables que nous utiliserons dans le fichier cloudflare.tf afin de gérer notre / nos zone.
  • cloudflare_zone_settings_override : la configuration générale de la zone Cloudflare du domaine : compression, minification des resources statiques, niveau de cache… Autant d’éléments gérés par Cloudflare qui ne le seront pas au niveau de l’origine.
  • cloudflare_record : la génération des enregistrements DNS de notre domaine
  • cloudflare_rulesetredirect_to_main_domain : il s’agit d’une redirection 301, effectuée directement au niveau de Cloudflare, de votre (vos) alias, vers le domaine principal. Ce n’est pas grand chose, mais on décharge le serveur Web de cette action.
  • cloudflare_rulesetwordpress_cache_rules : nous définissons ici un ensemble de règles pour la mise en cache des fichiers statiques, et nous excluons la parte Admin de WordPress.
  • cloudflare_rulesetwordpress_firewall_rules :
    • La première règle bloque toute les IP qui sont dans notre liste d’IP
    • La seconde autorise notre IP Safe, les bots connus enregistrés chez Cloudflare, et quelques chemins URIs / UA
    • La troisième limite les attaques sur le xmlrpc.php et les requêtes sans domaine referer.
    • La quatrième limite les accès à l’admin WordPress à la France
    • La cinquième règle bloque certains pays et les requêtes sur les formulaires de commentaires de WordPress lorsqu’il n’y a pas de referer.
## Définition des variables
variable "zone_name" {}
variable "zone_id" {}
variable "record" {}
variable "alias_domain" {}
variable "main_domain" {}
variable "mon_ip_safe" {} # IP de votre bureau / domicile à autoriser

## Configuration de la zone
resource "cloudflare_zone_settings_override" "settings" { 
  zone_id = var.zone_id 
  settings { 
    always_online = "on" # On active le mode always online, qui affiche une page du Web Archive même si l'origine est KO
    always_use_https = "off" 
    automatic_https_rewrites = "off" 
    brotli = "on" # On active la compression brotli
    cache_level = "basic" 
    development_mode = "off"
    email_obfuscation = "off" 
    http3 = "on" # On active le HTTP3
    browser_cache_ttl = 0 # Respecte le TTL défini par l'origine
    early_hints = "on" # Préchargement des assets d'une page non cacheable
    ip_geolocation = "on" 
    ipv6 = "on" 
    max_upload = 100 
    min_tls_version = "1.2" 
    pseudo_ipv4 = "off" 
    rocket_loader = "off" 
    ssl = "strict" 
    minify { 
      css = "on" 
      js = "on"
      html = "off"
    } 
  } 
}

## Records DNS
resource "cloudflare_record" "record_mail_spf" {
  zone_id = "${var.zone_id}"
  name = "${var.zone_name}"
  value = "v=spf1 a mx include:spf.bldwebagency.fr -all"
  type = "TXT"
  ttl = 1
  proxied = false
}
resource "cloudflare_record" "record_mail_dmarc" {
  zone_id = "${var.zone_id}"
  name = "_dmarc.${var.zone_name}"
  value = "v=DMARC1; p=none;"
  type = "TXT"
  ttl = 1
  proxied = false
}
resource "cloudflare_record" "record_root_ipv4" {
  zone_id = "${var.zone_id}"
  name    = "${var.zone_name}"
  value   = ${var.record}
  type    = "A"
  ttl     = 1
  proxied = true
}

## Redirection vers le domaine principal
resource "cloudflare_ruleset" "redirect_to_main_domain" {
  zone_id     = "${var.zone_id}"
  name        = "redirects"
  description = "Redirect to main domain"
  kind        = "zone"
  phase       = "http_request_dynamic_redirect"

  rules {
    action = "redirect"
    action_parameters {
      from_value {
        status_code = 301
        target_url {
          value = "https://${var.main_domain}"
        }
        preserve_query_string = false
      }
    }
    expression  = "(http.host contains \"${var.alias_domain}\")"
    description = "Redirect to main domain"
    enabled     = true
  }
}

## Règles de mise en cache pour WordPress
resource "cloudflare_ruleset" "wordpress_cache_rules" {
  zone_id = var.zone_id
  kind    = "zone"
  name    = "default"
  phase   = "http_request_cache_settings"
  rules {
    action = "set_cache_settings"
    action_parameters {
      browser_ttl {
        mode = "respect_origin"
      }
      cache = false
    }
    description = "Skip admin pages"
    enabled     = true
    expression  = "(http.request.uri.path contains \"wp-admin\") or (http.request.uri.path contains \"wp-login\")"
  }
  rules {
    action      = "set_cache_settings"
    description = "Cache static assets"
    enabled     = true
    expression  = "(http.request.uri.path contains \".webp\") or (http.request.uri.path contains \".avif\") or (http.request.uri.path contains \".woff\") or (http.request.uri.path contains \".woff2\") or (http.request.uri.path contains \".png\") or (http.request.uri.path contains \".svg\") or (http.request.uri.path contains \".jpeg\") or (http.request.uri.path contains \".jpg\") or (http.request.uri.path contains \".js\") or (http.request.uri.path contains \".css\")"
    action_parameters {
      browser_ttl {
        mode = "respect_origin"
      }
      cache = true
      cache_key {
        cache_deception_armor = false
        custom_key {
          query_string {
            exclude = ["*"]
          }
        }
        ignore_query_strings_order = true
      }
      edge_ttl {
        default = 2678400
        mode    = "override_origin"
      }
      origin_error_page_passthru = true
      serve_stale {
        disable_stale_while_updating = true
      }
    }
  }
  rules {
    action = "set_cache_settings"
    action_parameters {
      browser_ttl {
        default = 14400
        mode    = "override_origin"
      }
      cache = true
      cache_key {
        custom_key {
          query_string {
            exclude = ["*"]
          }
        }
      }
      edge_ttl {
        default = 172800
        mode    = "override_origin"
      }
      serve_stale {
        disable_stale_while_updating = true
      }
    }
    description = "Full cache on uploads"
    enabled     = true
    expression  = "(http.request.uri.path contains \"/wp-content/uploads/\")"
  }
}

## Règles de Firewall pour sécuriser notre blog WordPress
resource "cloudflare_ruleset" "wordpress_firewall_rules" {
  zone_id     = var.zone_id
  name        = "BLDWebAgency Firewall Rules"
  description = "BWA set of rules to protect websites against ddos"
  kind        = "zone"
  phase       = "http_request_firewall_custom"
  rules {
    description = "Block from IP List"
    action      = "block"
    expression  = "(ip.src in $bldwebagencylistip)"
    enabled     = true
  }
  rules {
    action = "skip"
    action_parameters {
      phases  = ["http_request_firewall_managed", "http_request_sbfm"]
      ruleset = "current"
    }
    description = "Allow Safe places"
    enabled     = true
    expression  = "(ip.src eq ${var.mon_ip_safe) or (cf.client.bot) or (http.request.uri.path contains \".ico\") or (http.user_agent contains \"bitlybot\") or (http.user_agent contains \"updown.io daemon 2.8\") or (http.request.uri.path contains \"favicon\") or (http.user_agent contains \"DuckDuckGo\") or (http.user_agent contains \"Pingdom\") or (http.user_agent contains \"PetalBot\") or (http.user_agent contains \"CFNetwork\") or (http.user_agent contains \"qwant.com\") or (http.user_agent contains \"bingbot\") or (http.user_agent contains \"updown.io daemon 2.6\") or (http.user_agent contains \"Stripe/1.0\")"
    logging {
      enabled = true
    }
  }
  rules {
    description = "Restrict referer for WP Paths"
    action      = "managed_challenge"
    expression  = "(http.request.uri eq \"/xmlrpc.php\") or (http.request.uri.path contains \"/wp-content/\" and not http.referer contains \"${var.zone_name}\") or (http.request.uri.path contains \"/wp-includes/\" and not http.referer contains \"${var.zone_name}\")"
    enabled     = true
  }
  rules {
    description = "Challenge wp-admin out of France"
    action      = "managed_challenge"
    expression  = "(http.request.uri.path contains \"/wp-login.php\" and  ip.geoip.country ne \"FR\") or (http.request.uri.query contains \"action=lostpassword\" and http.referer ne \"${var.zone_name}\")"
    enabled     = true
  }
  rules {
    description = "Restrict some WP Path and Block countries"
    action      = "managed_challenge"
    expression  = "(ip.geoip.country in {\"SG\" \"BR\" \"RU\" \"CN\" \"IQ\" \"AZ\" \"SG\" \"AF\"}) or (http.request.uri contains \"/wp-comments-post.php\" and http.request.method eq \"POST\" and not http.referer contains \"${var.zone_name}\")"
    enabled     = true
  }
}

Déploiement de la configuration

Pour initialiser notre module et le provider, nous allons exécuter les commandes suivantes :

cd ~/Terraform/cloudflare-config
terraform init
terraform plan

A ce stade, l’output de la commande devrait afficher l’ensemble des configurer à ajouter. Si tout vous semble correct, vous pouvez passer à l’application :

terraform apply

Vous devriez voir l’ensemble des configurations déployées dans l’interface Web de Cloudflare, sous Règles > Règles de cache, Sécurité > Waf …

Comment bloquer les IP sur Cloudflare depuis Fail2ban

Fail2ban est un outil merveilleux qui permet de surveiller des logs et de détecter des patterns qui permettront à l’outil de déclencher des actions. Nous avions rédigé un article sur la configuration du plugin Fail2ban pour WordPress.

Ici, nous allons :

  • Lister les Jails Fail2ban essentielles pour une sécurité optimale
  • Créer une nouvelle action permettant d’ajouter les nouvelles IP bloquées dans notre liste Cloudflare
  • Déployer cette action dans certaines de nos Jails

Le fichier jail.local de Fail2ban

Ce fichier définit quelques règles de base, des configuration de bandtime, findtime, maxretry par défaut ainsi que les IP à ignorer (notre SAFE_IP par exemple) :

[INCLUDES]
before = paths-debian.conf

[DEFAULT]
ignoreip = SAFE_IP
ignorecommand =
bantime  = 12h
findtime  = 10m
maxretry = 3
maxmatches = %(maxretry)s
backend = auto
usedns = warn
logencoding = auto
enabled = false
mode = normal
filter = %(__name__)s[mode=%(mode)s]


#
# ACTIONS
#

destemail = root@localhost
sender = root@<fq-hostname>
mta = sendmail
protocol = tcp
chain = <known/chain>
port = 0:65535
fail2ban_agent = Fail2Ban/%(fail2ban_version)s
banaction = iptables-multiport
banaction_allports = iptables-allports
action_ = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
action_mw = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
            %(mta)s-whois[name=%(__name__)s, sender="%(sender)s", dest="%(destemail)s", protocol="%(protocol)s", chain="%(chain)s"]
action_mwl = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
             %(mta)s-whois-lines[name=%(__name__)s, sender="%(sender)s", dest="%(destemail)s", logpath="%(logpath)s", chain="%(chain)s"]
action_xarf = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
             xarf-login-attack[service=%(__name__)s, sender="%(sender)s", logpath="%(logpath)s", port="%(port)s"]

# ban IP on CloudFlare & send an e-mail with whois report and relevant log lines
# to the destemail.
action_cf_mwl = cloudflare[cfuser="%(cfemail)s", cftoken="%(cfapikey)s"]
                %(mta)s-whois-lines[name=%(__name__)s, sender="%(sender)s", dest="%(destemail)s", logpath="%(logpath)s", chain="%(chain)s"]
action_blocklist_de  = blocklist_de[email="%(sender)s", service=%(filter)s, apikey="%(blocklist_de_apikey)s", agent="%(fail2ban_agent)s"]


action_badips = badips.py[category="%(__name__)s", banaction="%(banaction)s", agent="%(fail2ban_agent)s"]
action_badips_report = badips[category="%(__name__)s", agent="%(fail2ban_agent)s"]

action_abuseipdb = abuseipdb
action = %(action_)s

#
# JAILS
#

## See fail.local

Le fichier jail.local

Ce fichier est inclu par défaut avec le fichier jail.conf, il permet de centraliser toutes nos jails et leur configuration. Nous avons ajouté ici (au cas où), toutes les IP Cloudflare (IPV4, IPV6) à la liste des ignoreip.

[sshd] ## Bloque l'IP sur le port SSH 1 heure après 3 tentatives sur 5 minutes
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
action = ufw[application="OpenSSH"]
findtime = 300
bantime = 3600
maxretry = 3
ignoreip = 163.172.53.51 163.172.51.134 163.172.33.112 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 104.16.0.0/13 104.24.0.0/14 172.64.0.0/13 131.0.72.0/22 2400:cb00::/32 2606:4700::/32 2803:f800::/32 2405:b500::/32 2405:8100::/32 2a06:98c0::/29 2c0f:f248::/32

[recidive] ## Bloque une IP déjà bannie 2 fois sur 1 journée, pendant 12h
enabled = true
logpath = /var/log/fail2ban.log
filter = recidive
action = ufw
findtime = 86400 ; 1 day
maxretry = 2
bantime = 43200 ; 12 hours
ignoreip = 163.172.53.51 163.172.51.134 163.172.33.112 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 104.16.0.0/13 104.24.0.0/14 172.64.0.0/13 131.0.72.0/22 2400:cb00::/32 2606:4700::/32 2803:f800::/32 2405:b500::/32 2405:8100::/32 2a06:98c0::/29 2c0f:f248::/32

[recidive2] ## Bloque une IP déjà bannie 7 fois sur 1 semaine, pendant 24h et l'ajoute à la liste d'IP Cloudflare
enabled = true
filter = recidive
action = route[blocktype="blackhole"]
        cloudflare-list
bantime = 86400 ;1 day
findtime = 604800 ;1 week
logpath = /var/log/fail2ban.log
maxretry = 7
ignoreip = 163.172.53.51 163.172.51.134 163.172.33.112 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 104.16.0.0/13 104.24.0.0/14 172.64.0.0/13 131.0.72.0/22 2400:cb00::/32 2606:4700::/32 2803:f800::/32 2405:b500::/32 2405:8100::/32 2a06:98c0::/29 2c0f:f248::/32

[recidive3] ## Bloque une IP déjà bannie 10 fois sur 1 mois, pendant 1 semaine et l'ajoute à la liste d'IP Cloudflare
enabled = true
filter = recidive
action = route[blocktype="blackhole"]
        cloudflare-list
bantime = 604800 ;1 week
findtime = 2592000 ;1 month
logpath = /var/log/fail2ban.log
maxretry = 10
ignoreip = 163.172.53.51 163.172.51.134 163.172.33.112 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 104.16.0.0/13 104.24.0.0/14 172.64.0.0/13 131.0.72.0/22 2400:cb00::/32 2606:4700::/32 2803:f800::/32 2405:b500::/32 2405:8100::/32 2a06:98c0::/29 2c0f:f248::/32

[recidive4] ## Bloque une IP déjà bannie 20 fois sur 6 mois, pendant 1 mois et l'ajoute à la liste d'IP Cloudflare
enabled = true
filter = recidive
action = route[blocktype="blackhole"]
        cloudflare-list
bantime = 2592000 ;1 month
findtime = 15552000 ;6 months
logpath = /var/log/fail2ban.log
maxretry = 20
ignoreip = 163.172.53.51 163.172.51.134 163.172.33.112 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 104.16.0.0/13 104.24.0.0/14 172.64.0.0/13 131.0.72.0/22 2400:cb00::/32 2606:4700::/32 2803:f800::/32 2405:b500::/32 2405:8100::/32 2a06:98c0::/29 2c0f:f248::/32

[wordpress-soft] ## Plugin WP-Fail2ban nécessaire
enabled = true
filter = wordpress-soft
logpath = /var/log/auth.log
action = ufw[application="Nginx"]
maxretry = 3
findtime = 600
bantime = 3600
port = http,https
ignoreip = 163.172.53.51 163.172.51.134 163.172.33.112 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 104.16.0.0/13 104.24.0.0/14 172.64.0.0/13 131.0.72.0/22 2400:cb00::/32 2606:4700::/32 2803:f800::/32 2405:b500::/32 2405:8100::/32 2a06:98c0::/29 2c0f:f248::/32

[nginx-forbidden]
enabled = true
filter = nginx-forbidden
port = http,https
logpath = /var/log/nginx/*error*.log
action = ufw[application="Nginx"]
findtime = 300
bantime = 21600
maxretry = 4
ignoreip = 163.172.53.51 163.172.51.134 163.172.33.112 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 104.16.0.0/13 104.24.0.0/14 172.64.0.0/13 131.0.72.0/22 2400:cb00::/32 2606:4700::/32 2803:f800::/32 2405:b500::/32 2405:8100::/32 2a06:98c0::/29 2c0f:f248::/32

Les filtres custom

[INCLUDES]
before = common.conf

[Definition]
_daemon = (?:fail2ban(?:-server|\.actions)\s*)
_jailname = recidive
failregex = ^%(__prefix_line)s(?:\s*fail2ban\.actions\s*%(__pid_re)s?:\s+)?NOTICE\s+\[(?!%(_jailname)s\])(?:.*)\]\s+Ban\s+<HOST>\s*$
datepattern = ^{DATE}
ignoreregex = 
journalmatch = _SYSTEMD_UNIT=fail2ban.service PRIORITY=5
[INCLUDES]
before = common.conf

[Definition]
_daemon = (?:wordpress|wp)
failregex = ^%(__prefix_line)sEmpty username from <HOST>$
            ^%(__prefix_line)sAuthentication failure for .* from <HOST>$
            ^%(__prefix_line)sREST authentication failure for .* from <HOST>$
            ^%(__prefix_line)sXML-RPC authentication failure for .* from <HOST>$
            ^%(__prefix_line)sAuthentication attempt for unknown user .* from <HOST>$
            ^%(__prefix_line)sBlocked username authentication attempt for .* from <HOST>$
            ^%(__prefix_line)sPingback requested from <HOST>$
            ^%(__prefix_line)sComment attempt on .* post \d+ from <HOST>$

ignoreregex =

L’action Fail2Ban pour Cloudflare

Il est plutôt simple d’appeler l’API Cloudflare pour ajouter une IP à la liste (actionban), mais pour retirer cette IP cela peut être un peut plus complexe. J’ai créé un script permettant de réaliser ces actions simplement et ce script est appelé par l’action cloudflare-list. Pensez à renseigner les valeurs des variables :

  • CF_EMAIL
  • CF_APIKEY
  • ACCOUNT_ID – ID de votre compte Cloudflare
  • LIST_ID – créée précédemment
#!/bin/bas

CF_EMAIL="CF_EMAIL"
CF_APIKEY="CF_APIKEY"
ACCOUNT_ID="ACCOUNT_ID"
LIST_ID="LIST_ID"

ACTION=$1
IP=$2

case $ACTION in
    ban)
        BANIP_RESULT=$(/usr/bin/curl -sX POST "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/rules/lists/${LIST_ID}/items" \
        -H "X-Auth-Email: ${CF_EMAIL}" -H "X-Auth-Key: ${CF_APIKEY}" -H "Content-Type: application/json" --data '[{"comment": "From Fail2Ban", "ip":"'${IP}'"}]' | /root/.local/bin/jq -r '.success')
        if [[ "$BANIP_RESULT" == "true" ]]; then
            echo "Successfully added to Cloudflare list"
        fi
        ;;
    unban)
        LIST_ITEM_ID=$(/usr/bin/curl -sX GET "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/rules/lists/${LIST_ID}/items" \
                -H "X-Auth-Email: ${CF_EMAIL}" -H "X-Auth-Key: ${CF_APIKEY}" -H "Content-Type: application/json" | /root/.local/bin/jq '.result[] | select(.ip == "'${IP}'")' | /root/.local/bin/jq -r '.id')
        LIST_ITEM_DELETE_RESULT=$(/usr/bin/curl -sX DELETE "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/rules/lists/${LIST_ID}/items" \
                -H "X-Auth-Email: ${CF_EMAIL}" -H "X-Auth-Key: ${CF_APIKEY}" -H "Content-Type: application/json" --data '{"items":[{"id":"'$ITEM_ID'"}]}' | /root/.local/bin/jq -r '.success')
        if [[ "$LIST_ITEM_DELETE_RESULT" == "true" ]]; then
            echo "Successfully removed to Cloudflare list"
        fi
        ;;
    *)
        ;;
esac

Désormais, déployons l’action cloudflare-list qui appellera ce script pour ajouter et retirer les IPs dans notre liste Fail2ban :

#
# Author: BLD Web Agency
#

[Definition]
actionstart =
actionstop =
actioncheck =
actionban = /root/fail2ban-cloudflare.sh "ban" "<ip>"
actionunban = /root/fail2ban-cloudflare.sh "unban" "<ip>"

[Init]

Conclusion

Grâce à cet article, vous êtes aujourd’hui en mesure :

  • de renforcer la sécurité de votre site WordPress avec des règles Firewall efficaces et une blacklist d’IP qui seront bloquée avant même d’atteindre l’origine
  • d’améliorer les performances de votre site avec des règles de cache optimisées, la compression brotli, hearly hint, http3 et la minification des resources statiques
  • de déployer une configuration complète Cloudflare avec Terraform
  • de créer des enregistrements DNS rapidement et pour un nombre illimité de domaines

L’un des plus grands avantages de Cloudflare et de cette configuration, c’est que votre serveur d’origine sera beaucoup moins sollicité , de nombreuses actions sera effectuées en amont, par Cloudflare et vous devriez voir le load de votre machine diminuer à vue d’oeil !