Deploying Fortigate Firewalls in Azure Transit Firenet

Introduction

There have been some blog articles already created describing the deployment of Fortigate Firewalls in Aviatrix Transit Firenet, incl. the best one that I have seen so far: Ricardo Trentin’s one. Nevertheless, I wanted to share additional pieces of information that might be informative.

The purpose of this post is to show:

  • how to create Firenet from normal Aviatrix Transit VNET in Azure
  • how to deploy Fortigate using specific image ID
  • how to perform initial setup of Frotigate Firewalls

Fortigate in Azure Firenet - overview

Aviatrix Firenet with Fortigate Firewalls - diagram
Aviatrix Firenet with Fortigate Firewalls - diagram

The Aviatrix mc-firenet module will be used for deploying Firewalls in Azure Transit. The mc-firenet module must be used in conjunction with mc-transit module. Hence, It is required to deploy Transit VNET using mc-transit module first.

The mc-transit module creates VNET in Azure with few subnets (those subnets can be seen in the diagram above). From Fortigate FW1’s perspective the most important ones are:
• “-Public-FW-ingress-egress-1” where Network Interface of FW1 port1 (Internet/WAN) is placed
• “-dmz-firewall-lan” where Network Interface of FW1 port 2 (LAN) is placed
In case of FW2 those subnets are called “-Public-FW-ingress-egress-2” and “-hagw-dmz-firewall-lan” respectively.

1 - Deploying the Fortigate Firewalls using Terraform

I have created Transit VNET using the following TF code:

module "mc-transit" { source = "terraform-aviatrix-modules/mc-transit/aviatrix" version = "2.4.0" cloud = "Azure" cidr = "10.11.0.0/16" region = "East US" account = "Azure-Jakub" enable_transit_firenet = true instance_size = "Standard_B2ms" }

Now, it is time to create Fortigate Firewalls (two of them) using mc-firenet module. As you may notice below, we will use data source to gather the CIDR used by “-Public-FW-ingress-egress-1” subnet. The same CIDR is used for both “egress_cidr” and “mgmt_cidr” as there is no separate mgmt interface in Fortigate.

