Source Code

In the last post we covered configuring a single FortiGate named Core-DC. In this post we need to configure two FortiGates that will act as hubs in the SDWAN lab. The current design of the lab looks like this.

LAB Design

LAB

Using Modules

Hub1 and Hub1 are what we are going to focus on in this post. The majority of the configuration is the same on the two devices, the main differences are the IP addresses. In order to avoid cut and pasting the resources, we are going to use a terraform module. Hashicorp has this to say about modules: Modules are the main way to package and reuse resource configurations with Terraform.

The current directory structure looks like this.

.
├── hub1
│   ├── main.tf
│   ├── outputs.tf
│   ├── terraform.tfvars
│   └── variables.tf
├── hub2
│   ├── main.tf
│   ├── outputs.tf
│   ├── terraform.tfvars
│   └── variables.tf
└── modules
    └── advpn_hub
        ├── firewall.tf
        ├── main.tf
        ├── outputs.tf
        ├── router.tf
        ├── system.tf
        ├── variables.tf
        └── vpn.tf

Here are the main.tf files for Hub1 and Hub2.

Hub1 - main.tf

terraform {
  required_providers {
    fortios = {
      source = "fortinetdev/fortios"
    }
  }
}

provider "fortios" {
  hostname = var.management
  token    = var.token
  insecure = "true"
}

module "advpn_hub" {
  source = "../modules/advpn_hub"

  hostname    = "Hub1"
  loopback_ip = "172.28.255.200 255.255.255.255"
  wan1_ip     = "198.51.100.200 255.255.255.0"
  wan2_ip     = "192.88.99.200 255.255.255.0"
  port3_itr p    = "172.28.0.1 255.255.255.252"
  vpn1 = {
    prefix        = "192.168.100.0 255.255.255.0"
    ip            = "192.168.100.253 255.255.255.255"
    remote_ip     = "192.168.100.254 255.255.255.0"
    ipv4_start_ip = "192.168.100.1"
    ipv4_end_ip   = "192.168.100.252"
    ipv4_netmask  = "255.255.255.0"
    network_id    = "1"
    aspath        = "65000"
  }
  vpn2 = {
    prefix        = "192.168.99.0 255.255.255.0"
    ip            = "192.168.99.253 255.255.255.255"
    remote_ip     = "192.168.99.254 255.255.255.0"
    ipv4_start_ip = "192.168.99.1"
    ipv4_end_ip   = "192.168.99.252"
    ipv4_netmask  = "255.255.255.0"
    network_id    = "2"
    aspath        = "65000 65000"
  }
  bgp = {
    neighbor        = "172.28.0.2"
    neighbor_range1 = "192.168.100.0 255.255.255.0"
    neighbor_range2 = "192.168.99.0 255.255.255.0"
  }
}

Hub2 - main.tf

terraform {
  required_providers {
    fortios = {
      source = "fortinetdev/fortios"
      source = "fortios.billgrant.io/local/fortios"
    }
  }
}

provider "fortios" {
  hostname = var.management
  token    = var.token
  insecure = "true"
}

module "advpn_hub" {
  source = "../modules/advpn_hub"

  hostname    = "Hub2"
  loopback_ip = "172.28.255.201 255.255.255.255"
  wan1_ip     = "198.51.100.201 255.255.255.0"
  wan2_ip     = "192.88.99.201 255.255.255.0"
  port3_ip    = "172.28.0.5 255.255.255.252"
  vpn1 = {
    prefix        = "192.168.98.0 255.255.255.0"
    ip            = "192.168.98.253 255.255.255.255"
    remote_ip     = "192.168.98.254 255.255.255.0"
    ipv4_start_ip = "192.168.98.1"
    ipv4_end_ip   = "192.168.98.252"
    ipv4_netmask  = "255.255.255.0"
    network_id    = "1"
    aspath        = "65000"
  }
  vpn2 = {
    prefix        = "192.168.97.0 255.255.255.0"
    ip            = "192.168.97.253 255.255.255.255"
    remote_ip     = "192.168.97.254 255.255.255.0"
    ipv4_start_ip = "192.168.97.1"
    ipv4_end_ip   = "192.168.97.252"
    ipv4_netmask  = "255.255.255.0"
    network_id    = "2"
    aspath        = "65000 65000"
  }
  bgp = {
    neighbor        = "172.28.0.6"
    neighbor_range1 = "192.168.98.0 255.255.255.0"
    neighbor_range2 = "192.168.97.0 255.255.255.0"
  }
}

