VM: OPNSense Firewall
VM Information
Type: Virtual Machine (KVM/QEMU)
OS: OPNSense 25.x (FreeBSD 14.x)
Hostname: opnsense (customizable)
Status: running
Resources:
vCPUs: 4
RAM: 4 GB
Disk: 25 GB (dynamically expandable)Installed Software
OPNSense Firewall
Version: 25.x (current Stable)
Base: FreeBSD 14.x
Firewall Engine: pf (Packet Filter)
Web Interface: Lighttpd + PHPCaddy Reverse Proxy
Version: 2.x (current Stable)
Installation: FreeBSD Package
Service: caddy.service (systemd)
TLS: Let's Encrypt (automatic)DNS Resolver
Service: Unbound
Upstream: Cloudflare, Quad9
DNSSEC: Enabled
Query Logging: OptionalNetwork Configuration
Network Interfaces
WAN Interface:
- Type: Virtual (vmbr0 Bridge)
- IP: DHCP or static (ISP-dependent)
- Firewall: Default DENY (Whitelist)
LAN Interface:
- Type: Virtual (vmbr1 Bridge)
- IP: RFC1918 Private Network
- Gateway: For internal services
- DHCP: Enabled for internal clientsBridge Configuration (Proxmox)
vmbr0 (WAN):
- Connection to Internet Uplink
- Only OPNSense WAN Interface
vmbr1 (LAN):
- Private network for p2d2 services
- All LXC containers and internal VMsFirewall Rules
Firewall Philosophy
Default Policy: DROP (all packets)
Approach: Whitelist (only explicitly allowed connections)
Stateful: Yes (track established connections)
Logging: Log important eventsRule Categories
WAN → LAN (Inbound)
1. HTTPS (443) → Caddy Reverse Proxy: ALLOW
2. HTTP (80) → Caddy (Redirect to HTTPS): ALLOW
3. WireGuard (<VPN_PORT>) → VPN Server: ALLOW
4. All other ports: DENYLAN → Internet (Outbound)
1. HTTP/HTTPS (80/443): ALLOW (for APT updates, Let's Encrypt)
2. DNS (53): ALLOW (Upstream resolvers)
3. NTP (123): ALLOW (Time synchronization)
4. SMTP (25/587): ALLOW (only for Ory Email dispatch)
5. Others: DENY (only explicitly needed ports)LAN → LAN (Service-to-Service)
See Service Matrix in Network Documentation.
Caddy Reverse Proxy
Main Configuration (Caddyfile)
# /usr/local/etc/caddy/Caddyfile
# Global Settings
{
email admin@data-dna.eu
acme_ca https://acme-v02.api.letsencrypt.org/directory
}
# Auto-HTTPS for all domains
*.data-dna.eu {
tls {
dns cloudflare <CF_API_TOKEN>
}
@main host www.data-dna.eu
handle @main {
reverse_proxy http://frontend.lan:3000
}
@dev host dev.data-dna.eu
handle @dev {
reverse_proxy http://frontend.lan:3001
}
@feature1 host f-de1.data-dna.eu
handle @feature1 {
reverse_proxy http://frontend.lan:3002
}
@feature2 host f-de2.data-dna.eu
handle @feature2 {
reverse_proxy http://frontend.lan:3003
}
@feature3 host f-fv.data-dna.eu
handle @feature3 {
reverse_proxy http://frontend.lan:3004
}
@doc host doc.data-dna.eu
handle @doc {
reverse_proxy http://frontend.lan:4173
}
@ows host ows.data-dna.eu
handle @ows {
reverse_proxy http://geoserver.lan:8080
}
@wfs host wfs.data-dna.eu
handle @wfs {
reverse_proxy http://geoserver.lan:8080
}
@tiles host tiles.data-dna.eu
handle @tiles {
reverse_proxy http://mapproxy.lan:8080
}
@proxy host proxy.data-dna.eu
handle @proxy {
reverse_proxy http://mapproxy.lan:8080
}
}
# HTTP to HTTPS Redirect
http://*data-dna.eu {
redir https://{host}{uri} permanent
}Custom Configurations
# /usr/local/etc/caddy/caddy.d/geo-services.conf
# GeoServer WMS Optimizations
ows.data-dna.eu {
reverse_proxy http://geoserver.lan:8080 {
header_up X-Real-IP {remote_host}
header_up X-Forwarded-Proto {scheme}
}
# Rate Limiting for WMS
@wms {
path /geoserver/wms*
}
handle @wms {
rate_limit {
zone wms_zone 10r/s
key {remote_host}
}
reverse_proxy http://geoserver.lan:8080
}
}
# MapProxy Tile Service
tiles.data-dna.eu {
reverse_proxy http://mapproxy.lan:8080
# Caching for Tiles
header Cache-Control "public, max-age=86400"
# CORS for Web-Apps
header Access-Control-Allow-Origin *
}DNS Configuration (Unbound)
Local DNS Records
# /var/unbound/host_entries.conf
local-data: "postgresql.lan A <DB_CONTAINER_IP>"
local-data: "geoserver.lan A <GEOSERVER_CONTAINER_IP>"
local-data: "mapproxy.lan A <MAPPROXY_CONTAINER_IP>"
local-data: "frontend.lan A <FRONTEND_CONTAINER_IP>"
local-data: "osm-tiler.lan A <TILER_VM_IP>"
local-data-ptr: "<DB_CONTAINER_IP> postgresql.lan"Upstream Resolver
# /var/unbound/forwarding.conf
forward-zone:
name: "."
forward-addr: 1.1.1.1@853#cloudflare-dns.com
forward-addr: 9.9.9.9@853#dns.quad9.net
forward-tls-upstream: yesVPN Access (WireGuard)
Server Configuration
# /usr/local/etc/wireguard/wg0.conf
[Interface]
Address = <VPN_INTERNAL_IP>/24
PrivateKey = <SERVER_PRIVATE_KEY>
ListenPort = <VPN_PORT>
[Peer]
# Admin Client 1
PublicKey = <CLIENT_1_PUBLIC_KEY>
AllowedIPs = <CLIENT_1_VPN_IP>/32
PersistentKeepalive = 25
[Peer]
# Admin Client 2
PublicKey = <CLIENT_2_PUBLIC_KEY>
AllowedIPs = <CLIENT_2_VPN_IP>/32Firewall Rules for VPN
VPN → LAN: ALLOW (for Admin access)
VPN → WAN: DENY (only via NAT)
VPN Clients can access internal servicesBackup Strategy
PBS Snapshot (VM-Level)
- Schedule: Daily
- Retention: 7 days
- Type: QEMU Snapshot
Configuration Backup
# OPNSense Config Export
System → Configuration → Backups → Download
# Caddy Configuration
tar -czf /backup/caddy-config_$(date +%Y%m%d).tar.gz
/usr/local/etc/caddy/
# Automation via Cron
# /etc/cron.daily/opnsense-backup
#!/bin/sh
/usr/local/etc/rc.backup_running
tar -czf /backup/caddy-config_$(date +%Y%m%d).tar.gz
/usr/local/etc/caddy/Monitoring
Health Checks
# Service Status
service -e | grep -E "(caddy|unbound|wireguard)"
# Caddy Status
curl -I https://www.data-dna.eu
# DNS Resolution
nslookup www.data-dna.eu 127.0.0.1
# Firewall Stats
pfctl -si | grep -E "(states|searches)"Log Analysis
# Firewall Logs
tail -f /var/log/filter.log
# Caddy Logs
tail -f /var/log/caddy/access.log
tail -f /var/log/caddy/error.log
# System Logs
tail -f /var/log/system.logTroubleshooting
Network Problems
# Interface Status
ifconfig -a
# Routing Table
netstat -rn
# Firewall Rules
pfctl -sr
# Packet Capture
tcpdump -i <INTERFACE> -nCaddy Problems
# Service Status
service caddy status
# Configuration Validation
caddy validate --config /usr/local/etc/caddy/Caddyfile
# Log Analysis
journalctl -u caddy --no-pager -n 100DNS Problems
# Unbound Status
service unbound status
# Test DNS Resolution
dig @127.0.0.1 www.data-dna.eu
# Query Logs
tail -f /var/log/unbound.logSecurity Configuration
OPNSense Hardening
Web Interface:
- HTTPS-only
- Strong Admin Password
- 2FA optionally activatable
SSH Access:
- Only via VPN or Management VLAN
- Key-based Authentication
- Password Authentication disabled
Firewall:
- Default DENY Policy
- Stateful Packet Inspection
- Geo-Blocking optionalCaddy Security
TLS Configuration:
- TLS 1.2 minimum, TLS 1.3 preferred
- Modern Cipher Suites
- HSTS Header
- OCSP Stapling
Rate-Limiting:
- For all public endpoints
- IP-based limits
- Burst ProtectionBest Practices
✅ Do:
- Regular OPNSense updates (Security Patches)
- Review firewall rules regularly
- Use VPN for all admin access
- Monitor system logs
- Backup the configuration
❌ Don't:
- Use default passwords
- Open unnecessary ports
- Run without rate limiting
- Ignore firewall logs
- Allow direct SSH access from the internet