Automatisation de la gestion de règles AWS WAF avec AWS Firewall Manager, Terraform et Gitlab CI

GitLab CI
Temps de lecture : 10 minutes

La gestion des règles de sécurité peut s’avérer complexe dans un contexte où l’entreprise dispose de très nombreux comptes AWS et d’autant d’unités d’organisation (organisational units). Dans cet article tiré d’un cas d’usage client, nous verrons comment nous avons automatisé et centralisé le déploiement et la gestion de règles AWS WAF à l’aide d’AWS Firewall Manager, de Terraform et d’une pipeline CI/CD (Gitlab CI), notamment pour bloquer les IP externes malicieuses identifiées par les équipes sécurité.

Le besoin client à l’origine de ce projet était donc de centraliser les règles WAF, notamment celles permettant de bloquer les IP malicieuses. Ce besoin devait intégrer la contrainte liée au fait qu’il ne peut y avoir qu’un seul Firewall Manager pour l’organisation, ce qui apporte une certaine dose de complexité dans la gestion des spécificités de comptes et de leurs sous-environnements.

Disclaimer:

Le cas d’usage du client étant focalisé sur les règles WAF, il n’est fait aucune mention des règles applicables aux groupes de sécurité et à AWS Shield, que Firewall Manager sait également gérer.

Dans le cadre de notre projet, les exigences client étaient les suivantes :

  • déployer sur les comptes applicatifs et de manière centralisée l’ensemble des règles (dénommées “règles standards”), à la fois celles managées par AWS et celles construites par l’équipe Sécurité du client ;
  • déployer les règles dans les différents environnements (dev, stg, prd) ;
  • déployer les règles dans les différentes régions utilisées ;
  • déployer de manière centralisée les règles managées par le métier au sein des règles standards.

AWS Firewall Manager

AWS Firewall Manager est utilisé pour créer des stratégies (policies) WAF.

Ces stratégies contiennent des groupes de règles (rule groups) de pré-traitement et des groupes de règles de post-traitement qui sont combinées avec des groupes de règles locales pour créer une webACL locale.

Nous déployons une policy par environnement. Pour cela, nous incluons dans la policy une liste d’Unités Organisationnelles (OU) correspondant à l’environnement des métiers.

Firewall Manager étant régional, il est nécessaire de créer une policy par région où les ressources locales (ALB et API Gateway) protégées par le WAF sont présentes, ainsi qu’une policy pour Cloudfront qui est une ressource globale (dans la région us-east-1).

Ceci étant dit, cela induit la multiplication des petits pains ou plutôt des policies de la manière suivante : nombre de policies = Nombre d’environnements * ( nombre de régions + 1)

Application d’une règle générale

Concernant les IP malicieuses, un fichier est généré chaque jour dans un bucket S3. Les IP sont récupérées pour créer la règle qui sera ensuite appliquée sur tous les comptes AWS et sur tous les environnements.

Les ressources sont ensuite créées automatiquement avec Terraform, et le déploiement quotidien de la liste mise à jour s’effectue par un pipeline CI/CD (Gitlab CI). L’historique des adresses IP est conservé pendant 30 jours.

Dans Terraform, on crée un ensemble d’IP (IP Sets) dans lequel est enregistré l’ensemble des adresses IP.

resource "aws_wafv2_ip_set" "ipv4_list" {
 # required arguments
 name               = "waf-${var.naming_convention}-${lower(var.scope)}-ipv4_set"
 scope              = var.scope
 ip_address_version = "IPV4"    
 addresses          = var.ipv4_addresses_list


 # optional arguments
 description = "List of IPv4 IP addresses get from s3 bucket data source"
}


resource "aws_wafv2_ip_set" "ipv6_list" {
 # required arguments
 name               = "waf-${var.naming_convention}-${lower(var.scope)}-ipv6_set"
 scope              = var.scope
 ip_address_version = "IPV6"    
 addresses          = var.ipv6_addresses_list


 # optional arguments
 description = "List of IPv6 IP addresses get from s3 bucket data source"
}