The important part here is this

module "advpn_hub" {
  source = "../modules/advpn_hub"

This tells terraform to use the module in the advpn_hub. We can also pass variables onto the module. So lets take a look at the modules code.

advpn_hub module - main.tf

terraform {
  required_providers {
    fortios = {
      source = "fortinetdev/fortios"
    }
  }
}

Not much here because the provider configuration get passed onto the module from the respective hubs main.tf The other important part of the modules is the variables.tf.

advpn_hub module - variables.tf

variable "hostname" {
  description = "FortiGate system hostname"
  type        = string
}

variable "loopback_ip" {
  description = "Loopback (lo0) interface IP address"
  type        = string
}

variable "wan1_ip" {
  description = "WAN1 interface IP address"
  type        = string
}

variable "wan2_ip" {
  description = "WAN2 interface IP address"
  type        = string
}

variable "port3_ip" {
  description = "port3 interface IP address"
  type        = string
}

variable "vpn1" {
  description = "VPN1 specific parameters"
  type = object({
    prefix        = string
    ip            = string
    remote_ip     = string
    ipv4_start_ip = string
    ipv4_end_ip   = string
    ipv4_netmask  = string
    network_id    = string
    aspath        = string
  })
}

variable "vpn2" {
  description = "VPN2 specific parameters"
  type = object({
    prefix        = string
    ip            = string
    remote_ip     = string
    ipv4_start_ip = string
    ipv4_end_ip   = string
    ipv4_netmask  = string
    network_id    = string
    aspath        = string
  })
}

variable "bgp" {
  type = object({
    neighbor        = string
    neighbor_range1 = string
    neighbor_range2 = string
  })
}

When we run terraform apply on each of the hubs it will pass the variables from hub’s main.tf to the module.

Resource configuration

We have 33 total resources so I am not going to cover all of them. We will focus on the vpn settings.

advpn_hub module - vpn.tf

resource "fortios_vpnipsec_phase1interface" "VPN1_P1" {
  add_route             = "disable"
  auto_discovery_sender = "enable"
  dpd                   = "on-idle"
  dpd_retryinterval     = "60"
  ike_version           = "2"
  interface             = "WAN1"
  ipv4_end_ip           = var.vpn1.ipv4_end_ip
  ipv4_netmask          = var.vpn1.ipv4_netmask
  ipv4_start_ip         = var.vpn1.ipv4_start_ip
  mode_cfg              = "enable"
  name                  = "VPN1"
  net_device            = "disable"
  peertype              = "any"
  psksecret             = "fortinet"
  proposal              = "aes256-sha256"
  network_overlay       = "enable"
  network_id            = var.vpn1.network_id
  type                  = "dynamic"
  fec_ingress           = "enable"
  fec_egress            = "enable"
  depends_on            = [fortios_system_interface.WAN1]
}

resource "fortios_vpnipsec_phase2interface" "VPN1_P2" {
  phase1name = fortios_vpnipsec_phase1interface.VPN1_P1.name
  proposal   = "aes256-sha256"
  name       = "VPN1"
  depends_on = [fortios_vpnipsec_phase1interface.VPN1_P1]
}

resource "fortios_vpnipsec_phase1interface" "VPN2_P1" {
  add_route             = "disable"
  auto_discovery_sender = "enable"
  dpd                   = "on-idle"
  dpd_retryinterval     = "60"
  ike_version           = "2"
  interface             = "WAN2"
  ipv4_end_ip           = var.vpn2.ipv4_end_ip
  ipv4_netmask          = var.vpn2.ipv4_netmask
  ipv4_start_ip         = var.vpn2.ipv4_start_ip
  mode_cfg              = "enable"
  name                  = "VPN2"
  net_device            = "disable"
  peertype              = "any"
  psksecret             = "fortinet"
  proposal              = "aes256-sha256"
  network_overlay       = "enable"
  network_id            = var.vpn2.network_id
  type                  = "dynamic"
  fec_ingress           = "enable"
  fec_egress            = "enable"
  depends_on            = [fortios_system_interface.WAN2]
}

resource "fortios_vpnipsec_phase2interface" "VPN2_P2" {
  phase1name = fortios_vpnipsec_phase1interface.VPN2_P1.name
  proposal   = "aes256-sha256"
  name       = "VPN2"
  depends_on = [fortios_vpnipsec_phase1interface.VPN2_P1]
}

Something to not here. You cannot create the Phase2 interface on FortiGate unless the referenced Phase1 interface exists. That is where depends_on comes in. If we look at the code for "fortios_vpnipsec_phase2interface" "VPN2_P2".

resource "fortios_vpnipsec_phase2interface" "VPN2_P2" {
  phase1name = fortios_vpnipsec_phase1interface.VPN2_P1.name
  proposal   = "aes256-sha256"
  name       = "VPN2"
  depends_on = [fortios_vpnipsec_phase1interface.VPN2_P1]
}

The depends_on in the above code tells terraform to create VPN2_P1 before creating VPN2_P2. This helps ensure the command will not fail.

Running Terraform apply

The two hubs are only configured with a management IP and an API key. Lets take a look at the hubs current IPSec tunnels.

Hub1 Tunnels

HUB1

Hub2 Tunnels

HUB2

As we can see they are both blank. Okay lets run terraform apply

Hub1

Terraform will perform the following actions:

...

# module.advpn_hub.fortios_vpnipsec_phase1interface.VPN1_P1 will be created
  + resource "fortios_vpnipsec_phase1interface" "VPN1_P1" {
      + add_route                 = "disable"
      + auto_discovery_sender     = "enable"
      + dpd                       = "on-idle"
      + dpd_retryinterval         = "60"
      + dynamic_sort_subtable     = "false"
      + fec_egress                = "enable"
      + fec_ingress               = "enable"
      + ike_version               = "2"
      + interface                 = "WAN1"
      + ipv4_end_ip               = "192.168.100.252"
      + ipv4_netmask              = "255.255.255.0"
      + ipv4_start_ip             = "192.168.100.1"
      + mode_cfg                  = "enable"
      + name                      = "VPN1"
      + net_device                = "disable"
      + network_id                = 1
      + network_overlay           = "enable"
      + peertype                  = "any"
      + proposal                  = "aes256-sha256"
      + psksecret                 = (sensitive value)
      + type                      = "dynamic"
    }

