Building a FortiManager Lab on Proxmox — Part 4: A Lab Edge FortiGate VM in Front of FortiManager
A FortiManager that’s directly internet-reachable is a bad idea. A FortiManager that has the same default route as your admin workstation is a worse idea. The whole point of a serious lab is to build the security boundary you’d build in production — and the obvious tool for that, on a Fortinet lab, is another Fortinet device. Part 4 builds a small FortiGate-VM that acts as the edge of the lab: gateway, firewall, NAT, FortiGuard pinhole, and the only device that touches both the lab subnets and the outside world.
By the end of this part, the topology looks like this:
┌────────────────────────────┐
home/office LAN ───────│ FGT-LAB-EDGE (FortiGate-VM)│
(vmbr0 or vmbr-fgd) │ port1 (wan) = DHCP │
│ port2 (mgmt) 10.20.10.1 │
│ port3 (transit) 10.20.30.1│
│ port4 (cust) 10.20.40.1 │
└─┬──────────┬──────────┬────┘
│ │ │
vmbr-mgmt │ │ vmbr-transit
│ │
┌────┴───┐ │
│ FMG │ │
│ 10.20. │ │
│ 10.10 │ │ (Part 5 attaches managed FGTs here)
└────────┘ └───────────────────────────────
The lab edge FGT is the only device with a default route to the outside. FortiManager’s outbound traffic — FortiGuard, support portal, NTP, DNS — is sourced from port2 (FortiGuard egress NIC) onto vmbr-fgd, hops to the FGT, gets policy-checked, gets NAT’d, and goes out the FGT’s wan1.
Step 1 — Build the FGT-VM shell
The KVM build of FortiGate-VM follows the same shape as Part 2 — it’s a .qcow2 plus a smaller log disk, downloaded from the support portal. For the lab edge I run FortiGate-VM 7.6.x, sized small:
qm create 9100 \
--name fgt-lab-edge \
--memory 4096 \
--cores 2 \
--sockets 1 \
--cpu host \
--machine q35 \
--bios seabios \
--ostype l26 \
--scsihw virtio-scsi-single \
--net0 virtio,bridge=vmbr-fgd,firewall=0 \
--net1 virtio,bridge=vmbr-mgmt,firewall=0 \
--net2 virtio,bridge=vmbr-transit,firewall=0 \
--net3 virtio,bridge=vmbr-cust,firewall=0 \
--serial0 socket \
--vga serial0
If you went with Pattern B in Part 3 (single physical NIC), substitute bridge=vmbr0 for net0. Everything else stays the same.
Import the disks:
cd /root/fgt-7.6.2
qm importdisk 9100 fortios.qcow2 local-lvm --format qcow2
qm importdisk 9100 datadrive.qcow2 local-lvm --format qcow2
qm set 9100 --scsi0 local-lvm:vm-9100-disk-0,discard=on,iothread=1,ssd=1
qm set 9100 --scsi1 local-lvm:vm-9100-disk-1,discard=on,iothread=1,ssd=1
qm set 9100 --boot order=scsi0
Note the NIC ordering matters because FortiOS maps port1 to the first virtio NIC (net0), port2 to the second, and so on. The mapping above is intentional:
| Proxmox NIC | Bridge | FortiGate port | Role |
|---|---|---|---|
| net0 | vmbr-fgd (or vmbr0) | port1 | WAN — only port with internet |
| net1 | vmbr-mgmt | port2 | Management VLAN — FMG, FAZ, admin |
| net2 | vmbr-transit | port3 | Transit to managed FGTs |
| net3 | vmbr-cust | port4 | Customer/LAN segment |
If you swap the order in qm create, every CLI snippet below is wrong. Stick to the order above.
Start the VM and attach a console:
qm start 9100
qm terminal 9100
Default login admin / blank. Set a strong password immediately.
Step 2 — Address the interfaces
WAN first — DHCP from your home network on Pattern A or B:
config system interface
edit "port1"
set mode dhcp
set role wan
set allowaccess ping
set defaultgw enable
next
end
set defaultgw enable is the key — DHCP-learned routes only become usable as default if this is on. Once it’s up, get system interface physical will show port1 with an IP and get router info routing-table all will show a default route via the DHCP gateway.
Now the lab-internal interfaces:
config system interface
edit "port2"
set ip 10.20.10.1 255.255.255.0
set role lan
set allowaccess ping ssh https
set description "MGMT — FMG, FAZ, admin workstation"
next
edit "port3"
set ip 10.20.30.1 255.255.255.0
set role lan
set allowaccess ping
set description "TRANSIT — managed FGTs WAN-side"
next
edit "port4"
set ip 10.20.40.1 255.255.255.0
set role lan
set allowaccess ping
set description "CUST — customer-side LAN"
next
end
allowaccess ping ssh https on port2 only — that’s the lab management plane and the only place I want admin access on the FGT itself. The transit and customer interfaces are ping-only, which is enough for diagnostics.
Step 3 — A small DHCP server on the management segment
So the FortiManager’s port1 (and any admin workstation you drop on vmbr-mgmt) gets an address without manual config:
config system dhcp server
edit 1
set interface "port2"
set default-gateway 10.20.10.1
set netmask 255.255.255.0
set dns-service default
config ip-range
edit 1
set start-ip 10.20.10.100
set end-ip 10.20.10.200
next
end
config reserved-address
edit 1
set ip 10.20.10.10
set mac AA:BB:CC:DD:EE:01
set description "FMG-LAB-01"
next
end
next
end
The reservation pins the FortiManager to 10.20.10.10 regardless of DHCP timing. Find the actual MAC with qm config 9001 | grep ^net0 — it’s after virtio=. Update the reservation accordingly.
Step 4 — Address objects and groups
Build the address objects you’ll reference in policy. Doing this once up front beats inlining IPs into every rule:
config firewall address
edit "net-mgmt"
set subnet 10.20.10.0 255.255.255.0
next
edit "net-fgd"
set subnet 10.20.20.0 255.255.255.0
next
edit "net-transit"
set subnet 10.20.30.0 255.255.255.0
next
edit "net-cust"
set subnet 10.20.40.0 255.255.255.0
next
edit "host-fmg"
set subnet 10.20.10.10 255.255.255.255
next
edit "host-admin-ws"
set subnet 10.20.10.50 255.255.255.255
next
end
config firewall addrgrp
edit "lab-internal"
set member "net-mgmt" "net-transit" "net-cust"
next
end
The lab-internal group is what you’d allow east-west; everything else in policy is purpose-specific.
Step 5 — Policies
The whole reason for the FGT-edge is the policy set. The minimum useful set:
config firewall policy
# 1. FMG outbound to FortiGuard / NTP / DNS / support portal
edit 10
set name "fmg-fortiguard-out"
set srcintf "port2"
set dstintf "port1"
set srcaddr "host-fmg"
set dstaddr "all"
set action accept
set service "DNS" "HTTPS" "FortiGuard-Web-Filter" "FortiGuard-AntiSpam" "PING" "NTP"
set schedule "always"
set logtraffic all
set nat enable
set comments "FMG -> internet, scoped to required services"
next
# 2. Admin workstation -> FMG GUI
edit 20
set name "admin-to-fmg"
set srcintf "port2"
set dstintf "port2"
set srcaddr "host-admin-ws"
set dstaddr "host-fmg"
set action accept
set service "HTTPS" "SSH" "PING"
set schedule "always"
set logtraffic all
next
# 3. Managed FGTs (transit) -> FMG (mgmt) for registration / FGFM
edit 30
set name "managed-fgt-to-fmg"
set srcintf "port3"
set dstintf "port2"
set srcaddr "net-transit"
set dstaddr "host-fmg"
set action accept
set service "HTTPS" "PING"
set service "FGFM"
set schedule "always"
set logtraffic all
set comments "FGFM = FortiGate-FortiManager protocol on TCP/541"
next
# 4. Managed FGTs -> internet (FortiGuard, NTP)
edit 40
set name "managed-fgt-out"
set srcintf "port3"
set dstintf "port1"
set srcaddr "net-transit"
set dstaddr "all"
set action accept
set service "DNS" "HTTPS" "PING" "NTP"
set schedule "always"
set logtraffic all
set nat enable
next
# 5. Customer LAN -> internet (everything)
edit 50
set name "cust-out"
set srcintf "port4"
set dstintf "port1"
set srcaddr "net-cust"
set dstaddr "all"
set action accept
set service "ALL"
set schedule "always"
set logtraffic all
set nat enable
set utm-status enable
set ssl-ssh-profile "certificate-inspection"
set webfilter-profile "default"
next
# 6. Implicit deny (always log denies in a lab — debugging gold)
edit 99
set name "log-everything-else"
set srcintf "any"
set dstintf "any"
set srcaddr "all"
set dstaddr "all"
set action deny
set service "ALL"
set schedule "always"
set logtraffic all
next
end
Read it from the top:
- Rule 10 is the FortiGuard pinhole. Note
host-fmgas the source — only the FortiManager can use this rule. A compromised admin workstation can’t piggy-back. The service list is scoped: DNS, HTTPS, NTP, ICMP, plus the FortiGuard-specific service objects FortiOS ships out of the box. No “service ALL” here — if FMG suddenly tries to talk to something on a non-standard port, it should fail loudly. - Rule 20 is intra-segment, on the same interface, but listed explicitly. Without it, intra-VLAN traffic on
port2between admin workstation and FMG is fine without policy on most FortiOS versions (intra-zone traffic permitted by default), but I prefer to make it explicit and logged. Setintra-zonedeny on the zone if you want zone-based enforcement, but for a lab the explicit policy is fine. - Rule 30 is the FGFM registration path. TCP/541 is the FortiGate-FortiManager protocol that handles registration, config sync, and policy installs. The Fortinet-supplied service object is named
FGFM. Without this rule, managed FGTs in the transit segment can’t talk to FMG. - Rule 40 lets the managed FGTs themselves reach FortiGuard. They need their own update path.
- Rule 50 is the customer LAN’s general internet egress with UTM enabled. Useful for testing FortiGuard categorisation behaviour from FMG-pushed profiles in Part 5.
- Rule 99 is the explicit deny with logging. Implicit deny exists, but FortiOS doesn’t log it by default. In a lab, log everything — denied traffic is the most useful signal you have when something doesn’t work.
Step 6 — What FortiManager actually needs to reach
A FortiManager doing real work talks to a surprising number of external services. The list, with the destinations the lab edge needs to permit:
| Service | Destination | Port | Required for |
|---|---|---|---|
| FortiGuard updates | update.fortiguard.net | TCP/443, TCP/8890 | AV / IPS / web-filter signature pulls |
| FortiGuard rating | *.fortiguard.net | TCP/443 | URL category lookups (GUI testing) |
| FortiCloud / support | support.fortinet.com, forticloud.com | TCP/443 | Licence activation, contract checks |
| DNS | 1.1.1.1 / your local resolver | UDP/53, TCP/53 | All of the above |
| NTP | pool.ntp.org | UDP/123 | Clock sync (TLS to CDN fails on drift) |
All HTTPS-out, no inbound. The FortiGuard “service” objects in FortiOS already encode the right ports — Rule 10 above covers the lot when you include FortiGuard-Web-Filter, FortiGuard-AntiSpam, HTTPS, DNS, NTP. If you want belt-and-braces visibility, swap the service group for a single ALL while you’re stabilising and switch to scoped services once you’ve watched the FortiView traffic for a session.
Step 7 — Anti-rules (the things that should not work)
Equally important. Build the rules that prove the boundary exists:
- Anything on
vmbr-fgdshould NOT reach FMG. The FortiGuard egress NIC of the FortiManager (port2in the FMG, onvmbr-fgd) is outbound-only by design. - Anything on
vmbr-custshould NOT reach FMG or transit. Customer LAN can reach the internet, nothing else. port1(WAN) should NOT have inbound rules. The lab is outbound-only. If you ever need to test inbound, put a VIP on the WAN with explicit destination (an internal-only test endpoint) — never expose the FMG GUI to the WAN side.
You can prove these by enabling Rule 99 (the deny-with-log) and watching Log & Report → Forward Traffic — every blocked attempt shows up. On a clean lab the only traffic in the deny log should be background broadcast / multicast that never leaves the box.
Step 8 — Local-in policy on the lab edge
The FGT itself is reachable on port2 for admin. Lock that down with a local-in policy so the management VLAN can talk to it but nothing else can:
config firewall local-in-policy
edit 1
set intf "port2"
set srcaddr "host-admin-ws" "host-fmg"
set dstaddr "all"
set action accept
set service "HTTPS" "SSH" "PING"
set schedule "always"
next
edit 99
set intf "any"
set srcaddr "all"
set dstaddr "all"
set action deny
set service "HTTPS" "SSH"
set schedule "always"
next
end
That stops port1 (WAN) from being a management plane even if a stray allowaccess slips through. On a lab, a single set allowaccess ping ssh https on port1 “for convenience” is the most common mistake.
Step 9 — Verification
Five things worth proving end-to-end before Part 5:
# 1. FGT-edge has internet
exec ping 1.1.1.1
exec ping update.fortiguard.net
# 2. FortiGuard contract registered
diagnose autoupdate versions
get system fortiguard
# 3. FMG can reach internet through FGT-edge
# (run from FMG console)
exec ping 1.1.1.1
exec ping update.fortiguard.net
diagnose test application updated 1
# 4. Admin workstation can reach FMG GUI but not the FGT-edge WAN
curl -k https://10.20.10.10 # FMG GUI
curl -k --max-time 3 https://<wan-ip> # should fail (local-in deny)
# 5. The deny log is empty of unexpected traffic
diagnose log test # generates a few sample log entries
exec log filter category 0
exec log display
The shape of the verification is the boundary itself: FMG can reach the internet, admins can reach FMG, and nothing else works. That’s exactly the property the lab edge exists to provide.
Step 10 — Snapshot
Before Part 5 attaches managed FGTs and starts changing things:
qm snapshot 9100 edge-baseline --description "FGT-edge built, policies stable, FMG reachable, no managed FGTs yet"
qm snapshot 9001 fmg-pre-managed --description "FMG behind FGT-edge, FortiGuard live, no managed devices yet"
What’s next
Part 5 is the payoff: build two managed FortiGate-VMs on vmbr-transit, register them to the FortiManager via FGFM through the lab edge’s Rule 30, create an ADOM, build a policy package, and push an install. From that point you have a fully working integration lab — break it, snapshot it, rebuild it, and learn the operational rhythm of central management without touching anything in production.
The single-line summary for Part 4: FortiGate-VM as the lab edge, four NICs mapped to the four bridges, policy set scoped to exactly what FMG needs and nothing more, local-in policy locking down the FGT itself, deny-with-log catching anything else.