On remarque qu’il est nécessaire de créer 2 IP Sets pour différencier les versions d’adresses IP (v4 et v6).

Les adresses sont récoltées à partir des fichiers CSV des 30 derniers jours à l’aide d’un script python exécuté dans le pipeline et enregistré en tant que liste dans un fichier de variable tfvars sous la forme suivante :

ipv4_addresses_list = ["1.10.253.207/32", "1.189.101.91/32", ..., "99.81.95.247/32"]
ipv6_addresses_list = ["2a0e:1bc1:56:1000::12f:b91c/128", "2a0e:1bc1:56:1000::12f:c91b/128", ..., "2a0e:1bc1:56:1000::12f:c19b/128"]

Ces 2 IP Sets sont ensuite intégrés dans l’unique règle de la rule group dont le but est de bloquer les flux provenant des adresses IP.

resource "aws_wafv2_rule_group" "vaf_v2" {
 # required arguments
 name     = "waf-${var.naming_convention}-rulegroup-blockip-${random_integer.index.result}"
 capacity = 2
 scope    = var.scope
 visibility_config {
   cloudwatch_metrics_enabled = false
   metric_name                = "rule-blockip-metric"
   sampled_requests_enabled   = false
 }


 lifecycle {
   create_before_destroy = true
 }
 # optional arguments
 description = "simple rule group aims to block specific IP addresses"
 rule {
   # required sub-arguments
   name     = "waf-${var.naming_convention}-rule-blockip"
   priority = 1
   action {
     block {}
   }
   statement {
     or_statement {
       statement {
         ip_set_reference_statement {
           arn = aws_wafv2_ip_set.ipv4_list.arn
         }
       }
       statement {
         ip_set_reference_statement {
           arn = aws_wafv2_ip_set.ipv6_list.arn
         }
       }
     }
   }
   visibility_config {
     cloudwatch_metrics_enabled = false
     metric_name                = "rule-blockip-metric"
     sampled_requests_enabled   = false
   }
 }
}

Le schéma suivant représente le fonctionnement du script python qui récupère les adresses IP :

La gestion des régions

Nous déployons les règles AWS sur différentes régions. Pour cela nous créons dans le code Terraform un provider AWS pour chacune d’entre elles avec un alias et une région différente. La liste des régions utilisées pouvant évoluer au fil du temps, et n’étant pas la même pour chaque métier, le client a demandé à ce que l’on puisse configurer la liste des régions pour chaque business line. C’est ainsi qu’est né un fichier de configuration au format json,  reprenant la liste des business lines et des régions associées.

{
   "business_lines": [
       {
           "name": "Identity & Body Piercing Solutions",
           "regions": [
               "eu-west-1",
               "eu-east-2"
           ]
       },
       {
           "name": "Microwave & Cupcakes Sub-Systems",
           "regions": [
               "eu-west-2"
           ]
       },
       {
           "name": "Partners Particulier",
           "regions": [
               "ap-southeast-1"
           ]
       }
   ]
}

Ce fichier de configuration est l’entrée d’un script python qui permet de créer les fichiers Terraform contenant les différents provider et les appels au module de création de policies, à partir de modèles jinja2.

Providers.jinja2 :

provider "aws" {
 alias  = "{{short_region}}"
 region = "{{region}}"
 default_tags {
   tags = local.tags
 }
}

main.jinja2 :

module "fms_policy-{{blName}}-{{short_region}}" {
 source = "./modules/fms_policies"
 providers = {
   aws = aws.{{short_region}}
   aws.organization = aws.organization
 }
 env                                      = var.aws_env
 naming_convention                        = "${var.bl}-${substr(var.aws_env, 0, 1)}-{{short_region}}-${var.appstackcode}-{{blName}}-${var.branch_name}"
 managed_rule_groups_list                 = var.managed_rule_groups_list
 ipv4_addresses_list                      = var.ipv4_addresses_list
 ipv6_addresses_list                      = var.ipv6_addresses_list
 aws_iam_role_firehose_role_arn           = aws_iam_role.firehose_role.arn
 aws_s3_bucket_firehose_arn               = aws_s3_bucket.firehose.arn
 target_account_list                      = var.target_account_list
 resource_type_list                       = var.resource_type_list
 business_lines_parent_id                 = var.business_lines_parent_id
 excluded_custom_business_lines           = var.excluded_custom_business_lines
 {% if business_line_name is not none -%}
 custom_business_line                     = "{{business_line_name}}"
 bl_short_name                            = "{{blName}}"
 customer_rules_list                      = {{rules_list}}
 {% endif %}
}

