No description
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-08-07 10:37:59 +02:00
.gitignore Add FortiGate HA hub-and-spoke Terraform deployment 2026-08-07 10:37:59 +02:00
.terraform.lock.hcl Add FortiGate HA hub-and-spoke Terraform deployment 2026-08-07 10:37:59 +02:00
00-variables.auto.tfvars.example Add FortiGate HA hub-and-spoke Terraform deployment 2026-08-07 10:37:59 +02:00
01-provider.tf Add FortiGate HA hub-and-spoke Terraform deployment 2026-08-07 10:37:59 +02:00
02-config.tf Add FortiGate HA hub-and-spoke Terraform deployment 2026-08-07 10:37:59 +02:00
03-fortigate-infra.tf Add FortiGate HA hub-and-spoke Terraform deployment 2026-08-07 10:37:59 +02:00
04-fortigate-network.tf Add FortiGate HA hub-and-spoke Terraform deployment 2026-08-07 10:37:59 +02:00
05-fortigate-appliance1.tf Add FortiGate HA hub-and-spoke Terraform deployment 2026-08-07 10:37:59 +02:00
06-fortigate-appliance2.tf Add FortiGate HA hub-and-spoke Terraform deployment 2026-08-07 10:37:59 +02:00
07-spoke-vm.tf Add FortiGate HA hub-and-spoke Terraform deployment 2026-08-07 10:37:59 +02:00
HubSpokeForti.drawio Add FortiGate HA hub-and-spoke Terraform deployment 2026-08-07 10:37:59 +02:00
HubSpokeForti.png Add FortiGate HA hub-and-spoke Terraform deployment 2026-08-07 10:37:59 +02:00
README.md Add FortiGate HA hub-and-spoke Terraform deployment 2026-08-07 10:37:59 +02:00

FortiGate HA on STACKIT

This directory contains the Terraform configuration for deploying a High Availability (HA) cluster of two FortiGate firewalls on the STACKIT cloud platform. The configuration includes the networks, virtual machines, volumes, interfaces, routing tables and security groups required by the deployment.

Hub-and-spoke architecture with FortiGate HA

The editable source is available in HubSpokeForti.drawio.

Content Readme


Directory Structure

01-provider.tf

This file initializes the STACKIT Terraform Provider. It defines which cloud platform is being managed and specifies the version to be used. The latest version is always selected; in the specific deployment, Version 0.69.0 was used.

02-config.tf

This is where the most important configuration variables are defined, determining the target project and hardware specifications. Parameters include project_id, the path to the service_account_key_path, and default settings for default_region, default_az, and the VM flavor (e.g., m2i.2).

03-fortigate-infra.tf

This file creates the STACKIT projects and imports the local FortiOS image from fortios.qcow2.

04-fortigate-network.tf

Here, the four fortigate-specific networks (Management, LAN, Synchronization, WAN) and the associated components are configured:

  • fortigate_mgmt_net
  • fortigate_lan_net
  • fortigate_sync_net
  • fortigate_wan_net
  • A VIP (Virtual IP) Interface for HA operation. The VIP should not be attached to any VM.
  • A Public IP address for external access (e.g., WAN).
  • The necessary Security Groups and Rules to control traffic.

05-fortigate-appliance1.tf and 06-fortigate-appliance2.tf

These files each define one of the two FortiGate VMs that form the HA cluster. The configuration includes:

  • Creation of the boot volume for the VM.
  • Creation of the VM itself.
  • Creation and correct sequential attachment of the four network interfaces:
    1. Management (mgmt)
    2. WAN (wan)
    3. LAN (lan)
    4. Synchronization (sync)

Use the STACKIT portal or CLI to retrieve the cloud NIC MAC addresses, then compare them with get system interface physical in FortiOS.


Prerequisites

Before deploying the fortigate HA construct, ensure the following tools and assets are available:

1. Software Tools

  • Terraform CLI: You need the Terraform Command Line Interface installed and configured to execute the Infrastructure-as-Code files (*.tf).
  • STACKIT CLI (Optional): The STACKIT Command Line Interface is not required by Terraform, but is useful for inspecting projects and network interfaces.

2. FortiGate Assets

  • FortiOS image: Place the licensed QCOW2 image at ./fortios.qcow2, or update local_file_path in 03-fortigate-infra.tf.
  • FortiGate licenses: Each VM needs a valid, compatible license that supports the intended HA deployment.

Infrastructure Guide

