Full physical and logical documentation of the homelab network — firewall, switching, server hypervisor and all running workloads. Every design decision is documented with reasoning.
10.0.X.0/24 as illustrative addressing rather than the real production scheme. The structure is what matters; the numbers are placeholders.| VM / Host | OS | VLAN | Role | Key Services |
|---|---|---|---|---|
| proxmox-01 | Proxmox VE 8.x | VLAN 20 | Hypervisor | KVM host, Cloud-Init, ZFS storage |
| srv-ubuntu-01 | Ubuntu 22.04 LTS | VLAN 10 | Services | Nextcloud, Vaultwarden, Gitea, n8n |
| srv-ubuntu-02 | Ubuntu 22.04 LTS | VLAN 10 | Media | Jellyfin, Plex, media storage |
| srv-ubuntu-03 | Ubuntu 22.04 LTS | VLAN 10 | Monitoring | Grafana, Prometheus, Uptime Kuma |
| srv-rhel-01 | RHEL 9 | VLAN 10 | Enterprise | Production workloads, SELinux enforcing |
| win-server-01 | Windows Server 2022 | VLAN 20 | Directory | Active Directory, DNS, DHCP, GPO |
| ai-workload-01 | Ubuntu 22.04 LTS | VLAN 10 | AI / ML | Ollama, Stable Diffusion, ComfyUI, MLflow, Jupyter |
| docker-host-01 | Ubuntu 22.04 LTS | VLAN 10 | Containers | Docker, Kubernetes, Portainer, Nginx Proxy Manager |
| cicd-01 | Ubuntu 22.04 LTS | VLAN 10 | CI/CD | Gitea, Drone CI, automated pipelines |
| mailcow-01 | Ubuntu 22.04 LTS | VLAN 10 | Mailcow — SMTP, IMAP, DKIM, SPF, DMARC |
Detailed configuration and explanation of every protocol in use — VLANs, LACP, STP, routing, DHCP and DNS. Each section includes working CLI config, reasoning and links to official standards.
| VLAN | Name | Subnet | Gateway | DHCP Range | Internet |
|---|---|---|---|---|---|
| 10 | Servers | 10.0.10.0/24 | 10.0.10.1 | .10 – .200 | Allowed |
| 20 | Management | 10.0.20.0/24 | 10.0.20.1 | .10 – .50 | Restricted |
| 30 | Users | 10.0.30.0/24 | 10.0.30.1 | .10 – .200 | Allowed |
| 40 | IoT | 10.0.40.0/24 | 10.0.40.1 | .10 – .150 | Allowed |
| 999 | Unused-Native | — | — | — | Blocked |
| 1 | default | — | — | — | Disabled |
! ── Create VLANs ────────────────────────────────────────── vlan 10 name Servers vlan 20 name Management vlan 30 name Users vlan 40 name IoT vlan 999 name Unused-Native ! ! ── Disable VLAN 1 — security hardening ────────────────── interface Vlan1 no ip address shutdown ! ! ── Trunk to FortiGate — allow only named VLANs ────────── interface GigabitEthernet0/1 description Uplink_to_FortiGate_LAG switchport mode trunk switchport trunk allowed vlan 10,20,30,40 switchport trunk native vlan 999 no shutdown ! ! ── Access port — server on VLAN 10 ───────────────────── interface GigabitEthernet0/10 description Proxmox_Node_1 switchport mode access switchport access vlan 10 spanning-tree portfast spanning-tree bpduguard enable no shutdown ! ! ── L3 SVI — inter-VLAN routing on Cisco ──────────────── interface Vlan10 ip address 10.0.10.2 255.255.255.0 no shutdown ! ! ── Verify ─────────────────────────────────────────────── show vlan brief show interfaces trunk
! ── Create the logical Port-Channel interface first ────── interface Port-channel1 description LAG_to_FortiGate switchport mode trunk switchport trunk allowed vlan 10,20,30,40 switchport trunk native vlan 999 ! ! ── Assign physical members — active mode initiates LACP ─ interface GigabitEthernet0/1 description LAG_link1_to_FortiGate channel-group 1 mode active no shutdown interface GigabitEthernet0/2 description LAG_link2_to_FortiGate channel-group 1 mode active no shutdown ! ! ── Verify ─────────────────────────────────────────────── show etherchannel summary show lacp neighbor show interfaces port-channel 1 status
| LACP Mode | Behaviour | PDUs sent? | Used where |
|---|---|---|---|
active | Initiates negotiation | Yes | Cisco switch — proactively forms LAG |
passive | Responds only | Only if peer sends first | FortiGate — responds to Cisco |
on | Static — no negotiation | No | Not used — no failover detection |
! ── Enable RSTP globally ────────────────────────────────── spanning-tree mode rapid-pvst ! ── Set Cisco as root for all VLANs (priority 4096) ────── ! Default priority is 32768 — lower wins the election spanning-tree vlan 10,20,30,40 priority 4096 ! ── Edge ports — skip 30s listening/learning ──────────── ! PortFast: use ONLY on ports connected to end devices interface range GigabitEthernet0/10 - 24 spanning-tree portfast ! BPDU Guard: shut port immediately if BPDU received ! Prevents rogue switch from joining topology spanning-tree bpduguard enable ! ── Enable BPDU Guard globally on all PortFast ports ───── spanning-tree portfast bpduguard default ! ── Auto-recover from err-disable after 5 minutes ─────── errdisable recovery cause bpduguard errdisable recovery interval 300 ! ── Verify ─────────────────────────────────────────────── show spanning-tree vlan 10 show spanning-tree summary
| Feature | Setting | Port type | Effect |
|---|---|---|---|
| RSTP mode | rapid-pvst | All | Sub-second convergence on topology change |
| Root priority | 4096 | Switch global | Cisco core always wins root election |
| PortFast | Enabled | Access ports only | Devices reach network immediately, no 30s wait |
| BPDU Guard | Enabled | All PortFast ports | Port shuts if another switch is plugged in |
| Loop Guard | Enabled | Non-designated uplinks | Prevents unidirectional failures creating loops |
Every unit in the rack plays a specific role in the protocol stack. Hover over any device to see what it does — in both plain and technical language.
| Concept | Value | Explanation |
|---|---|---|
| Protocol type | Link-state (LSA flooding) | Each router shares its link state with all others — every router has the same map of the network |
| Algorithm | Dijkstra SPF | Shortest Path First — calculates least-cost path to every destination |
| Area | Area 0 — backbone | Single area lab — all routers in same area, full LSA exchange |
| Adjacency type | Point-to-point | FortiGate ↔ Cisco SVI — no DR/BDR election needed |
| Hello interval | 10s (default) | Routers exchange Hello packets to discover and maintain neighbours |
| Dead interval | 40s | If no Hello received in 40s, neighbour declared dead and routes removed |
| Metric | Cost (based on bandwidth) | Higher bandwidth = lower cost = preferred path |
| Redistribution | Static → OSPF | Default route redistributed from FortiGate into OSPF so Cisco knows where to send internet traffic |
# ── Enable OSPF on FortiGate ────────────────────────────── config router ospf set router-id 10.0.20.1 # FortiGate loopback / mgmt IP set redistribute connected # share all connected networks set redistribute static # share default route 0.0.0.0/0 to internet config area edit 0.0.0.0 # Area 0 — backbone next end config ospf-interface edit "ospf-to-cisco" set interface "vlan20" # mgmt VLAN — connects to Cisco SVI set area 0.0.0.0 set network-type point-to-point set hello-interval 10 set dead-interval 40 next end config network edit 1 set prefix 10.0.0.0 255.255.0.0 # advertise all VLANs set area 0.0.0.0 next end end # ── Redistribute default route to Cisco ────────────────── config router ospf set default-information-originate always end
! ── Enable OSPF process on Cisco L3 switch ─────────────── router ospf 1 router-id 10.0.20.2 ! Cisco SVI management IP passive-interface default ! never send OSPF on access ports no passive-interface Vlan20 ! OSPF only on mgmt SVI toward FGT network 10.0.0.0 0.0.255.255 area 0 ! all SVIs in area 0 default-information originate ! accept default route from FortiGate ! ! ── OSPF on the uplink SVI to FortiGate ───────────────── interface Vlan20 ip ospf 1 area 0 ip ospf network point-to-point ip ospf hello-interval 10 ip ospf dead-interval 40 ! ! ── Verify ─────────────────────────────────────────────── show ip ospf neighbor show ip ospf database show ip route ospf
| Concept | Value | Explanation |
|---|---|---|
| Protocol type | Path-vector, exterior | Carries path of AS numbers — prevents loops, enables policy-based routing |
| Session type | eBGP (external) | Between FortiGate (homelab AS) and ISP router — different Autonomous Systems |
| Transport | TCP port 179 | BGP runs over TCP — reliable, ordered delivery |
| Homelab AS | 65001 (private range) | Private AS 64512–65535 for internal use; ISP has a real public AS number |
| Route selection | Best path algorithm | Prefers: highest LOCAL_PREF → lowest AS_PATH → lowest MED → eBGP over iBGP → lowest router-id |
| Dual WAN use | Primary/backup ISP | FortiGate sets LOCAL_PREF to prefer WAN1; if WAN1 fails, traffic shifts to WAN2 automatically |
| Keepalive | 60s (Hold timer: 180s) | BGP sends keepalives to maintain session; if no message in 180s, session reset |
# ── BGP configuration — FortiGate peering with ISP ─────── config router bgp set as 65001 # our private AS number set router-id 10.0.20.1 # FortiGate management IP config neighbor # ── ISP 1 peer (WAN1) ────────────────────────────── edit 203.0.113.1 # ISP 1 gateway IP set remote-as 64500 # ISP 1 public AS set interface "wan1" set update-source "wan1" # LOCAL_PREF 200 = strongly prefer WAN1 set route-map-in "SET_LOCALPREF_WAN1" set keepalive-timer 60 set holdtime-timer 180 next # ── ISP 2 peer (WAN2 — backup) ───────────────────── edit 198.51.100.1 # ISP 2 gateway IP set remote-as 64501 # ISP 2 public AS set interface "wan2" set update-source "wan2" # LOCAL_PREF 100 = use WAN2 only as backup set route-map-in "SET_LOCALPREF_WAN2" next end # ── Route maps to set LOCAL_PREF per ISP ────────────── config route-map edit "SET_LOCALPREF_WAN1" config rule edit 1 set set-local-preference 200 next end next edit "SET_LOCALPREF_WAN2" config rule edit 1 set set-local-preference 100 next end next end end # ── Verify ──────────────────────────────────────────────── get router info bgp summary get router info bgp neighbors get router info routing-table bgp
| VLAN | DHCP Server | Scope | Gateway | DNS |
|---|---|---|---|---|
| VLAN 10 | FortiGate | 10.0.10.10–.200 | 10.0.10.1 | 10.0.10.53 (Pi-hole) |
| VLAN 20 | Windows Server AD | 10.0.20.10–.50 | 10.0.20.1 | 10.0.20.10 (AD DNS) |
| VLAN 30 | FortiGate | 10.0.30.10–.200 | 10.0.30.1 | 10.0.10.53 (Pi-hole) |
| VLAN 40 | FortiGate | 10.0.40.10–.150 | 10.0.40.1 | 10.0.10.53 (Pi-hole) |
# ── DHCP relay on Aruba access switch ───────────────────── # When a device on VLAN 30 sends a DHCP broadcast, # Aruba forwards it as unicast to FortiGate 10.0.30.1 interface vlan30 ip address 10.0.30.2/24 ip helper-address 10.0.30.1 # FortiGate as DHCP server no shutdown interface vlan40 ip address 10.0.40.2/24 ip helper-address 10.0.40.1 no shutdown
| Feature | Detail | Explanation |
|---|---|---|
| Protocol | FGCP | FortiGate Clustering Protocol — proprietary L2 protocol on heartbeat interface |
| Mode | Active-Passive | FGT-A handles all traffic; FGT-B is on hot standby with full config sync |
| Heartbeat | Dedicated HA port | Direct crossover link between FGT-A and FGT-B — not through any switch |
| Session sync | Yes — stateful | Active sessions (TCP, VPN, NAT translations) synced to passive; no drops on failover |
| Failover trigger | Link failure, CPU overload, process crash | HA monitors interface status, CPU load and daemon health |
| Failover time | <1 second | Passive takes over virtual MAC and IP; downstream switches see no change |
| Config sync | Automatic, continuous | Any config change on active is instantly pushed to passive unit |
| Virtual MAC | Shared cluster MAC | Both units share a virtual MAC per interface so ARP cache on switches never changes |
# ── Configure HA on FGT-A (primary) ────────────────────── config system ha set mode a-p # active-passive set group-name "homelab-cluster" set group-id 1 set password "HA-secret-key" # must match on both units set priority 200 # higher = primary (FGT-A=200, FGT-B=100) set hbdev "port4" 50 # heartbeat on port4, priority 50 set session-sync-dev "port4" # session table sync on same link set override enable # re-elect primary after failure recovery set monitor "wan1" "port1" # monitor these interfaces for failover trigger end # ── Same config on FGT-B but priority 100 ──────────────── # set priority 100 # ── Verify cluster status ───────────────────────────────── get system ha status diagnose sys ha status diagnose sys ha checksum show # config sync verification
! ── DHCP Snooping — per VLAN ───────────────────────────── ip dhcp snooping ip dhcp snooping vlan 10,30,40 no ip dhcp snooping information option ! remove option 82 (causes issues) ! ! ── Trust only the uplink port to FortiGate ────────────── interface Port-channel1 ip dhcp snooping trust ! ! Access ports are untrusted by default — rogue DHCP blocked ! ! ── Dynamic ARP Inspection — per VLAN ─────────────────── ip arp inspection vlan 10,30,40 ip arp inspection validate src-mac dst-mac ip ! ! ── Trust the uplink for ARP too ───────────────────────── interface Port-channel1 ip arp inspection trust ! ! ── Verify ─────────────────────────────────────────────── show ip dhcp snooping binding show ip arp inspection vlan 10
All security policies, ACLs, NAT rules and port security configurations. Every rule has documented reasoning — not just what was configured, but why.
| ID | Policy Name | Src Interface | Dst Interface | Action | NAT | Log |
|---|---|---|---|---|---|---|
| 10 | Servers-to-Internet | vlan10 | wan1 | ACCEPT | PAT | all |
| 20 | Users-to-Internet | vlan30 | wan1 | ACCEPT | PAT | utm |
| 30 | IoT-Internet-only | vlan40 | wan1 | ACCEPT | PAT | utm |
| 35 | IoT-block-Servers | vlan40 | vlan10 | DENY | — | all |
| 36 | IoT-block-Mgmt | vlan40 | vlan20 | DENY | — | all |
| 40 | Users-to-Servers | vlan30 | vlan10 | ACCEPT | — | utm |
| 50 | Inbound-Cloudflare | wan1 | vlan10 | ACCEPT | VIP | all |
| 99 | Default-Deny-All | any | any | DENY | — | all |
# ── Address objects — reused across all policies ───────── config firewall address edit "VLAN10-Servers" set subnet 10.0.10.0 255.255.255.0 next edit "VLAN20-Management" set subnet 10.0.20.0 255.255.255.0 next edit "VLAN40-IoT" set subnet 10.0.40.0 255.255.255.0 next end # ── VIP — inbound DNAT for Cloudflare Tunnel ───────────── config firewall vip edit "VIP-Cloudflare" set extintf "wan1" set extip 0.0.0.0 set mappedip "10.0.10.20-10.0.10.20" set portforward enable set protocol tcp set extport 443 set mappedport 443 next end # ── Deny IoT from reaching servers — logged ─────────────── config firewall policy edit 35 set name "IoT-block-Servers" set srcintf "vlan40" set dstintf "vlan10" set srcaddr "VLAN40-IoT" set dstaddr "VLAN10-Servers" set action deny set schedule "always" set service "ALL" set logtraffic all next end
| Type | Direction | Mechanism | Applied to |
|---|---|---|---|
| PAT / Overload | Outbound | Many:1 — all hosts share WAN IP, differentiated by port | All VLANs to internet |
| VIP (DNAT) | Inbound | WAN IP:port mapped to internal host:port | Cloudflare Tunnel connector only |
| Cloudflare Tunnel | Outbound (public) | Outbound persistent tunnel — Cloudflare proxies inbound traffic | All public-facing services |
! ── Template — apply to all access ports ──────────────── interface GigabitEthernet0/10 description Server_Access_VLAN10 switchport mode access switchport access vlan 10 ! Port security — max 3 MACs, restrict + log violations switchport port-security maximum 3 switchport port-security violation restrict switchport port-security ! PortFast — immediate forwarding (no STP delay) spanning-tree portfast ! BPDU Guard — shut if another switch connects spanning-tree bpduguard enable ! Storm control — max 10% broadcast, 10% multicast storm-control broadcast level 10.00 storm-control multicast level 10.00 storm-control action shutdown no shutdown ! ! Violation modes: ! protect = drop + no log, port stays up ! restrict = drop + SNMP trap log, port stays up ← used ! shutdown = err-disable immediately (strictest)
Every service running on the homelab infrastructure. For each: what it does, where it runs, official documentation and configuration notes.
Open-source KVM/QEMU hypervisor with a web UI. Manages all VMs, networking bridges and ZFS storage. Cloud-Init integration enables zero-touch VM provisioning.
Docker runs containerised services. Kubernetes handles orchestration for multi-container workloads. Portainer provides a visual management UI for both.
Manages all reverse proxy rules and HTTPS termination. Routes external hostnames to the correct internal service. Integrated Let's Encrypt for automatic certificate renewal.
Lightweight Bitwarden-compatible server. Full Bitwarden clients (browser extensions, mobile apps) connect to this private instance. TOTP, passkeys and secure sharing supported.
Self-hosted GitHub alternative. Repositories, issues, pull requests, webhooks and CI/CD trigger integration with Drone CI. Full Git over SSH and HTTPS.
Full email stack — Postfix (SMTP), Dovecot (IMAP), Rspamd (spam filtering), ClamAV (antivirus), SOGo webmail. DKIM, SPF and DMARC configured for mail delivery.
Visualises metrics from Prometheus. Custom dashboards for CPU, RAM, network I/O, disk usage and service health across all VMs. Alerting via email and webhook.
Scrapes metrics from Node Exporter on every VM, cAdvisor on container hosts and application exporters. Time-series database. PromQL for custom queries.
Visual no-code/low-code workflow automation. Connects services, triggers on events, runs scheduled tasks. Integrates with Gitea webhooks, Grafana alerts and external APIs.
Triggered by Gitea webhooks on every push. Runs pipelines: build, test, lint, deploy. Docker-native — each pipeline step runs in an isolated container.

