Managing Azure Application Gateway WAF with Terraform

Learn how to set up and manage Azure Application Gateway WAF using Terraform, including security rules and protection policies

Managing Azure Application Gateway WAF with Terraform

Azure Application Gateway WAF (Web Application Firewall) provides centralized protection for your web applications. This guide demonstrates how to set up and manage WAF using Terraform.

Video Tutorial

Learn more about managing Azure WAF with Terraform in this comprehensive video tutorial:

Prerequisites

  • Azure CLI configured with appropriate permissions
  • Terraform installed (version 1.0.0 or later)
  • Resource group created
  • Understanding of web security concepts

Project Structure

terraform-azure-waf/
├── main.tf
├── variables.tf
├── outputs.tf
├── modules/
│   └── waf/
│       ├── main.tf
│       ├── variables.tf
│       └── outputs.tf
└── policies/
    └── security.json

WAF Configuration

Create modules/waf/main.tf:

# WAF Policy
resource "azurerm_web_application_firewall_policy" "main" {
  name                = "${var.project_name}-waf-policy"
  resource_group_name = var.resource_group_name
  location            = var.location

  custom_rules {
    name      = "BlockCountries"
    priority  = 1
    rule_type = "MatchRule"
    action    = "Block"

    match_conditions {
      match_variables {
        variable_name = "RemoteAddr"
      }
      operator           = "GeoMatch"
      negation_condition = false
      match_values       = ["CN", "RU"]
    }
  }

  custom_rules {
    name      = "BlockIPRange"
    priority  = 2
    rule_type = "MatchRule"
    action    = "Block"

    match_conditions {
      match_variables {
        variable_name = "RemoteAddr"
      }
      operator           = "IPMatch"
      negation_condition = false
      match_values       = ["192.168.1.0/24", "10.0.0.0/24"]
    }
  }

  custom_rules {
    name      = "BlockUserAgent"
    priority  = 3
    rule_type = "MatchRule"
    action    = "Block"

    match_conditions {
      match_variables {
        variable_name = "RequestHeaders"
        selector      = "User-Agent"
      }
      operator           = "Contains"
      negation_condition = false
      match_values       = ["bad-bot", "malicious-scanner"]
    }
  }

  policy_settings {
    enabled                     = true
    mode                       = "Prevention"
    request_body_check         = true
    file_upload_limit_in_mb    = 100
    max_request_body_size_in_kb = 128
  }

  managed_rules {
    managed_rule_set {
      type    = "OWASP"
      version = "3.2"

      rule_group_override {
        rule_group_name = "REQUEST-920-PROTOCOL-ENFORCEMENT"
        rule {
          id      = "920300"
          enabled = true
          action  = "Block"
        }
        rule {
          id      = "920330"
          enabled = true
          action  = "Block"
        }
      }

      rule_group_override {
        rule_group_name = "REQUEST-930-APPLICATION-ATTACK-LFI"
        rule {
          id      = "930100"
          enabled = true
          action  = "Block"
        }
      }

      rule_group_override {
        rule_group_name = "REQUEST-942-APPLICATION-ATTACK-SQLI"
        disabled_rules = ["942100", "942110", "942120"]
      }
    }

    managed_rule_set {
      type    = "Microsoft_BotManagerRuleSet"
      version = "1.0"
    }
  }

  tags = var.tags
}

# Application Gateway
resource "azurerm_application_gateway" "main" {
  name                = "${var.project_name}-appgw"
  resource_group_name = var.resource_group_name
  location            = var.location

  sku {
    name     = "WAF_v2"
    tier     = "WAF_v2"
    capacity = 2
  }

  gateway_ip_configuration {
    name      = "gateway-ip-configuration"
    subnet_id = azurerm_subnet.frontend.id
  }

  frontend_port {
    name = "http"
    port = 80
  }

  frontend_port {
    name = "https"
    port = 443
  }

  frontend_ip_configuration {
    name                 = "frontend"
    public_ip_address_id = azurerm_public_ip.main.id
  }

  backend_address_pool {
    name = "backend"
  }

  backend_http_settings {
    name                  = "http"
    cookie_based_affinity = "Disabled"
    port                  = 80
    protocol             = "Http"
    request_timeout      = 60
    probe_name           = "probe"
  }

  probe {
    name                = "probe"
    host                = "example.com"
    interval            = 30
    timeout             = 30
    unhealthy_threshold = 3
    protocol            = "Http"
    port                = 80
    path                = "/health"
    match {
      status_code = ["200-399"]
    }
  }

  http_listener {
    name                           = "http"
    frontend_ip_configuration_name = "frontend"
    frontend_port_name             = "http"
    protocol                       = "Http"
  }

  ssl_certificate {
    name     = "ssl-cert"
    data     = filebase64("${path.module}/cert.pfx")
    password = var.cert_password
  }

  ssl_policy {
    policy_type = "Predefined"
    policy_name = "AppGwSslPolicy20170401S"
  }

  request_routing_rule {
    name                       = "rule1"
    rule_type                  = "Basic"
    http_listener_name         = "http"
    backend_address_pool_name  = "backend"
    backend_http_settings_name = "http"
    priority                   = 1
  }

  waf_configuration {
    enabled                  = true
    firewall_mode           = "Prevention"
    rule_set_type           = "OWASP"
    rule_set_version        = "3.2"
    file_upload_limit_mb    = 100
    max_request_body_size_kb = 128
  }

  firewall_policy_id = azurerm_web_application_firewall_policy.main.id

  zones = ["1", "2", "3"]

  tags = var.tags
}