  # module.advpn_hub.fortios_vpnipsec_phase1interface.VPN2_P1 will be created
  + resource "fortios_vpnipsec_phase1interface" "VPN2_P1" {
      + add_route                 = "disable"
      + auto_discovery_sender     = "enable"
      + dpd                       = "on-idle"
      + dpd_retryinterval         = "60"
      + dynamic_sort_subtable     = "false"
      + fec_egress                = "enable"
      + fec_ingress               = "enable"
      + ike_version               = "2"
      + interface                 = "WAN2"
      + ipv4_end_ip               = "192.168.99.252"
      + ipv4_netmask              = "255.255.255.0"
      + ipv4_start_ip             = "192.168.99.1"
      + mode_cfg                  = "enable"
      + name                      = "VPN2"
      + net_device                = "disable"
      + network_id                = 2
      + network_overlay           = "enable"
      + peertype                  = "any"
      + proposal                  = "aes256-sha256"
      + psksecret                 = (sensitive value)
      + type                      = "dynamic"
    }

  # module.advpn_hub.fortios_vpnipsec_phase2interface.VPN1_P2 will be created
  + resource "fortios_vpnipsec_phase2interface" "VPN1_P2" {
      + name                     = "VPN1"
      + phase1name               = "VPN1"
      + proposal                 = "aes256-sha256"
    }