Runs large language models locally — Llama 3, Mistral, Phi, CodeLlama and others. OpenAI-compatible API endpoint. Models run on GPU via PCIe passthrough. Zero data to cloud.
Local Stable Diffusion inference with ComfyUI as the node-based workflow editor. Runs SDXL, SD 1.5 and LoRA fine-tuned models. GPU-accelerated via CUDA passthrough.
Tracks ML experiments — parameters, metrics, artifacts and model versions. Integrates with PyTorch and TensorFlow training loops. Provides a UI to compare runs and promote models.
Interactive notebooks for Python data science, ML experimentation and documentation. Runs on the AI VM with direct access to PyTorch/TensorFlow and GPU compute.
Open-source media server. Streams video, music and photos to any device. Hardware transcoding via GPU for efficient streaming without high CPU load.
Alternative media server to Jellyfin with broader client support. Runs alongside Jellyfin on the same VM, sharing the same media library mount.
Complete documentation of the FortiGate DDNS configuration and IPsec site-to-site VPN setup. Because the WAN IP is dynamic (ISP does not provide a static IP), a DDNS hostname is configured on the FortiGate so remote peers can always reach the firewall by name, not by IP.
| Problem | Solution | How it works |
|---|---|---|
| WAN IP changes dynamically | DDNS hostname | FortiGate polls or pushes updated IP to a DDNS provider whenever WAN IP changes |
| VPN peer needs stable address | Hostname in VPN config | Remote peer uses homelab.fortiddns.com — resolves to current WAN IP automatically |
| Self-signed cert mismatch | FortiGate built-in DDNS | FortiGate DDNS integrates with FortiGuard — no external account needed, managed in CLI/GUI |
*.fortiddns.com subdomain tied to your FortiGate's serial number. No external DDNS provider account is needed. The FortiGate updates the record automatically whenever the WAN IP changes. You can also configure third-party DDNS providers (No-IP, Dynu, DynDNS) via the same interface.# ── Enable FortiGate built-in DDNS (FortiDDNS) ─────────── # This registers a hostname: <serial>.fortiddns.com config system ddns edit 1 set ddns-server FortiGuardDDNS set use-public-ip enable set update-interval 300 # update every 5 minutes set monitor-interface "wan1" # watch WAN interface for IP change set ddns-domain "homelab.fortiddns.com" next end # ── Alternative: third-party DDNS provider (e.g. No-IP) ── config system ddns edit 2 set ddns-server No-IP set ddns-username "your-noip-username" set ddns-password "your-noip-password" set ddns-domain "homelab.ddns.net" set monitor-interface "wan1" set update-interval 300 next end # ── Verify DDNS is updating correctly ───────────────────── diagnose debug application ddnsd -1 diagnose debug enable # Then check: current IP in DDNS record should match WAN IP get system ddns
| Step | Protocol | What happens |
|---|---|---|
| 1 — DNS Resolution | DNS | Remote peer resolves homelab.fortiddns.com → current WAN IP of homelab FortiGate |
| 2 — IKE Phase 1 | IKEv2 UDP 500/4500 | Both sides authenticate using pre-shared key (PSK) and negotiate encryption parameters (AES-256, SHA-256) |
| 3 — IKE Phase 2 | IKEv2 | Child SA negotiated — defines which traffic is protected (interesting traffic selectors) |
| 4 — Tunnel Up | IPsec ESP | Encrypted tunnel established. Traffic between defined subnets routes through tunnel automatically |
| 5 — IP Change | DDNS + IKE DPD | If WAN IP changes: DDNS updates, remote peer re-resolves hostname, IKE Dead Peer Detection triggers re-negotiation |
# ── Phase 1 — IKE negotiation ───────────────────────────── # This defines HOW the tunnel is negotiated config vpn ipsec phase1-interface edit "SiteB-VPN" set interface "wan1" set ike-version 2 # Remote peer — use DDNS hostname, not static IP set remote-gw-ddns "siteB.fortiddns.com" # Authentication with pre-shared key set authmethod psk set psksecret "YourStrongPreSharedKey!2024" # Encryption — AES-256-GCM is preferred for IKEv2 set proposal aes256gcm-prfsha384 set dhgrp 20 # ECDH P-384 # Dead Peer Detection — detect if tunnel drops set dpd on-demand set dpd-retrycount 3 set dpd-retryinterval 10 # Local gateway ID = DDNS hostname of THIS FortiGate set localid "homelab.fortiddns.com" set localid-type fqdn set mode-cfg disable set net-device enable next end
# ── Phase 2 — what traffic goes through the tunnel ──────── config vpn ipsec phase2-interface edit "SiteB-VPN-P2" set phase1name "SiteB-VPN" # Traffic selectors — subnets on each side set src-subnet 10.0.10.0 255.255.255.0 # local LAN (homelab) set dst-subnet 192.168.1.0 255.255.255.0 # remote LAN (site B) # ESP encryption for data set proposal aes256gcm set dhgrp 20 # Rekey every 8 hours set keylifeseconds 28800 set auto-negotiate enable next end # ── Static route — send remote subnet through tunnel ────── config router static edit 10 set dst 192.168.1.0 255.255.255.0 set device "SiteB-VPN" set comment "Route to Site B through IPsec tunnel" next end # ── Firewall policy — allow traffic through tunnel ───────── config firewall policy edit 60 set name "LAN-to-SiteB" set srcintf "vlan10" set dstintf "SiteB-VPN" set srcaddr "VLAN10-Servers" set dstaddr "SiteB-LAN" set action accept set schedule "always" set service "ALL" set logtraffic all next edit 61 set name "SiteB-to-LAN" set srcintf "SiteB-VPN" set dstintf "vlan10" set srcaddr "SiteB-LAN" set dstaddr "VLAN10-Servers" set action accept set schedule "always" set service "ALL" set logtraffic all next end
# ── Check tunnel status ─────────────────────────────────── get vpn ipsec tunnel summary get vpn ipsec tunnel details # ── Check IKE SA (Phase 1) ──────────────────────────────── diagnose vpn ike status diagnose vpn ike gateway list # ── Check IPsec SA (Phase 2) ───────────────────────────── diagnose vpn tunnel list # ── Real-time debug (Phase 1 negotiation) ──────────────── diagnose debug application ike -1 diagnose debug enable # Run the above, then bring tunnel up, check output diagnose debug disable # ── Ping through tunnel to verify routing ──────────────── execute ping-options source 10.0.10.1 # source from inside VLAN execute ping 192.168.1.1 # ping remote site B gateway # ── Force tunnel re-negotiation ────────────────────────── diagnose vpn ike restart
# ── Create WireGuard interface ──────────────────────────── config system wireguard edit "wg0" set listen-port 51820 set private-key "<FortiGate-private-key-base64>" # Assign IP from a dedicated WireGuard subnet config peers edit 1 set name "laptop-client" set public-key "<client-public-key-base64>" set allowed-ips 10.0.50.2 255.255.255.255 next end next end # ── Client config (wg0.conf on laptop) ─────────────────── # [Interface] # PrivateKey = <client-private-key> # Address = 10.0.50.2/24 # DNS = 10.0.40.1 # Pi-hole # # [Peer] # PublicKey = <FortiGate-public-key> # Endpoint = homelab.fortiddns.com:51820 # DDNS hostname # AllowedIPs = 10.0.0.0/16 # route all homelab traffic # PersistentKeepalive = 25
| Parameter | Value used | Why |
|---|---|---|
| IKE Version | IKEv2 | More efficient than IKEv1, better handling of NAT traversal and DDNS peers |
| Phase 1 Encryption | AES-256-GCM | AEAD cipher — encryption and authentication in one pass, no separate HMAC needed |
| DH Group | Group 20 (P-384) | Elliptic curve key exchange — strong security with shorter keys than classic DH |
| Authentication | PSK (Pre-Shared Key) | Simpler than PKI certificates for a homelab; use a strong random 32+ char key |
| Local ID type | FQDN | Identifies this FortiGate by its DDNS hostname, not IP — needed for dynamic WAN |
| DPD (Dead Peer Detection) | on-demand | Sends keepalive only when there is traffic — detects dropped tunnels without flooding |
| Phase 2 Lifetime | 28800s (8h) | Regular rekey prevents long-term key compromise; matches common enterprise policy |
| NAT Traversal | Enabled (IKEv2 default) | Encapsulates ESP in UDP 4500 so it passes through NAT/PAT on either side |
An honest technical review of architectural decisions made throughout the homelab build — what works, what the alternatives were, and what would be the better choice in a production enterprise environment.
Static routes work fine for a homelab, but OSPF (Open Shortest Path First) would dynamically learn and propagate routes between FortiGate and Cisco. If a link fails, OSPF reconverges automatically with zero manual intervention. In enterprise environments, OSPF or EIGRP is standard for any multi-router topology.
Cisco OSPF Configuration GuidePort security with MAC limiting is a good start, but 802.1X NAC would authenticate every device with a certificate or credentials before allowing network access. A RADIUS server (FreeRADIUS or Windows NPS) rejects unconfigured devices entirely — even if they're plugged into a port.
Cisco 802.1X GuideA single FortiGate is a single point of failure. In production, two FortiGate units run in HA mode — one active, one passive. If the primary fails, failover takes under 1 second with state synchronised (sessions preserved). Requires a second unit and a dedicated HA heartbeat link.
FortiGate HA DocumentationManagement traffic currently shares VLAN 20 with some infrastructure services. A fully separate out-of-band (OOB) management network — physically separate NICs, its own switch ports, no routing to production VLANs — is the enterprise standard. It ensures network devices are reachable even if the production network is down.
Cisco OOB ManagementVMs are currently provisioned with Cloud-Init and Ansible. Terraform with the Proxmox provider would declaratively define every VM — CPU, RAM, disk, network — in version-controlled HCL files. Running terraform apply creates, modifies or destroys resources. terraform plan shows changes before applying. State is tracked automatically.
Current storage is ZFS on a single Proxmox node — no redundancy. Ceph (natively supported in Proxmox) distributes data across multiple nodes with configurable replication. A 3-node Proxmox cluster with Ceph would survive a full node failure with zero data loss and zero VM downtime via live migration.
Proxmox Ceph GuideCurrent CI/CD pushes changes via Drone CI. ArgoCD implements GitOps — Kubernetes cluster state is defined in Git, and ArgoCD continuously reconciles the live cluster to match. Any drift (manual changes, failures) is automatically corrected. This is the industry standard for production Kubernetes deployments.
ArgoCD DocumentationAnsible vault and environment variables are used for secrets currently. HashiCorp Vault would centralise all secrets — API keys, certificates, SSH keys and database passwords — with fine-grained access control, audit logging, automatic rotation and dynamic credentials. Services would request secrets at runtime rather than having them stored in config files.
HashiCorp Vault DocsPrometheus + Grafana covers metrics well. Full observability adds distributed tracing and structured logging via OpenTelemetry — the vendor-neutral standard. Traces show exactly which service caused a slowdown. Logs, metrics and traces correlated in one view (e.g. via Grafana + Loki + Tempo stack).
OpenTelemetry DocsVLAN-based segmentation is perimeter-based — traffic within a VLAN is unrestricted. Micro-segmentation (e.g. via VMware NSX-T or Cilium in Kubernetes) applies policy at the individual workload level. Every container or VM can only communicate with explicitly permitted peers, regardless of VLAN.
Cilium Network Policy