Step 1: Prepare the Environment

  1. Clone the Repository:
    git clone <repository-url>
    cd fortigate-HA
    
  2. Provide the FortiOS Image:
    • Place the QCOW2 file at ./fortios.qcow2, or adjust local_file_path in 03-fortigate-infra.tf.

Step 2: Configure Terraform Variables

  1. Create Variables File:

    • The required variables are defined in 02-config.tf. To provide the values, create a new file named 00-variables.auto.tfvars in the root directory.
    • Fill in the values based on your project and environment:
  2. Configure STACKIT Service Account:

    • Generate a STACKIT Service Account Key (JSON format) and place the path to this key in the service_account_key_path variable.
  3. Configure Terraform Backend:

    • Recommendation: For production environments, it is highly recommended to configure a remote backend (e.g., a STACKIT Object Storage Bucket) in 01-provider.tf to store the tfstate securely. Alternatively, you can use the default local backend configuration for testing purposes.

Step 3: Execute Terraform

  1. Initialize Terraform:
    • Ensure that you are in the project folder (cd fortigate-HA).
    • Initialize the working directory and download the necessary providers (including the STACKIT provider).
    terraform init
    
  2. Review the Plan:
    • Check the infrastructure changes Terraform intends to make.
    terraform plan
    
  3. Apply the Configuration:
    • Execute the plan to deploy all resources (Networks, Images, Volumes, VMs/Appliances).
    terraform apply
    
    • Wait until the process is complete and the VMs are fully provisioned and booted.

Step 4: Initial FortiGate Appliance Configuration

After successful deployment, the two fortigate Appliances need an initial configuration via the console to enable management access.

The commands in this guide use placeholders instead of deployment-specific IP addresses. Replace them with the values from 00-variables.auto.tfvars or, for dynamically allocated addresses, with the values shown in STACKIT:

Placeholder Source
<FGT1_MGMT_IP> / <FGT2_MGMT_IP> fortigate1_mgmt_ip / fortigate2_mgmt_ip
<MGMT_NETMASK> IPv4 netmask derived from fortigate_mgmt_net_range
<FGT1_SYNC_IP> / <FGT2_SYNC_IP> Current STACKIT addresses of the sync NICs
<SYNC_NETMASK> IPv4 netmask derived from fortigate_sync_net_range
<WAN_VIP> fortigate_wan_vip
<WAN_NETMASK> IPv4 netmask derived from fortigate_wan_net_range
<PUBLIC_IP> Public IP assigned by STACKIT
<STACKIT_WAN_GATEWAY> Gateway of fortigate_wan_net shown by STACKIT

For example, a Terraform range ending in /24 corresponds to the FortiOS netmask 255.255.255.0. Do not copy addresses from an example deployment after changing the Terraform network ranges or IP variables.

  1. Access the Console:

    • In the STACKIT Portal, access the Console of Appliance 1 (fortigate-appliance1).
  2. Initial Login:

    • Log in as admin. On a new FortiGate image the password is normally empty and FortiOS prompts for a new password immediately. Image-specific defaults take precedence.
  3. Configure Management Interface:

    • Use the FortiOS CLI from the VM console.
    • Identify the interface corresponding to the management network by comparing its MAC address with the STACKIT network interface. In an unchanged deployment this is port1.
    • Configure the node-specific address from fortigate_mgmt_net. On Appliance 1, use:
    config system interface
        edit "port1"
            set mode static
            set ip <FGT1_MGMT_IP> <MGMT_NETMASK>
            set allowaccess ping https ssh
        next
    end
    
  4. Repeat for Appliance 2:

    • Perform the same initial console configuration for Appliance 2 (fortigate-appliance2), using <FGT2_MGMT_IP> and <MGMT_NETMASK>.
  5. Access Web Interface:

    • Use a jump host VM within fortigate_mgmt_net, or a device with an appropriate route, to access https://<FGT1_MGMT_IP> or https://<FGT2_MGMT_IP>. HTTPS uses port 443 unless admin-sport was changed.

FortiGate HA Configuration Guide

This deployment uses a two-node FortiGate FGCP active-passive cluster. STACKIT is a cloud/KVM environment with anti-spoofing controls, so use a Layer 3 unicast heartbeat. Do not use the default broadcast heartbeat: it uses non-IP Layer 2 EtherTypes and normally applies FGCP virtual MAC addresses.

