Last updated on: 2023-10-18 04:01:19.

Scenario #4: Manage an F5 BIG-IP Advanced WAF Policy on different devices

The goal of this lab is to manage an F5 BIG-IP Advanced WAF Policy on multiple devices, including:

  • Different standalone devices, serving the same applications.
  • Different devices serving different purposes; for example, changes tested first in a BIG-IP QA/test environment before applying them into a production environment.

Pre-requisites

On the BIG-IP:

  • BIG-IP version 16.1 or newer
  • Advanced WAF Provisioned
  • Credentials with REST API access

On Terraform:

Create a policy

  1. Create 4 files:

    variables.tf
    1
    2
    3
    4
    variable qa_bigip {}
    variable prod_bigip {}
    variable username {}
    variable password {}
    

    inputs.auto.tfvars
    1
    2
    3
    4
    qa_bigip = "10.1.1.9:443"
    prod_bigip = "10.1.1.8:443"
    username = "admin"
    password = "whatIsYourBigIPPassword?"
    

    main.tf
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    terraform {
      required_providers {
        bigip = {
          source = "F5Networks/bigip"
          version = "1.15"
        }
      }
    }
    provider "bigip" {
      alias    = "qa"
      address  = var.qa_bigip
      username = var.username
      password = var.password
    }
    provider "bigip" {
      alias    = "prod"
      address  = var.prod_bigip
      username = var.username
      password = var.password
    }
    
    data "http" "scenario4" {
      url = "https://raw.githubusercontent.com/fchmainy/awaf_tf_docs/main/0.Appendix/scenario4.json"
      request_headers = {
         Accept = "application/json"
      }
    }
    
    resource "bigip_waf_policy" "s4_qa" {
        provider               = bigip.qa
        application_language = "utf-8"
        partition            = "Common"
        name                 = "scenario4"
        template_name        = "POLICY_TEMPLATE_FUNDAMENTAL"
        type                 = "security"
        policy_import_json   = data.http.scenario4.body
    }
    
    resource "bigip_waf_policy" "s4_prod" {
        provider               = bigip.prod
        application_language = "utf-8"
        partition            = "Common"
        name                 = "scenario4"
        template_name        = "POLICY_TEMPLATE_FUNDAMENTAL"
        type                 = "security"
        policy_import_json   = data.http.scenario4.body
    }
    

    Note

    The template name can be set to anything. When it is imported, the value is overwritten.

    This example references an existing policy from a GitHub repository, but you can also create a policy from zero on both BIG-IPs.

  2. Initialize, plan, and apply your new Terraform project.

    foo@bar:~$ terraform init
    Initializing the backend...
    
    Initializing provider plugins...
    [...]
    Terraform has been successfully initialized!
    
    foo@bar:~$ terraform plan -out scenario4 > output_scenario4.1
    foo@bar:~$ more output_scenario4.1
    foo@bar:~$ terraform apply "scenario4"
    
  3. For both BIG-IPs, verify that the two policies are present and consistent.

Simulate a WAF Policy workflow

A common workflow includes the following processes:

  1. Enforce attack signatures in the test environment.
  2. Verify that these changes do not break the application and identify potential false positives (FP).
  3. Finalize the changes in a test environment, before applying them to your production environment.

Enforce attack signatures on the test environment

  1. In order to track attack signature changes, use a Terraform HCL map to add a signature list definition in the inputs.auto.tfvars file:

    signatures = {
        200101559 = {
            signature_id    = 200101559
            description     = "src http: (Header)"
            enabled         = true
            perform_staging = false
        }
        200101558 = {
            signature_id    = 200101558
            description     = "src http: (Parameter)"
            enabled         = true
            perform_staging = false
        }
        200003067 = {
            signature_id    = 200003067
            description     = "\"/..namedfork/data\" execution attempt (Headers)"
            enabled         = true
            perform_staging = false
        }
        200003066 = {
            signature_id    = 200003066
            description     = "\"/..namedfork/data\" execution attempt (Parameters)"
            enabled         = true
            perform_staging = false
        }
        200003068 = {
            signature_id    = 200003068
            description     = "\"/..namedfork/data\" execution attempt (URI)"
            enabled         = true
            perform_staging = false
        }
    }
    

  2. Create a signatures.tf file with a map to all the attack signatures defined, previously:

    variable "signatures" {
      type = map(object({
            signature_id    = number
        enabled              = bool
        perform_staging      = bool
            description     = string
      }))
    }
    
    
    data "bigip_waf_signatures" "map_qa" {
      provider               = bigip.qa
      for_each               = var.signatures
      signature_id           = each.value["signature_id"]
      description            = each.value["description"]
      enabled                = each.value["enabled"]
      perform_staging        = each.value["perform_staging"]
    }
    
    data "bigip_waf_signatures" "map_prod" {
      provider           = bigip.prod
      for_each           = var.signatures
      signature_id       = each.value["signature_id"]
      description        = each.value["description"]
      enabled            = each.value["enabled"]
      perform_staging        = each.value["perform_staging"]
    }
    

    This example defines two different maps: one for the test environment BIG-IP and one for the production environment BIG-IP, because the bigip_waf_signatures data source are linked to their BIG-IP for consistency. Unlike the parameters and URLs data sources, which are just “json payload generators”, the attack signature data sources must first read the existence of the signature IDs and their status on the BIG-IP, before applying a configuration change.

  3. Update the main.tf file:

    resource "bigip_waf_policy" "s4_qa" {
        provider                 = bigip.qa
        application_language = "utf-8"
        partition            = "Common"
        name                 = "scenario4"
        template_name        = "POLICY_TEMPLATE_FUNDAMENTAL"
        type                 = "security"
        policy_import_json   = data.http.scenario4.body
        signatures           = [ for k,v in data.bigip_waf_signatures.map_qa: v.json ]
    }
    
    resource "bigip_waf_policy" "s4_prod" {
        provider                 = bigip.prod
        application_language = "utf-8"
        partition            = "Common"
        name                 = "scenario4"
        template_name        = "POLICY_TEMPLATE_FUNDAMENTAL"
        type                 = "security"
        policy_import_json   = data.http.scenario4.body
    }
    
  4. Plan and apply:

    foo@bar:~$ terraform plan -out scenario4 > output_scenario4.2
    foo@bar:~$ more output_scenario4.2
    foo@bar:~$ terraform apply "scenario4"
    