# SSL Certificate
resource "azurerm_key_vault_certificate" "main" {
  name         = "${var.project_name}-cert"
  key_vault_id = var.key_vault_id

  certificate_policy {
    issuer_parameters {
      name = "Self"
    }

    key_properties {
      exportable = true
      key_size   = 2048
      key_type   = "RSA"
      reuse_key  = true
    }

    lifetime_action {
      action {
        action_type = "AutoRenew"
      }

      trigger {
        days_before_expiry = 30
      }
    }

    secret_properties {
      content_type = "application/x-pkcs12"
    }

    x509_certificate_properties {
      key_usage = [
        "cRLSign",
        "dataEncipherment",
        "digitalSignature",
        "keyAgreement",
        "keyCertSign",
        "keyEncipherment",
      ]

      subject            = "CN=example.com"
      validity_in_months = 12

      subject_alternative_names {
        dns_names = ["example.com", "www.example.com"]
      }
    }
  }
}

Monitoring Configuration

  1. Diagnostic Settings
resource "azurerm_monitor_diagnostic_setting" "waf" {
  name                       = "${var.project_name}-diag"
  target_resource_id        = azurerm_application_gateway.main.id
  log_analytics_workspace_id = var.log_analytics_workspace_id

  log {
    category = "ApplicationGatewayAccessLog"
    enabled  = true

    retention_policy {
      enabled = true
      days    = 30
    }
  }

  log {
    category = "ApplicationGatewayFirewallLog"
    enabled  = true

    retention_policy {
      enabled = true
      days    = 30
    }
  }

  metric {
    category = "AllMetrics"
    enabled  = true

    retention_policy {
      enabled = true
      days    = 30
    }
  }
}

# WAF Alerts
resource "azurerm_monitor_metric_alert" "waf_blocked" {
  name                = "${var.project_name}-waf-blocked-alert"
  resource_group_name = var.resource_group_name
  scopes              = [azurerm_application_gateway.main.id]
  description         = "Alert when WAF blocks requests"

  criteria {
    metric_namespace = "Microsoft.Network/applicationGateways"
    metric_name      = "BlockedReqCount"
    aggregation      = "Total"
    operator         = "GreaterThan"
    threshold        = 100
  }

  action {
    action_group_id = var.action_group_id
  }
}

resource "azurerm_monitor_metric_alert" "waf_latency" {
  name                = "${var.project_name}-waf-latency-alert"
  resource_group_name = var.resource_group_name
  scopes              = [azurerm_application_gateway.main.id]
  description         = "Alert when WAF latency is high"

  criteria {
    metric_namespace = "Microsoft.Network/applicationGateways"
    metric_name      = "ApplicationGatewayTotalTime"
    aggregation      = "Average"
    operator         = "GreaterThan"
    threshold        = 1000
  }

  action {
    action_group_id = var.action_group_id
  }
}

Best Practices

  1. Security

    • Enable Prevention mode
    • Configure custom rules
    • Monitor attacks
    • Update policies
  2. Performance

    • Use v2 SKU
    • Configure caching
    • Monitor latency
    • Scale appropriately
  3. High Availability

    • Use multiple zones
    • Configure health probes
    • Monitor status
    • Implement backup
  4. Cost Optimization

    • Choose right SKU
    • Monitor usage
    • Optimize rules
    • Review metrics

Advanced Features

  1. Custom Error Pages
resource "azurerm_storage_account" "error_pages" {
  name                     = "${var.project_name}errorpages"
  resource_group_name      = var.resource_group_name
  location                 = var.location
  account_tier             = "Standard"
  account_replication_type = "LRS"

  static_website {
    index_document = "index.html"
    error_404_document = "404.html"
  }

  tags = var.tags
}

Conclusion

You’ve learned how to set up and manage Azure Application Gateway WAF using Terraform. This setup provides:

  • Application protection
  • Custom rules
  • Traffic monitoring
  • Security policies

Remember to:

  • Monitor attacks
  • Update rules
  • Review configurations
  • Maintain security