Cluster formation and public VIP failover are separate acceptance tests. A healthy two-member FGCP cluster does not by itself prove that the STACKIT upstream network will move a VIP or Public IP to the new primary.

1. Prerequisites

Before enabling HA, verify on both appliances:

get system status
get system interface physical
get system ha status

Both appliances must have:

  • The same FortiGate VM model, FortiOS version and build.
  • Valid, compatible FortiGate VM licenses with HA support.
  • The same operating mode and VDOM layout.
  • Different serial numbers and hostnames.
  • Four attached NICs with the expected MAC addresses.

For an unchanged Terraform deployment, attachment order is:

FortiGate interface STACKIT network Purpose
port1 fortigate_mgmt_net Per-node management
port2 fortigate_wan_net WAN and public VIP traffic
port3 fortigate_lan_net Protected LAN traffic
port4 fortigate_sync_net HA heartbeat and synchronization

Always verify the mapping by MAC address. Attachment order can change if NICs are manually detached and reattached.

The STACKIT addresses assigned to the sync NICs must also be configured inside FortiOS. They are allocated dynamically and can change with every deployment. This guide uses the following placeholders:

  • <FGT1_SYNC_IP>: current IPv4 address of nic_sync_fortigate1
  • <FGT2_SYNC_IP>: current IPv4 address of nic_sync_fortigate2

Retrieve the current assignments from the STACKIT portal or CLI and replace every placeholder before entering the commands in FortiOS.

On FortiGate 1:

config system interface
    edit "port4"
        set mode static
        set ip <FGT1_SYNC_IP> <SYNC_NETMASK>
        set allowaccess ping
    next
end

On FortiGate 2:

config system interface
    edit "port4"
        set mode static
        set ip <FGT2_SYNC_IP> <SYNC_NETMASK>
        set allowaccess ping
    next
end

Test in both directions with an explicit source address:

execute ping-options reset
execute ping-options source <LOCAL_SYNC_IP>
execute ping <PEER_SYNC_IP>

Do not continue until this works. If it fails, run the following command on both nodes while sending pings:

diagnose sniffer packet port4 'arp or icmp' 4 0 l

The Terraform sync NICs have STACKIT port security disabled, so a security-group rule should not be needed. No gateway is required between addresses in the same sync subnet.

3. Configure active-passive unicast HA

Use the same group name, group ID and password on both nodes. Replace the example password.

FortiGate 1:

config system global
    set hostname "fortigate-node1"
end

config system ha
    set mode a-p
    set group-name "STACKIT-FGT-HA"
    set group-id 10
    set password "REPLACE-WITH-SAME-STRONG-PASSWORD"
    set hbdev "port4" 50
    set unicast-hb enable
    set unicast-hb-peerip <FGT2_SYNC_IP>
    set monitor "port2" "port3"
    set priority 200
    set override disable
    set session-pickup enable
    set session-pickup-connectionless enable
    set gratuitous-arps enable
    set arps 10
    set arps-interval 1
end

FortiGate 2:

config system global
    set hostname "fortigate-node2"
end

config system ha
    set mode a-p
    set group-name "STACKIT-FGT-HA"
    set group-id 10
    set password "REPLACE-WITH-SAME-STRONG-PASSWORD"
    set hbdev "port4" 50
    set unicast-hb enable
    set unicast-hb-peerip <FGT1_SYNC_IP>
    set monitor "port2" "port3"
    set priority 100
    set override disable
    set session-pickup enable
    set session-pickup-connectionless enable
    set gratuitous-arps enable
    set arps 10
    set arps-interval 1
end

Unicast HA is important for two reasons:

  1. It transports heartbeat traffic as IP unicast instead of relying on cloud support for Fortinet's Layer 2 heartbeat EtherTypes.
  2. Fortinet documents that unicast HA does not replace the VM interfaces' hypervisor-assigned MAC addresses with HA virtual MAC addresses.

override disable avoids automatic failback and an additional disruption when the preferred node returns. Enable override only if deterministic failback to the higher-priority node is a requirement.

4. Verify cluster formation

Run on both nodes:

get system ha status
diagnose sys ha status
diagnose sys ha dump-by group

The result must show one primary, one secondary, port4 as the heartbeat device, and an in-sync configuration. Use CLI status as the authority; some FortiOS versions have had cosmetic GUI role-reporting defects.

Normal interface configuration is synchronized by FGCP. Configure port1 as an HA reserved management interface if both nodes must retain separately reachable management addresses after joining the cluster.