Verify the changes

You can verify that the 5 attack signatures have been enabled and enforced on the scenario4 WAF Policy on the QA BIG-IP (first 5 lines in the attack signatures list of the Advanced WAF Policy).

In the application, identify that these last changes on the test device have introduced some false positives. Using the log events on the F5 BIG-IP Advanced WAF GUI, identify the following:

  • The attack signature “200101558” should be disabled globally
  • The attack signature “200003068” should be disabled for the “/U1” URL
  • The attack signaure “200003067” should be enabled globally but disabled specifically for the parameter “P1”.

Finalize changes in the test environment

  1. Do the following, to final changes before moving into a production environment:

    inputs.auto.tfvars
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    signatures = {
        200101559 = {
            signature_id    = 200101559
            description     = "src http: (Header)"
            enabled         = true
            perform_staging = false
        }
        200101558 = {
            signature_id    = 200101558
            description     = "src http: (Parameter)"
            enabled         = false
            perform_staging = false
        }
        200003067 = {
            signature_id    = 200003067
            description     = "\"/..namedfork/data\" execution attempt (Headers)"
            enabled         = true
            perform_staging = false
        }
        200003066 = {
            signature_id    = 200003066
            description     = "\"/..namedfork/data\" execution attempt (Parameters)"
            enabled         = true
            perform_staging = false
        }
        200003068 = {
            signature_id    = 200003068
            description     = "\"/..namedfork/data\" execution attempt (URI)"
            enabled         = true
            perform_staging = false
        }
    }
    

    parameters.tf
    1
    2
    3
    4
    5
    6
    7
    8
    data "bigip_waf_entity_parameter" "P1" {
      name                            = "P1"
      type                            = "explicit"
      data_type                       = "alpha-numeric"
      perform_staging                 = true
      signature_overrides_disable     = [200003067]
      //url                           = data.bigip_waf_entity_url.U1
    }
    

    urls.tf
    1
    2
    3
    4
    5
    6
    data "bigip_waf_entity_url" "U1" {
      name                            = "/U1"
      type                            = "explicit"
      perform_staging                 = false
      signature_overrides_disable     = [200003068]
    }
    

  2. Update the main.tf file:

    main.tf
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    resource "bigip_waf_policy" "s4_qa" {
        provider               = bigip.qa
        application_language = "utf-8"
        partition            = "Common"
        name                 = "scenario4"
        template_name        = "POLICY_TEMPLATE_FUNDAMENTAL"
        type                 = "security"
        policy_import_json   = data.http.scenario4.body
        signatures             = [ for k,v in data.bigip_waf_signatures.map_qa: v.json ]
        parameters             = [data.bigip_waf_entity_parameter.P1.json]
        urls           = [data.bigip_waf_entity_url.U1.json]
    }
    
    resource "bigip_waf_policy" "s4_prod" {
        provider               = bigip.prod
        application_language = "utf-8"
        partition            = "Common"
        name                 = "scenario4"
        template_name        = "POLICY_TEMPLATE_FUNDAMENTAL"
        type                 = "security"
        policy_import_json   = data.http.scenario4.body
        signatures             = [ for k,v in data.bigip_waf_signatures.map_prod: v.json ]
        parameters             = [data.bigip_waf_entity_parameter.P1.json]
        urls           = [data.bigip_waf_entity_url.U1.json]
    }
    

  3. Plan and apply:

    foo@bar:~$ terraform plan -out scenario4 > output_scenario4.3
    foo@bar:~$ more output_scenario4.3
    foo@bar:~$ terraform apply "scenario4"
    

What’s Next?