  # module.advpn_hub.fortios_vpnipsec_phase2interface.VPN2_P2 will be created
  + resource "fortios_vpnipsec_phase2interface" "VPN2_P2" {
      + name                     = "VPN2"
      + phase1name               = "VPN2"
      + proposal                 = "aes256-sha256"
    }

Plan: 33 to add, 0 to change, 0 to destroy.

...

module.advpn_hub.fortios_vpnipsec_phase1interface.VPN2_P1: Creation complete after 0s [id=VPN2]
module.advpn_hub.fortios_vpnipsec_phase1interface.VPN1_P1: Creation complete after 0s [id=VPN1]
module.advpn_hub.fortios_vpnipsec_phase2interface.VPN2_P2: Creation complete after 0s [id=VPN2]
module.advpn_hub.fortios_vpnipsec_phase2interface.VPN1_P2: Creation complete after 0s [id=VPN1]

...

Apply complete! Resources: 33 added, 0 changed, 0 destroyed.

Hub2

Terraform will perform the following actions:

...

 # module.advpn_hub.fortios_vpnipsec_phase1interface.VPN1_P1 will be created
  + resource "fortios_vpnipsec_phase1interface" "VPN1_P1" {
      + add_route                 = "disable"
      + auto_discovery_sender     = "enable"
      + dpd                       = "on-idle"
      + dpd_retryinterval         = "60"
      + dynamic_sort_subtable     = "false"
      + fec_egress                = "enable"
      + fec_ingress               = "enable"
      + ike_version               = "2"
      + interface                 = "WAN1"
      + ipv4_end_ip               = "192.168.98.252"
      + ipv4_netmask              = "255.255.255.0"
      + ipv4_start_ip             = "192.168.98.1"
      + mode_cfg                  = "enable"
      + name                      = "VPN1"
      + net_device                = "disable"
      + network_id                = 1
      + network_overlay           = "enable"
      + peertype                  = "any"
      + proposal                  = "aes256-sha256"
      + type                      = "dynamic"
    }

  # module.advpn_hub.fortios_vpnipsec_phase1interface.VPN2_P1 will be created
  + resource "fortios_vpnipsec_phase1interface" "VPN2_P1" {
      + add_route                 = "disable"
      + auto_discovery_sender     = "enable"
      + dpd                       = "on-idle"
      + dpd_retryinterval         = "60"
      + dynamic_sort_subtable     = "false"
      + fec_egress                = "enable"
      + fec_ingress               = "enable"
      + ike_version               = "2"
      + interface                 = "WAN2"
      + ipv4_end_ip               = "192.168.97.252"
      + ipv4_netmask              = "255.255.255.0"
      + ipv4_start_ip             = "192.168.97.1"
      + mode_cfg                  = "enable"
      + name                      = "VPN2"
      + net_device                = "disable"
      + network_id                = 2
      + network_overlay           = "enable"
      + peertype                  = "any"
      + proposal                  = "aes256-sha256"
      + type                      = "dynamic"
    }

  # module.advpn_hub.fortios_vpnipsec_phase2interface.VPN1_P2 will be created
  + resource "fortios_vpnipsec_phase2interface" "VPN1_P2" {
      + name                     = "VPN1"
      + phase1name               = "VPN1"
      + proposal                 = "aes256-sha256"
    }

  # module.advpn_hub.fortios_vpnipsec_phase2interface.VPN2_P2 will be created
  + resource "fortios_vpnipsec_phase2interface" "VPN2_P2" {
      + name                     = "VPN2"
      + phase1name               = "VPN2"
      + proposal                 = "aes256-sha256"
    }

Plan: 33 to add, 0 to change, 0 to destroy.

...

module.advpn_hub.fortios_vpnipsec_phase1interface.VPN2_P1: Creation complete after 0s [id=VPN2]
module.advpn_hub.fortios_vpnipsec_phase1interface.VPN1_P1: Creation complete after 0s [id=VPN1]
module.advpn_hub.fortios_vpnipsec_phase2interface.VPN1_P2: Creation complete after 1s [id=VPN1]
module.advpn_hub.fortios_vpnipsec_phase2interface.VPN2_P2: Creation complete after 1s [id=VPN2]

...

Apply complete! Resources: 33 added, 0 changed, 0 destroyed.

Lets take a look at the IPsec tunnels

Hub1 Configured

hub1_configured

Hub2 Configured

hub2_configured

Everything looks good. If some day I needed to add a third hub, it would be easy as creating a few commands and the variables. In the next post I am going to configure the branches and add them to FortiManager. Here is the source code used in this post.