Le schéma suivant représente le fonctionnement du script python qui récupère la liste des régions par Business line :

La gestion des environnements

Comme nous l’avons évoqué plus haut, dans une organisation avec de nombreux comptes et environnements, on ne veut pas appliquer les mêmes règles partout. Par exemple, les règles sur un environnement de production seront différentes de celles des autres environnements à l’instant ‘t’ afin de valider que l’évolution d’une règle n’impacte pas l’utilisation d’une application . Afin de gérer les environnements, nous avons donc ciblé les Organisational Units dans l’organisation, avec une OU par environnement (développement/staging/production) et les sous-OU des métiers :

La solution était donc la création d’une règle spécifique (policy), qui sera ensuite appliquée à une ou plusieurs OU. Par exemple, la policy de production s’applique à tous les comptes de production, il était donc nécessaire de pouvoir cibler par environnement et de cibler uniquement les bonnes sous unités organisationnelles en dessous de la production.

La gestion des exceptions

Mais la complexité ne s’arrête pas là.

Alors que l’utilisation nominale de Firewall Manager permet  de laisser la main aux départements pour ajouter des règles qui seront insérées localement, une des exigences du client est de déployer de manière centralisée les règles managées par le métier au sein des règles standards. D’autant que Firewall Manager permet de gérer tous types de règles, par exemple faire de l’analyse d’accès sur une application, ou identifier l’origine géographique des flux. Une règle spécifique sera donc créée également dans Firewall Manager, mais appliquée uniquement pour la BU concernée. Il est donc nécessaire de filtrer cette BU pour ne pas y appliquer la règle commune et y appliquer une règle qui mixe la règle commune et la règle spécifique.         

Le code suivant permet de récupérer la liste des IDds d’OU pour lesquelles on appliquera la policy standard (target_ou_id_list_excluded_custom_business_line) et la liste des IDs d’OU pour lesquelles il y aura une règle custom (custom_business_line_ou_id) :

data "aws_organizations_organizational_units" "environments" {
 provider = aws.organization
 parent_id = var.business_lines_parent_id
}


data "aws_organizations_organizational_units" "business_lines" {
 provider = aws.organization
 parent_id = local.target_env[0].id
}


locals {
 target_env = [for env in data.aws_organizations_organizational_units.environments.children : env if env.name == title(var.env)]
 target_ou_id_list_excluded_custom_business_line = toset([for business_line in  data.aws_organizations_organizational_units.business_lines.children : business_line.id if !contains(var.excluded_custom_business_lines, business_line.name)])
 custom_business_line_ou_id = toset([for business_line in  data.aws_organizations_organizational_units.business_lines.children : business_line.id if business_line.name == var.custom_business_line])
 target_ou_id_list = var.custom_business_line != null ? local.custom_business_line_ou_id : local.target_ou_id_list_excluded_custom_business_line
}

Pour qu’une business line puisse ajouter des règles customisées, celles-ci doivent être placées dans le répertoire config/custom_rules et écrites en HCL :

 rule {
   name     = "waf-${var.naming_convention}-simple-first"
   priority = 0
   action {
     block {}
   }
   statement {
     byte_match_statement {
       search_string = "bWF0Y2hlZF9zdHJpbmc="
       field_to_match {
         method {}
       }
       text_transformation {
         priority = 0
         type = "NONE"
       }
       positional_constraint = "EXACTLY"
     }
   }
   visibility_config {
     cloudwatch_metrics_enabled = false
     metric_name                = "waf-${var.naming_convention}-simple-first-metric"
     sampled_requests_enabled   = false
   }
 }