5. Configure the WAN route

STACKIT can supply a default route through the management interface (port1) by DHCP with administrative distance 5. A less-preferred WAN route causes replies to Public IP traffic to leave through port1; FortiOS then drops the asymmetric flow during its reverse-path check.

Configure the default route through port2 with distance 1. Replace the example gateway with the actual gateway of fortigate_wan_net:

config router static
    edit 1
        set dst 0.0.0.0 0.0.0.0
        set gateway <STACKIT-WAN-GATEWAY>
        set device "port2"
        set distance 1
        set status enable
    next
end

If port1 remains a DHCP client, prevent it from installing a default route:

config system interface
    edit "port1"
        set defaultgw disable
    next
end

Verify that the active default route uses port2 and has distance 1:

get router info routing-table details 0.0.0.0

6. Configure the STACKIT virtual IP

The infrastructure uses the OpenStack/OVN virtual-port pattern. The addresses below refer to the placeholders defined in Step 4 and therefore remain valid when the Terraform values change:

  1. The unbound STACKIT NIC VIP reserves private address <WAN_VIP>.
  2. STACKIT Public IP <PUBLIC_IP> is bound to that NIC and translated 1:1 to <WAN_VIP>.
  3. Both FortiGate WAN NICs contain the exact <WAN_VIP>/32 allowed address, allowing either hypervisor-assigned MAC to source the VIP.
  4. OVN binds the virtual port to the active FortiGate after receiving a valid ARP ownership announcement.

The validated FortiOS configuration requires the private VIP in both places:

  1. As a Secondary IP on port2, so the FortiGate locally owns and processes traffic addressed to <WAN_VIP>.
  2. As an IP Pool with ARP replies enabled, so FortiOS announces ownership correctly during HA failover.

Configure the Secondary IP on the active cluster member; FGCP synchronizes the interface configuration:

config system interface
    edit "port2"
        set secondary-IP enable
        config secondaryip
            edit 1
                set ip <WAN_VIP> <WAN_NETMASK>
                set allowaccess ping
            next
        end
    next
end

Configure the same address as an IP Pool. Do not replace this setup with a FortiOS firewall vip that uses a dummy DNAT target:

config firewall ippool
    edit "STACKIT_WAN_VIP"
        set type overload
        set startip <WAN_VIP>
        set endip <WAN_VIP>
        set arp-reply enable
        set arp-intf "port2"
        set associated-interface "port2"
    next
end

Both configurations are required in the tested deployment. The Secondary IP makes <WAN_VIP> a local interface destination, while the IP Pool with arp-reply enable provides the ARP ownership behavior needed for OVN virtual-port rebinding. Increasing the HA GARP count from the default five to ten improves failover robustness but replaces neither configuration:

config system ha
    set gratuitous-arps enable
    set arps 10
    set arps-interval 1
end

Only enable the pool in a firewall policy if outbound sessions must be source-NATed to <WAN_VIP>:

config firewall policy
    edit <OUTBOUND-POLICY-ID>
        set nat enable
        set ippool enable
        set poolname "STACKIT_WAN_VIP"
    next
end

The Public IP is implemented by STACKIT and must not be configured inside FortiOS. Internet traffic reaches port2 with destination <WAN_VIP> after STACKIT performs DNAT.

Verify the effective settings:

show full-configuration firewall ippool
show full-configuration system interface port2
show full-configuration system ha
diagnose hardware deviceinfo nic port2

Current_HWaddr must equal Permanent_HWaddr and the STACKIT-assigned WAN vNIC MAC on the active node.

7. Failover acceptance test

  1. Confirm that both members are in sync.

  2. Start a continuous external ping, a long TCP download through the firewall, and any required VPN test.

  3. Capture ARP on WAN and confirm that <WAN_VIP> is announced:

    diagnose sniffer packet port2 'arp' 6 0 l
    
  4. Stop or power off the current primary VM.

  5. Record election time, first GARP time, Public IP recovery time, packet loss and session survival.

  6. Confirm that the new primary transmits with its own hypervisor-assigned MAC.

  7. Start the former primary again and confirm it rejoins as secondary without disrupting traffic when override is disabled.

The new primary must send GARP for <WAN_VIP> using its own STACKIT WAN vNIC MAC. The deployment passes only if cluster election, OVN virtual-port rebinding, Public IP convergence, and the required session types all succeed. A successful FGCP role change alone is not sufficient.

References