data "azurerm_subnet" "public_ingress_egress_firewall_subnet" { name = "${module.mc-transit.vpc.name}-Public-FW-ingress-egress-1" virtual_network_name = module.mc-transit.vpc.name resource_group_name = module.mc-transit.vpc.resource_group } module "mc-firenet" { source = "terraform-aviatrix-modules/mc-firenet/aviatrix" version = "1.4.0" #required: transit_module = module.mc-transit firewall_image = "Fortinet FortiGate (PAYG_20190624) Next-Generation Firewall Latest Release" # name as it appears in Aviatrix Controller GUI: Firewall Network -> Setup -> Firewall -> step 2a, field: Firewall Image #optional: custom_fw_names = ["Azure-FW1", "Azure-FW2"] #firewall_image_version = "7.0.5" # Aviatrix Controller GUI: Firewall Network -> Setup -> Firewall -> step 2a, field: Firewall Image Version firewall_image_id = "fortinet:fortinet_fortigate-vm_v5:fortinet_fg-vm_payg_2022:7.0.9" # if firewall_image_id is used -> argument firewall_image_version must be commented out fw_amount = "2" egress_cidr = data.azurerm_subnet.public_ingress_egress_firewall_subnet.address_prefix # Public-FW-ingress-egress-1 subnet CIDR for FW az-1 egress_enabled = true # default = false, once set to true a default route will be generated by Transit GW and advertised everywhere inspection_enabled = true instance_size = "Standard_D3_v2" mgmt_cidr = data.azurerm_subnet.public_ingress_egress_firewall_subnet.address_prefix # Public-FW-ingress-egress-1 subnet CIDR for FW az-1 username = "fwadmin" # default username is fwadmin password = "kuba1234!" }

Please notice that if you want to use the FortiOS version that is not listed in Controller GUI, you must use the “firewall_image_id” argument instead of “firewall_Image_version” (as shown above).

Creating REST_API User

The REST_API user will be created using Fortigate GUI. The user will have Admin Profile assigned.

Let’s create Admin Profile “api_admin” first:

Fortigate - Admin Profile creation
Fortigate - Admin Profile creation

The set of permissions depends on the pieces of configuration you want to modify later with Terraform, e.g.

  • interface/port – requires Read/Write for Network/Configuration
  • (optionally) static route – requires Read/Write for Network/Router
  • (optionally) policy – requires Read/Write for Firewall/Policy
Fortigate - Required Permissions
Fortigate - Required Permissions

Once Admin Profile is there, the REST-API user can be created:

Fortigate - creation of REST_API user
Fortigate - creation of REST_API user
Fortigate - creation of REST_API user
Fortigate - creation of REST_API user

Please copy the Token for the API-admin user and store it in a safe place. It will be required in the configuration later.

Fortigate Provider in Terraform

The “fortios” is the name of the provider to be used in Terraform.

There are two Firewalls deployed => which means two Tokens => and two provider sections must be configured. The Terraform “alias” will be used to distinguish between FW-1 and FW-2.

The “azurerm” provider is required by data sources used later.

terraform { required_providers { aviatrix = { source = "AviatrixSystems/aviatrix" version = "3.0.0" } azurerm = { source = "hashicorp/azurerm" version = ">= 2.39" } fortios = { source = "fortinetdev/fortios" version = "1.16.0" } } } provider "aviatrix" { # Aviatrix Controller username = var.controller_username password = var.controller_password controller_ip = var.controller_ip } provider "fortios" { hostname = "" # IP of the FW token = "" # token from FW GUI for REST API admin. data source can be used with a reference to Key Vault or Secrets Manager insecure = "true" alias = "fw1" } provider "fortios" { hostname = "" # IP of the FW token = "" # token from FW GUI for REST API admin. data source can be used with a reference to Key Vault or Secrets Manager insecure = "true" alias = "fw2" } provider "azurerm" { features {} }

Fortigate Interface Configuration using Terraform

The WAN interface (port1) is configured properly by default. Though port2 (LAN interface) configuration must be adjusted (and its default config varies between FortiOS versions).

Use the following TF code to configure port2 (Read/Write permission for Network/Configuration is required):

# Configure port2 (LAN) for Firewall Instance [0] resource "fortios_system_interface" "fw1_lan_intf" { provider = fortios.fw1 algorithm = "L4" defaultgw = "disable" #default gateway from dhcp must be disabled alias = "lan-int" mtu = 1500 mtu_override = "disable" name = "port2" type = "physical" vdom = "root" mode = "dhcp" allowaccess = "https" #https required by Azure LB Health Checks depends_on = [module.mc-firenet] } # Configure port2 (LAN) for Firewall Instance [1] resource "fortios_system_interface" "fw2_lan_intf" { provider = fortios.fw2 algorithm = "L4" defaultgw = "disable" #default gateway from dhcp must be disabled alias = "lan-int" mtu = 1500 mtu_override = "disable" name = "port2" type = "physical" vdom = "root" mode = "dhcp" allowaccess = "https" #https required by Azure LB Health Checks depends_on = [module.mc-firenet] }

Aviatrix Vendor Integration using Terraform

The “Vendor Integration” feature configures static routes (for RFC1918 and Azure LB HealthCheck) on Fortigate.

Please notice that a Fortigate token for REST_API is required.

And REST_API user must have the following permission: Read/Write for both Network/Router Network/Configuration.

# Fortigates tokens can be stored in Key Vault or Secret Manager and referenced as data source # Remark: appropriate KeyVault Access Policy must be set data "azurerm_key_vault_secret" "fortigate11token" { name = "fortigate11token" key_vault_id = "/subscriptions/0820a232-012e-4b20-844e-64047cbbaf20/resourceGroups/fw_yara_kv/providers/Microsoft.KeyVault/vaults/fwyarakv" # ID from JSON view of KeyVault } data "azurerm_key_vault_secret" "fortigate12token" { name = "fortigate12token" key_vault_id = "/subscriptions/0820a232-012e-4b20-844e-64047cbbaf20/resourceGroups/fw_yara_kv/providers/Microsoft.KeyVault/vaults/fwyarakv" # ID from JSON view of KeyVault } data "aviatrix_firenet_vendor_integration" "fw1" { vpc_id = module.mc-transit.transit_gateway.vpc_id instance_id = module.mc-firenet.aviatrix_firewall_instance[0].instance_id vendor_type = "Fortinet FortiGate" # "Generic", "Palo Alto Networks VM-Series", "Aviatrix FQDN Gateway" and "Fortinet FortiGate" public_ip = module.mc-firenet.aviatrix_firewall_instance[0].public_ip username = "apiadmin" # REST_API user password = module.mc-firenet.aviatrix_firewall_instance[0].password api_token = data.azurerm_key_vault_secret.fortigate11token.value #"z5Qh5jp1jkz47Qmg4mHtHQr1j3g9gn" # Fortigate REST_API user token for FW1 firewall_name = module.mc-firenet.aviatrix_firewall_instance[0].firewall_name save = true #synchronize = true # "save" and "synchronize" cannot be invoked at the same time } data "aviatrix_firenet_vendor_integration" "fw2" { vpc_id = module.mc-transit.transit_gateway.vpc_id instance_id = module.mc-firenet.aviatrix_firewall_instance[1].instance_id vendor_type = "Fortinet FortiGate" # "Generic", "Palo Alto Networks VM-Series", "Aviatrix FQDN Gateway" and "Fortinet FortiGate" public_ip = module.mc-firenet.aviatrix_firewall_instance[1].public_ip username = "apiadmin" # REST_API user password = module.mc-firenet.aviatrix_firewall_instance[1].password api_token = data.azurerm_key_vault_secret.fortigate12token.value #"NHcwt5kg94j1nHb1bw94jN30j4Nzmn" # Fortigate REST_API user token for FW2 firewall_name = module.mc-firenet.aviatrix_firewall_instance[1].firewall_name save = true #synchronize = true # "save" and "synchronize" cannot be invoked at the same time }

The following routes are configured after successful Vendor Integration:

Fortigate - routes created by Aviatrix Vendor Integration
Fortigate - routes created by Aviatrix Vendor Integration

Optional: Static Routes configuration using TF instead of Aviatrix Vendor Integration

Please notice that if you do not want to use Aviatrix Vendor Integration (though I cannot see any reason for not using it) you can create all the static routes using thefortios resource called “fortios_router_static”. Example below:

############################################################################## # Optional: Static Route # Static Routes will be configured via Aviatrix Vendor Integration feature # However, if required, static route can be configured using TF code # Example of the code is presented below ############################################################################## # Data Sources are required in order to configure static route on FW with appropriate next-hop IP data "azurerm_subnet" "dmz_firewall_lan_1" { name = "av-gw-${module.mc-transit.vpc.name}-dmz-firewall-lan" virtual_network_name = module.mc-transit.vpc.name resource_group_name = module.mc-transit.vpc.resource_group } data "azurerm_subnet" "dmz_firewall_lan_2" { name = "av-gw-${module.mc-transit.vpc.name}-hagw-dmz-firewall-lan" virtual_network_name = module.mc-transit.vpc.name resource_group_name = module.mc-transit.vpc.resource_group } # Configure static route for Azure LB Health Checks resource "fortios_router_static" "fw1_route_azurelb_hc" { provider = fortios.fw1 comment = "Route for Azure Load Balancer Health Check" device = "port2" # fixed port/interface number distance = 10 dst = "168.63.129.16 255.255.255.255" # fixed IP address 168.63.129.16 255.255.255.255 dynamic_gateway = "disable" gateway = "${tonumber(split(".", split("/", data.azurerm_subnet.dmz_firewall_lan_1.address_prefix)[0])[0])}.${tonumber(split(".", split("/", data.azurerm_subnet.dmz_firewall_lan_1.address_prefix)[0])[1])}.${tonumber(split(".", split("/", data.azurerm_subnet.dmz_firewall_lan_1.address_prefix)[0])[2])}.${tonumber(split(".", split("/", data.azurerm_subnet.dmz_firewall_lan_1.address_prefix)[0])[3]) + 1}" #"10.11.0.81" #next-hop equals to dmz-fiewall-lan virtual router IP x.x.x.81, or hagw-dmz-fiewall-lan virtual router IP x.x.x.129. Expression takes subnet_range and adds +1 to the last octet src = "0.0.0.0 0.0.0.0" status = "enable" depends_on = [module.mc-firenet] } # Configure static route for Azure LB Health Checks resource "fortios_router_static" "fw2_route_azurelb_hc" { provider = fortios.fw2 comment = "Route for Azure Load Balancer Health Check" device = "port2" # fixed port/interface number distance = 10 dst = "168.63.129.16 255.255.255.255" # fixed IP address dynamic_gateway = "disable" gateway = "${tonumber(split(".", split("/", data.azurerm_subnet.dmz_firewall_lan_2.address_prefix)[0])[0])}.${tonumber(split(".", split("/", data.azurerm_subnet.dmz_firewall_lan_2.address_prefix)[0])[1])}.${tonumber(split(".", split("/", data.azurerm_subnet.dmz_firewall_lan_2.address_prefix)[0])[2])}.${tonumber(split(".", split("/", data.azurerm_subnet.dmz_firewall_lan_2.address_prefix)[0])[3]) + 1}" #"10.11.0.81" #next-hop equals to dmz-fiewall-lan virtual router IP x.x.x.81, or hagw-dmz-fiewall-lan virtual router IP x.x.x.129. Expression takes subnet_range and adds +1 to the last octet src = "0.0.0.0 0.0.0.0" status = "enable" depends_on = [module.mc-firenet] }

Optional: Policy Rules creation using Terraform

The following code presents how to create Policy rule using Terraform.

############################################################################## # Optional: Fortigate Default LAN allow any/any Policy for East-West traffic ############################################################################## resource "fortios_firewall_policy" "fw1_default_lan_allow" { provider = fortios.fw1 name = "Default LAN to LAN Allow All" srcaddr { name = "all" } srcintf { name = "port2" } dstaddr { name = "all" } dstintf { name = "port2" } service { name = "ALL" } action = "accept" logtraffic = "all" logtraffic_start = "enable" # for loggin traffic when the session starts nat = "disable" status = "enable" } resource "fortios_firewall_policy" "fw2_default_lan_allow" { provider = fortios.fw2 name = "Default LAN to LAN Allow All" srcaddr { name = "all" } srcintf { name = "port2" } dstaddr { name = "all" } dstintf { name = "port2" } service { name = "ALL" } action = "accept" logtraffic = "all" logtraffic_start = "enable" # for loggin traffic when the session starts nat = "disable" status = "enable" }

2 - Verification

Azure LB Health Check probes

Once the Static Route for Azure LB HealthCheck is created (either using Aviatrix Vendor Integration or Terraform resource) you can check the Health Check status in Azure Portal.

Go to Load Balancer -> select the proper LB -> Metrics

Change the Metric to Health Probe Status

Health Checks must be successful for Azure Load Balancer to send traffic to Fortigate FWs.

Fortigate - Azure LB Health Checks
Fortigate - Azure LB Health Checks

Logs for Local Traffic

To check that Health Probes are getting to the Firewall itself, we must enable proper logging for Local Traffic.

Go to Log & Report -> Log Settings

Change Local Traffic Log to “All”

Fortigate - enabling Local Traffic Logs
Fortigate - enabling Local Traffic Logs

Now you can go to Log & Report -> Local Traffic

Click on “Add Filter”, select Destination, and put Azure LB HealthCheck IP 168.63.129.16

Fortigate - checking Local Traffic Logs
Fortigate - checking Local Traffic Logs

Leave a Reply

Your email address will not be published. Required fields are marked *