Et elles doivent être référencées dans le fichier business_lines.json :

{
   "business_lines": [
       {
           "name": "Identity & Body Piercing Solutions",
           "regions": [
               "eu-west-1",
               "eu-east-2"
           ],
           "custom_rules": [
               "IBPS-001",
               "IBPS-002"
           ]
       },
       {
           "name": "Microwave & Cupcakes Sub-Systems",
           "regions": [
               "eu-west-2"
           ],
           "custom_rules": [
               "MCS-001",
               "MCS-002"
           ]
       },
       {
           "name": "Partners Particulier",
           "regions": [
               "ap-southeast-1"
           ]
       }
   ]
}

C’est à nouveau un script python qui permet de lire les informations de configuration et de créer les fichiers Terraform contenant les différentes règles et les appels au module de création de policies, à partir de modèles jinja2.

Customer_waf_v2.jinja2 :

resource "aws_wafv2_rule_group" "{{bl_short_name}}_waf_v2" {
 count = "{{bl_short_name}}" == var.bl_short_name ? 1 : 0
 # required arguments
 name     = "waf-${var.naming_convention}-{{bl_short_name}}-rulegroups"
 capacity = {{capacity}}
 scope    = var.scope
 visibility_config {
   cloudwatch_metrics_enabled = false
   metric_name                = "{{bl_short_name}}-rule-metric"
   sampled_requests_enabled   = false
 }


 lifecycle {
   create_before_destroy = true
 }
  # optional arguments
 description = "Rule group from {{bl_name}}"
 {{rule_content}}
}

Le schéma suivant représente le fonctionnement du script python qui crée les règles custom pour les Business lines : 

Au final, avant l’application du code Terraform, la mise en place se fait de la manière suivante : 

  1. Récupérer les adresses IP du bucket S3
  2. Ecrire les adresses sous forme de liste dans le fichier `terraform.auto.tfvars`
  3. Obtenir toutes les régions à partir du fichier de configuration
  4. Effectuer le rendu des providers à partir du modèle `providers.jinja2`
  5. Ecrire les providers dans le fichier `10_main.tf`
  6. Effectuer le rendu des appels au module à partir du modèle `main.jinja2`
  7. Ecrire les appels au module dans le fichier `10_main.tf`
  8. Récupérer la liste des Business Lines ayant des règles personnalisées
  9. Lire le contenu des fichiers de règles à partir du répertoire `config/custom_rules/`
  10. Effectuer le rendu du bloc aws_wafv2_rule_group à partir du modèle `11_customer_waf_v2.jinja2`
  11. Ecrire chaque groupe de règles dans `modules/fms_policies/11_waf_v2.tf`
  12. Effectuer le rendu des appels au module à partir du modèle `main.jinja2`
  13. Ecrire les appels au module dans le fichier `10_main.tf`
  14. Ecrire la liste des Business Line exclues des policies standard dans le fichier `terraform.auto.tfvars`
  15.  Ecrire la liste des groupes de règles des Business Line dans `modules/fms_policies/11_waf_v2.tf` 

Synthèse

Livrables projet : 

  • Récupération des IP malicieuses et création des IP sets (IPV4 et IPV6)
  • Rétention des IP sur 30 jours
  • Création des policies communes à l’ensemble des BU (intégration de groupes de règles standards)
  • Gestion des policies personnalisées pour les BU (intégration à la fois des règles communes et des règles spécifiques à la BU)
  • Filtrage des OU de destination dans AWS Organization en fonction de l’environnement
  • Gestion des logs de gestion des règles centralisés via Amazon Kinesis Data Firehose dans un bucket AWS S3
  • Documentation projet et transfert de connaissances au référent DevSecOps client

Bénéfices client : 

  • Mise à jour automatique et quotidienne des IP à filtrer
  • Amélioration de la protection automatisée contre les menaces externes
  • Gains de temps pour les équipes OPS et diminution du risque d’erreur
  • Montée en compétence du client sur les services Amazon Kinesis Data Firehose, AWS WAF V2 et AWS Firewall Manager

Commentaires :

A lire également sur le sujet :