Shore Up
Two office buildings joined by a sealed pipeline running underground between them, with a padlock clamped over the pipe at each end.
FirewallsSecurity

Route-Based Site-to-Site IPsec VPN Between Two FortiGates

Ketan Aagja9 min read
No ratings yet

Before you run this

This guide builds a route-based IPsec tunnel between two FortiGate firewalls so the LAN behind Site A can reach the LAN behind Site B and vice versa. "Route-based" means each end gets a virtual tunnel interface, and you steer traffic into the tunnel with a static route plus firewall policies — the mainstream FortiOS approach for a permanent office-to-office link.

You need super_admin / read-write admin access on both FortiGates. You will edit VPN config, static routes, and firewall policies on each unit.

Because you are touching routing and policy on a production firewall:

  • Keep a separate out-of-band or console connection open (serial console or an SSH session sourced from an interface you are not about to reroute). A bad static route or a policy that shadows your management access can lock you out of the GUI/SSH and cut production traffic at the same time.
  • Back up the running config on both units first. GUI: System > Configuration > Backup. CLI: execute backup config to TFTP/USB. If you have config revisions enabled (System > Settings > Configuration Revisions / stored on disk), take a fresh revision now — that is your one-click rollback path via Revisions > Revert.
  • Do this in a maintenance window. Bringing the tunnel up is additive, but the static route and firewall policies can change how existing traffic is forwarded.
  • Test the settings on a lab pair or a test VDOM before you touch production if you can. Read every command before you paste it.

Nothing here is silently destructive, but a mistyped static route or an over-broad policy is a production incident. The undo section at the end reverses everything cleanly, and a config restore reverses it completely.

Assumptions

I'm writing for FortiOS 7.4 (the CLI below is the same across the 7.x line, but menu labels drift, so trust the CLI over my GUI hints). Both firewalls have a static public IP on their WAN interface and are the routing gateway for their own LAN. I'm using IKEv2, a single pair of subnets, and pre-shared-key authentication.

Concrete example values — replace all of these:

Site A Site B
WAN interface wan1 wan1
Public IP 198.51.100.1 203.0.113.1
LAN subnet 10.10.0.0/24 10.20.0.0/24
LAN interface lan lan

The pre-shared key is shown as <pre-shared-key>. Use the same long random string on both ends, and set it out of band, not in a ticket.

Step 1 — Address objects (both sites)

Create objects for the local and remote LANs. On Site A:

config firewall address
    edit "SiteA_LAN"
        set subnet 10.10.0.0 255.255.255.0
    next
    edit "SiteB_LAN"
        set subnet 10.20.0.0 255.255.255.0
    next
end

On Site B, create the same two objects — the definitions are identical, only which one is "local" differs.

Step 2 — Phase 1 (IKE gateway)

This is the interface tunnel and the peer definition. The proposal, DH group, and PSK must match exactly on both ends or the tunnel never comes up.

Site A:

config vpn ipsec phase1-interface
    edit "vpn-to-SiteB"
        set interface "wan1"
        set ike-version 2
        set peertype any
        set proposal aes256-sha256
        set dhgrp 14
        set remote-gw 203.0.113.1        # Site B's public IP
        set psksecret <pre-shared-key>
    next
end

Site B is the mirror — same name-your-own convention, pointing back at Site A:

config vpn ipsec phase1-interface
    edit "vpn-to-SiteA"
        set interface "wan1"
        set ike-version 2
        set peertype any
        set proposal aes256-sha256
        set dhgrp 14
        set remote-gw 198.51.100.1       # Site A's public IP
        set psksecret <pre-shared-key>
    next
end

The tunnel interface takes the name you gave the phase1 (vpn-to-SiteB / vpn-to-SiteA). You'll reference that name in the route and the policies.

If you later need more control over how the tunnel interface behaves (for example the net-device and add-route settings, which matter mostly with multiple selectors or dynamic routing), check the FortiOS Administration Guide under IPsec VPN concepts before changing them — I'm leaving them at their defaults here because a single static route-based tunnel doesn't need them touched.

Step 3 — Phase 2 (selectors)

Bind the local and remote subnets. Proposal must again match on both ends.

Site A:

config vpn ipsec phase2-interface
    edit "vpn-to-SiteB-p2"
        set phase1name "vpn-to-SiteB"
        set proposal aes256-sha256
        set src-subnet 10.10.0.0 255.255.255.0
        set dst-subnet 10.20.0.0 255.255.255.0
    next
end

Site B — source and destination swap:

config vpn ipsec phase2-interface
    edit "vpn-to-SiteA-p2"
        set phase1name "vpn-to-SiteA"
        set proposal aes256-sha256
        set src-subnet 10.20.0.0 255.255.255.0
        set dst-subnet 10.10.0.0 255.255.255.0
    next
end

Step 4 — Static route

Tell each FortiGate to reach the far LAN through the tunnel interface.

Site A:

config router static
    edit 0
        set dst 10.20.0.0 255.255.255.0
        set device "vpn-to-SiteB"
    next
end

Site B:

config router static
    edit 0
        set dst 10.10.0.0 255.255.255.0
        set device "vpn-to-SiteA"
    next
end

edit 0 lets FortiOS assign the next free route ID.

Step 5 — Firewall policies (both directions)

Route-based VPN carries no traffic until policy allows it. You need one policy each way. Site A:

config firewall policy
    edit 0
        set name "SiteA-to-SiteB"
        set srcintf "lan"
        set dstintf "vpn-to-SiteB"
        set srcaddr "SiteA_LAN"
        set dstaddr "SiteB_LAN"
        set action accept
        set schedule "always"
        set service "ALL"
    next
    edit 0
        set name "SiteB-to-SiteA"
        set srcintf "vpn-to-SiteB"
        set dstintf "lan"
        set srcaddr "SiteB_LAN"
        set dstaddr "SiteA_LAN"
        set action accept
        set schedule "always"
        set service "ALL"
    next
end

Site B is the mirror: swap the interfaces and the source/destination objects. Tighten service from ALL to only what the sites actually need once the link is proven.

Do not NAT this traffic — leave NAT off on these policies (the default when you don't set set nat enable), otherwise the far side sees translated addresses and return routing breaks.

GUI equivalent

If you prefer the GUI, VPN > IPsec Wizard > Site to Site walks through phase 1/2, the route, and the policies in one flow, then you refine the result under VPN > IPsec Tunnels. The CLI above is what the wizard produces, minus the wizard's naming. I use the CLI because it's reproducible and easy to diff.

Verify it worked

Bring the tunnel up by sending traffic, or force it. From Site A CLI:

diagnose vpn tunnel list                 # phase2 SAs, tunnel status
get vpn ipsec tunnel summary             # one-line up/down per tunnel
diagnose vpn ike gateway list            # phase1 / IKE state

Then generate real traffic from the LAN side by sourcing a ping from the internal interface:

execute ping-options source 10.10.0.1
execute ping 10.20.0.1

Use the actual Site A LAN interface IP and a live host at Site B. In the GUI, Dashboard > Network > IPsec (or VPN > IPsec Tunnels) should show the tunnel Up with byte counters climbing once traffic flows.

If phase 1 won't come up, run diagnose debug application ike -1 then diagnose debug enable on one unit while triggering traffic, and watch for proposal/PSK mismatch messages. Turn it off with diagnose debug disable. Mismatched proposals, DH groups, or a typo'd PSK are the usual cause.

Undo / rollback

If you took a config revision or backup before starting, restoring it is the cleanest reversal: System > Configuration > Revisions > Revert, or execute restore config.

To remove the pieces manually on each unit, delete them in reverse order of the dependency chain — policies first, then route, then phase 2, then phase 1:

config firewall policy
    delete <policy-id>
end
config router static
    delete <route-id>
end
config vpn ipsec phase2-interface
    delete "vpn-to-SiteB-p2"
end
config vpn ipsec phase1-interface
    delete "vpn-to-SiteB"
end

Find the numeric policy and route IDs first with show firewall policy and show router static. Do the mirror deletions on the other FortiGate. The address objects are harmless to leave, but you can remove them once nothing references them.

Written by
Ketan Aagja

Runs enterprise networks and security for a living, and writes Shore Up to turn two decades of hands-on Linux, Windows and mail-server work into guides you can actually use.

More about the author →

Was this article helpful?

Tap a star — no sign-in needed.

Be the first to rate this article.