Firewall
The Firewall module is FreeSDN’s unified network-security surface. It handles firewall rule CRUD and reorder, NAT, VPN tunnels with live stats, and IDS/IPS for supported adapters. It also absorbs the former Gateway module, giving you gateway orchestration: designate one firewall as the site brain, assign network controllers as limbs, define canonical VLANs once, and distribute them everywhere.
You interact with the Firewall module through two separate worlds that are distinct by design:
- FreeSDN’s own normalized tables - rules, NAT rules, VPN tunnels, and IDS alerts stored in the
firewallschema. These are portable, org-scoped, appear in config backups, and are managed through the Pending Changes UI. - Live device reads and writes - proxied through vendor adapters via
/api/v1/firewall/gateways/{id}/.... These talk directly to the connected OPNsense, pfSense, MikroTik, or OpenWRT device.
Adapters
Section titled “Adapters”| Adapter | Available integration | Release scope |
|---|---|---|
| OPNsense | Broad firewall, NAT, VPN and routing integration | Coverage varies by endpoint and device/API version; the prepared certificate inventory checks are described below |
| pfSense | Firewall, NAT and VPN integration | Verify the installed API package and operation compatibility; shared client code does not prove OPNsense endpoint compatibility |
| MikroTik (RouterOS v7) | REST API integration | No SSH or WinBox transport; qualify the required operations against the deployed RouterOS version |
| OpenWRT | Preview ubus/UCI integration and configuration export | Package availability and device acceptance remain operation-specific; do not treat the presence of a UI tab as a production-fleet qualification |
Adapter-facing operations use the applicable tenant/site permission checks, destination restrictions and write controls. Their exact behavior is operation-specific: inspect the result and Pending Changes state, and do not treat a generic HTTP success or an empty list as proof that a device operation completed.
Connecting a firewall
Section titled “Connecting a firewall”Gateway connections live at Firewall > Gateways in the UI, or via the API at /api/v1/firewall/gateways. Credentials are Fernet-encrypted at rest and never returned in plaintext; the detail endpoint returns a has_credentials: bool flag rather than the stored secret.
OPNsense
Section titled “OPNsense”OPNsense uses API key + API secret. Generate a key in OPNsense at System > Access > Users, create a user with restricted privileges, then add the key here.
# 1. Create the gateway connectioncurl -s -X POST https://<freesdn-host>/api/v1/firewall/gateways \ -H "Cookie: freesdn_access=<token>" \ -H "X-CSRF-Token: <csrf>" \ -H "Content-Type: application/json" \ -d '{ "name": "opn-prod", "vendor": "opnsense", "host": "opn.lan", "port": 443, "verify_ssl": true, "site_id": "<site-uuid>", "api_key": "<opnsense-key>", "api_secret": "<opnsense-secret>" }'
# 2. Test the saved connectioncurl -s -X POST https://<freesdn-host>/api/v1/firewall/gateways/<gateway-id>/test \ -H "Cookie: freesdn_access=<token>" \ -H "X-CSRF-Token: <csrf>"
# 3. Trigger initial synccurl -s -X POST https://<freesdn-host>/api/v1/firewall/gateways/<gateway-id>/sync \ -H "Cookie: freesdn_access=<token>" \ -H "X-CSRF-Token: <csrf>" \ -H "Content-Type: application/json" \ -d '{"full_sync": true}'pfSense
Section titled “pfSense”pfSense uses the same API key/secret credential shape as OPNsense with "vendor": "pfsense".
MikroTik
Section titled “MikroTik”MikroTik uses username/password with "vendor": "mikrotik". The adapter communicates over the MikroTik REST API (RouterOS v7+ required - no SSH, no WinBox API).
curl -s -X POST https://<freesdn-host>/api/v1/firewall/gateways \ -H "Cookie: freesdn_access=<token>" \ -H "X-CSRF-Token: <csrf>" \ -H "Content-Type: application/json" \ -d '{ "name": "mt-core", "vendor": "mikrotik", "host": "mt.lan", "port": 443, "verify_ssl": true, "site_id": "<site-uuid>", "username": "freesdn-api", "password": "<password>" }'Test before saving
Section titled “Test before saving”You can test connectivity without persisting a connection:
# Requires: firewall.manage_rulesPOST /api/v1/firewall/gateways/test{ "vendor": "opnsense", "host": "opn.lan", "api_key": "...", "api_secret": "..." }The saved-connection test endpoint (POST /gateways/{id}/test) accepts an optional verify_ssl override only. It uses the saved host and port so stored credentials cannot be redirected to a different destination. Test a new host with explicitly supplied credentials through the unsaved-connection endpoint; do not assume an edited host will be used by the saved test.
Certificate inventory and expiry monitoring
Section titled “Certificate inventory and expiry monitoring”The gateway System tab reports certificate expiry from the selected gateway’s live inventory. The GET /api/v1/firewall/gateways/{id}/certificates/certs and /certificates/expiry routes require firewall.view and the applicable organization/site scope. The expiry route accepts days_threshold from 1 through 365; the UI requests 30 days.
| State | Meaning | Operator action |
|---|---|---|
| Loading or refresh in progress | No new inventory has been confirmed | Wait for completion; an earlier healthy result is hidden during refresh |
| Confirmed empty inventory | A supported read succeeded with zero certificates | Confirm that this is expected for the device; it is not an inventory failure |
Unsupported (501) |
The adapter does not provide this read | Use the device’s own certificate inventory and record the coverage gap |
Unavailable (502) |
The read failed, was malformed, or did not return a complete consistent inventory | Check connectivity, API permissions and device/API compatibility, then use Retry |
| Unknown expiry | At least one returned certificate has a missing or invalid expiry date | Inspect it on the device; unknown dates require attention and are not counted as valid |
| Expired or expiring soon | A parsed expiry is past or within the configured threshold | Follow the device’s approved renewal procedure |
| Valid inventory | A nonempty successful inventory contains no expired, expiring or unknown entries | Continue monitoring; this is expiry status, not trust-chain or service-usage validation |
A refresh failure replaces the previous healthy display with an unavailable state. Retry performs another read; it does not issue, renew, delete or install certificates. Missing dates display as unknown rather than an epoch date. OPNsense inventory checks pagination totals and certificate identities and includes only public inventory fields. Private keys and raw provider error bodies are excluded from this path.
Trust-store, CA/CRL and ACME inventory reads have a separate prepared correction described below. Expiry monitoring does not establish certificate renewal, trust-chain validation or coverage of every vendor’s API.
This monitoring feature is separate from TLS verification of the connection to the gateway. Do not disable connection verification to make an expiry indicator green. It also does not provide managed configuration-backup retention or device restore; see backups and recovery.
Trust, CA, CRL and ACME inventory
Section titled “Trust, CA, CRL and ACME inventory”The trust-store overview (GET /api/v1/firewall/gateways/{id}/certificates), CA inventory (/certificates/cas), ACME overview (/acme) and ACME certificate inventory (/acme/certificates) require firewall.view and the applicable organization/site scope. A confirmed empty result means the supported read completed and returned no entries. It must not be used as a substitute for a failed or unsupported read.
| Result | Operator meaning |
|---|---|
| Loading or refreshing | The current read has not completed; cached counts are hidden |
| Confirmed empty | All required component reads succeeded and their validated inventories are empty |
Unsupported (501) |
This adapter does not provide the requested operation |
Unavailable (502) |
A component failed, returned malformed data, or returned incomplete or inconsistent inventory |
| Populated inventory | The validated read returned entries; counts alone do not prove active services or healthy certificates |
If any required trust or ACME component fails, the overview is unavailable. It does not combine failed components into a successful empty summary. A failed refresh hides previous counts and offers explicit retry. Retry performs reads only. Raw provider errors and private-key material are not displayed.
For OPNsense, the prepared adapter uses the documented ACME search endpoints and nested acmeclient.settings response. Both environment and log-level selections must be unambiguous. CRL search identifies CA rows separately from configured revocation lists: a CA without a CRL does not increase the CRL count. Certificate alternate names and last-update values come from the normalized inventory. Search output does not establish private-key presence; missing evidence remains unknown.
The upstream GET search recordset has a default limit of 9,999 rows. Returned totals and page metadata must match the retrieved records; a truncated response is unavailable, not a complete inventory. This implementation does not promise unbounded pagination. For a larger inventory, use the device’s supported management interface until full pagination is implemented and qualified.
Enabled in configuration is not issuance or renewal success. An ACME entry can be configured as enabled while validation, issuance, deployment or renewal has failed. Check the device’s own ACME job history and certificate state. The view does not perform public ACME transactions, validate certificate chains or revocation, or authorize device writes.
These corrections do not qualify every gateway aggregate, managed backup retention/restore, or hardware firmware/API version. Use controlled device-specific acceptance records before relying on them for production operations. Keep gateway connection TLS verification enabled independently of these inventory states.
Aggregate live status and monitoring
Section titled “Aggregate live status and monitoring”The selected VPN, Tailscale, HAProxy, proxy, CrowdSec, Monit, Telegraf and NetFlow panels now distinguish loading, confirmed results and unavailable reads. The overview also reports device-summary failures explicitly. A pending refresh hides prior values; a failed refresh keeps them hidden and provides a read-only retry. An unavailable monitoring read must not be interpreted as a stopped service or zero incidents.
The twelve selected live-read routes return 501 for an unsupported adapter read and 502 for failed required reads or malformed normalized results. Existing organization, site and permission checks still apply. Responses remain bound to the admitted gateway identity. OpenWrt retains its separate device-summary API contract rather than being forced into OPNsense’s interface fields.
For OPNsense aggregates, every required component must complete successfully. Cancellation is preserved, and provider error bodies are not shown. This does not establish complete validation of successful upstream responses: some underlying parsers still interpret malformed HTTP 200 bodies as defaults. Confirm the actual firmware and installed plugin responses before relying on these statuses operationally.
Monit, Telegraf, NetFlow and CrowdSec display the normalized API fields. Configuration enablement and a reported service state do not prove working traffic forwarding, VPN connectivity, incident collection or enforcement. CrowdSec’s bounded result window is not a complete incident history. Other independent overview, firmware and monitoring queries have separate qualification limits.
VPN raw-status display additionally masks known secret fields when their values are strings, arrays or objects, and masks uninspected containers beyond its nesting limit. The API also applies its shared redactor before delivery. This key-based protection does not promise to recognize arbitrary secrets under unknown field names.
These checks use synthetic device I/O, isolated PostgreSQL/API requests and the actual dashboard. They do not restart services, write gateway configuration, install plugins or perform backup/restore. Use an isolated target to rehearse failure and recovery on the intended image and firmware. Review the candidate’s docker/GATEWAY-AGGREGATE-READS and release support boundary for scope.
Write model and staging
Section titled “Write model and staging”Every adapter-facing write goes through the staged dual-gate:
ADAPTER_READ_ONLYmust befalse. The shipped Docker Compose stack runs read-WRITE (ADAPTER_READ_ONLY=false) so FreeSDN manages your gear out of the box. SetADAPTER_READ_ONLY=truein.envfor a monitor-only deployment.- Each mutation must include
force=true. This gate is unchanged: a staged change reaches a device only when an operator explicitly applies it.
Until both gates pass, writes are staged to the local database and the live device is not touched. Review pending changes in the UI before applying.
SSRF protection is enforced on the gateway host field and every diagnostics target: loopback, link-local, cloud-metadata ranges (127.0.0.0/8, 169.254.0.0/16, ::1, fe80::/10, 0.0.0.0/8) and names (localhost, metadata.google.internal) are rejected at schema validation time, before any adapter call.
Firewall rules (FreeSDN tables)
Section titled “Firewall rules (FreeSDN tables)”These endpoints manage FreeSDN’s own normalized rule rows in the firewall.rules table. They do not push to a live device automatically - use the gateway live-write endpoints or the distribution engine for that.
Key endpoints
Section titled “Key endpoints”| Method | Path | Purpose | Permission |
|---|---|---|---|
| GET | /api/v1/firewall/rules |
List rules - filter by device_id, action, is_enabled, site_id |
firewall.view |
| GET | /api/v1/firewall/rules/{id} |
Get one rule | firewall.view |
| POST | /api/v1/firewall/rules |
Create rule | firewall.manage_rules |
| PATCH | /api/v1/firewall/rules/{id} |
Update rule (partial) | firewall.manage_rules |
| DELETE | /api/v1/firewall/rules/{id} |
Soft-delete rule | firewall.manage_rules |
| POST | /api/v1/firewall/rules/reorder |
Reorder rules for a device | firewall.manage_rules |
Rule fields
Section titled “Rule fields”A rule record includes: source_address, source_port, source_zone, dest_address, dest_port, dest_zone, protocol (any/tcp/udp/icmp/…), action (allow / deny / reject / log), log_enabled, is_enabled, and rule_order. Note: schedule_id is not currently settable or returned via the REST API.
Reordering rules
Section titled “Reordering rules”Supply the target device’s UUID and a complete ordered list of rule UUIDs:
POST /api/v1/firewall/rules/reorder?device_id=<device-uuid>["<rule-uuid-1>", "<rule-uuid-2>", "<rule-uuid-3>"]The service sets rule_order = 1, 2, 3, … across only the matching non-deleted rules for that device. Rules not in the list are not touched.
PATCH field allowlist
Section titled “PATCH field allowlist”PATCH only applies whitelisted mutable fields. Non-whitelisted keys (such as device_id or hit_count) are silently ignored, not rejected.
NAT rules (FreeSDN tables)
Section titled “NAT rules (FreeSDN tables)”NAT rules live in firewall.nat_rules. Types: snat, dnat, masquerade, redirect.
| Method | Path | Purpose | Permission |
|---|---|---|---|
| GET | /api/v1/firewall/nat |
List NAT rules | firewall.view |
| GET | /api/v1/firewall/nat/{id} |
Get one NAT rule | firewall.view |
| POST | /api/v1/firewall/nat |
Create NAT rule | firewall.manage_nat |
| PATCH | /api/v1/firewall/nat/{id} |
Update NAT rule | firewall.manage_nat |
| DELETE | /api/v1/firewall/nat/{id} |
Soft-delete NAT rule | firewall.manage_nat |
For live port-forward and source-NAT operations directly on a connected device, use the gateway live-write endpoints (/gateways/{id}/port-forwards, /gateways/{id}/source-nat).
VPN tunnels (FreeSDN tables)
Section titled “VPN tunnels (FreeSDN tables)”VPN tunnels live in firewall.vpn_tunnels. Types: ipsec, openvpn, wireguard, l2tp.
| Method | Path | Purpose | Permission |
|---|---|---|---|
| GET | /api/v1/firewall/vpn |
List tunnels - filter by vpn_type, vpn_status |
firewall.view |
| GET | /api/v1/firewall/vpn/stats |
Aggregate stats: total / up / down / error | firewall.view |
| GET | /api/v1/firewall/vpn/{id} |
Get one tunnel | firewall.view |
| POST | /api/v1/firewall/vpn |
Create tunnel | firewall.manage_vpn |
| PATCH | /api/v1/firewall/vpn/{id} |
Update tunnel | firewall.manage_vpn |
| DELETE | /api/v1/firewall/vpn/{id} |
Soft-delete tunnel | firewall.manage_vpn |
VPN support by vendor
Section titled “VPN support by vendor”| Type | OPNsense | pfSense | MikroTik |
|---|---|---|---|
| IPsec (IKEv1/v2) | Yes | Yes | Yes |
| OpenVPN | Yes | Yes | - |
| WireGuard | Yes | Yes | Staged (no UI tab yet) |
| L2TP / PPTP / SSTP | - | - | Yes |
For live IPsec connect/disconnect, WireGuard server/peer management, and OpenVPN session management, use the gateway live-write endpoints under /api/v1/firewall/gateways/{id}/ipsec/, /wireguard/, and /openvpn/.
IDS/IPS
Section titled “IDS/IPS”FreeSDN tracks IDS alerts in firewall.ids_alerts (FreeSDN’s own table, populated by sync). Live IDS management goes through the gateway adapter.
FreeSDN IDS alert endpoints
Section titled “FreeSDN IDS alert endpoints”| Method | Path | Purpose | Permission |
|---|---|---|---|
| GET | /api/v1/firewall/ids/alerts |
Search alerts - filter by severity, time range, acknowledged, site_id |
firewall.view |
| GET | /api/v1/firewall/ids/alerts/stats |
Alert counts by severity + unacknowledged count | firewall.view |
| POST | /api/v1/firewall/ids/alerts/{id}/acknowledge |
Acknowledge an alert | firewall.manage_ids |
The ids tab in the UI shows a badge with the unacknowledged count.
Live IDS management (OPNsense)
Section titled “Live IDS management (OPNsense)”| Method | Path | Purpose | Permission |
|---|---|---|---|
| GET | /api/v1/firewall/gateways/{id}/ids/settings |
Current IDS settings | firewall.view |
| PUT | /api/v1/firewall/gateways/{id}/ids/settings |
Update settings | firewall.manage_rules |
| GET | /api/v1/firewall/gateways/{id}/ids/alerts |
Live alerts (up to 5,000) | firewall.view |
| DELETE | /api/v1/firewall/gateways/{id}/ids/alerts |
Clear alert log | firewall.manage_rules |
| GET | /api/v1/firewall/gateways/{id}/ids/rulesets |
Rulesets (ET/Open, etc.) | firewall.view |
| POST | /api/v1/firewall/gateways/{id}/ids/rules/{sid}/toggle |
Toggle a rule by SID | firewall.manage_rules |
| POST | /api/v1/firewall/gateways/{id}/ids/control |
Start / stop / restart / update-rules | firewall.manage_rules |
OPNsense supports Suricata. When a critical signature fires, the health monitor emits a firewall.event.ids_critical Fabric event.
IDS mode is configurable per org in module settings: ids_mode = detect (log only) or prevent (block). Default is detect.
Firewall logs
Section titled “Firewall logs”Traffic logs are stored in firewall.logs and scoped per device.
| Method | Path | Purpose | Permission |
|---|---|---|---|
| GET | /api/v1/firewall/logs |
Search logs - filter by action, source_ip, dest_ip, time range |
firewall.view_logs |
For a live streaming log from a connected device, use GET /api/v1/firewall/gateways/{id}/logs/stream (SSE StreamingResponse - long-lived connection).
Gateway connections - live device management
Section titled “Gateway connections - live device management”The /api/v1/firewall/gateways/ family proxies directly to vendor adapters. These are the endpoints behind the GatewayDetailPage tabs in the UI (18 tabs for OPNsense/pfSense, 13 for MikroTik).
Connection lifecycle
Section titled “Connection lifecycle”| Method | Path | Purpose | Permission |
|---|---|---|---|
| GET | /api/v1/firewall/gateways |
List connections | firewall.view |
| GET | /api/v1/firewall/gateways/summary |
Aggregate online/offline/sync state counts | firewall.view |
| GET | /api/v1/firewall/gateways/{id} |
Get one connection (has_credentials bool, no raw cred) |
firewall.view |
| POST | /api/v1/firewall/gateways |
Create connection | firewall.manage_rules |
| PATCH | /api/v1/firewall/gateways/{id} |
Update connection (evicts cached adapter) | firewall.manage_rules |
| DELETE | /api/v1/firewall/gateways/{id} |
Soft-delete (removes device-registry row) | firewall.manage_rules |
Live reads - selected endpoints
Section titled “Live reads - selected endpoints”| Method | Path | Purpose |
|---|---|---|
| GET | /gateways/{id}/status |
Live system status; updates is_online / last_seen |
| GET | /gateways/{id}/firewall-rules |
Live firewall rule list from device |
| GET | /gateways/{id}/nat-rules |
Live NAT rules |
| GET | /gateways/{id}/vpn |
Live VPN status (secrets redacted) |
| GET | /gateways/{id}/interfaces |
Live interface list + statistics |
| GET | /gateways/{id}/dhcp |
Live DHCP leases |
| GET | /gateways/{id}/arp |
ARP table |
| GET | /gateways/{id}/routes/table |
Kernel routing table |
| GET | /gateways/{id}/health-check |
Multi-subsystem deep health (healthy bool) |
| GET | /gateways/{id}/ha-status |
CARP / HA sync status |
All paths above are relative to /api/v1/firewall. All require firewall.view.
Live writes - selected endpoints
Section titled “Live writes - selected endpoints”| Method | Path | Purpose | Permission |
|---|---|---|---|
| POST | /gateways/{id}/firewall-rules |
Push rule to device | firewall.manage_rules |
| PUT | /gateways/{id}/firewall-rules/{vendor_rule_id} |
Update vendor rule | firewall.manage_rules |
| POST | /gateways/{id}/firewall-rules/{vendor_rule_id}/toggle?enabled=<bool> |
Enable (?enabled=true, default) or disable (?enabled=false) a rule |
firewall.manage_rules |
| POST | /gateways/{id}/port-forwards |
Create port-forward (DNAT) | firewall.manage_rules |
| POST | /gateways/{id}/source-nat |
Create SNAT rule | firewall.manage_rules |
| POST | /gateways/{id}/wireguard/servers |
Create WireGuard server | firewall.manage_rules |
| POST | /gateways/{id}/wireguard/peers |
Add WireGuard peer | firewall.manage_rules |
| POST | /gateways/{id}/openvpn/instances |
Create OpenVPN instance | firewall.manage_rules |
| POST | /gateways/{id}/ipsec/{vendor_id}/connect |
Bring IPsec tunnel up | firewall.manage_rules |
| POST | /gateways/{id}/ipsec/{vendor_id}/disconnect |
Tear IPsec tunnel down | firewall.manage_rules |
| POST | /gateways/{id}/routes/static |
Create static route | firewall.manage_rules |
| POST | /gateways/{id}/aliases |
Create alias (firewall object) | firewall.manage_rules |
| POST | /gateways/{id}/dns/overrides |
Create DNS host override | firewall.manage_rules |
| POST | /gateways/{id}/dhcp/static-mappings |
Create DHCP static mapping | firewall.manage_rules |
| POST | /gateways/{id}/services/{name}/control |
Start / stop / restart service | firewall.manage_rules |
| POST | /gateways/{id}/reboot |
Reboot device | firewall.admin |
| POST | /gateways/{id}/halt |
Halt device | firewall.admin |
| POST | /gateways/{id}/firmware/update |
Apply firmware update | firewall.admin |
| GET | /gateways/{id}/config/download |
Download running config | controller:write |
Diagnostics
Section titled “Diagnostics”All diagnostics run on the remote device and require firewall.view:
| Method | Path | Purpose |
|---|---|---|
| POST | /gateways/{id}/diagnostics/ping |
Ping (1-20 packets) |
| POST | /gateways/{id}/diagnostics/traceroute |
Traceroute |
| POST | /gateways/{id}/diagnostics/dns-lookup |
DNS lookup |
| GET | /gateways/{id}/diagnostics/connections |
Active PF state connections |
| GET | /gateways/{id}/diagnostics/pf-info |
PF filter info |
Diagnostics host fields are validated against the same SSRF allowlist as the gateway host field.
OPNsense - feature domains
Section titled “OPNsense - feature domains”OPNsense is the most complete adapter with 13 independent domains, each accessible from the GatewayDetailPage under its own tab:
| Domain | UI tab | Example endpoint prefix |
|---|---|---|
| Firewall rules | rules | /gateways/{id}/firewall-rules |
| NAT + port forwards | nat | /gateways/{id}/nat-rules, /port-forwards, /source-nat, /nat/onetoone |
| VPN (OpenVPN + IPsec + WireGuard) | vpn | /gateways/{id}/openvpn, /ipsec, /wireguard |
| Interfaces + VIPs + LAGGs | interfaces | /gateways/{id}/interfaces, /vlans, /virtual-ips, /laggs |
| DHCP (Kea DHCPv4/v6 + relay + static maps) | dhcp | /gateways/{id}/dhcp, /kea/dhcpv4, /kea/dhcpv6, /dhcp-relay |
| DNS (Unbound host/domain overrides) | dns | /gateways/{id}/dns/overrides, /dns/domain-overrides |
| Aliases (address + port groups) | aliases | /gateways/{id}/aliases |
| Routing (static + OSPF + BGP + ARP + NDP) | routing | /gateways/{id}/routes/static, /routes/table, /arp, /ndp |
| IDS/IPS (Suricata) | ids | /gateways/{id}/ids/... |
| Traffic shaper (pipes + queues + rules) | shaper | /gateways/{id}/shaper/pipes, /shaper/queues, /shaper/rules |
| Services (start/stop/restart) | services | /gateways/{id}/services |
| Backups + config diff | backups | /gateways/{id}/backups, /config/diff |
| System + firmware + monitoring | system / monitoring | /gateways/{id}/firmware, /monitoring/temperature, /monitoring/traffic |
Additional read surfaces: HAProxy (/haproxy, /haproxy/servers, /haproxy/backends, /haproxy/frontends), ACME certificates (/acme, /acme/certificates), Tailscale (/tailscale), CrowdSec (/crowdsec), captive portal (/captive-portal), Telegraf, Monit, NetFlow, DynDNS, Syslog.
MikroTik - feature surface
Section titled “MikroTik - feature surface”MikroTik requires RouterOS v7 and REST API access. The adapter communicates only over the RouterOS REST API - no SSH, no WinBox API.
| Status | Domains |
|---|---|
| UI tab available | System, Interfaces, IP, DHCP, Firewall, DNS, VPN (L2TP/PPTP/SSTP), Hotspot, Queues, Firmware, Backup, Topology/Neighbors, SNMP |
| API-only (no UI tab yet) | CAPsMAN, PPP/PPPoE, BGP/OSPF, IPsec/Security |
Firmware lifecycle (channel check, download, install), config backup/restore, topology/neighbor discovery (/ip/neighbor with LLDP/CDP/MNDP dedup), and SNMPv3 user management each have a dedicated UI tab.
Gateway orchestration
Section titled “Gateway orchestration”Orchestration is the multi-controller capability. You designate one gateway per site as the brain (the authoritative routing and VLAN authority) and one or more switch/AP controllers as limbs (Layer 2 devices that receive config from the brain). FreeSDN holds a canonical desired-state model and distributes it.
Architecture layers
Section titled “Architecture layers”| Layer | Description |
|---|---|
| Layer 0 | Controller-direct; always works; single-controller config push |
| Layer 1 | Multi-controller VLAN visibility; alignment score; per-cell copy |
| Layer 2 | Gateway orchestration; brain/limb roles; read-only brain; canonical VLAN distribution |
| Layer 3 | Full write-to-brain orchestration is not available |
The brain adapter is read-only by default in the orchestration context: FreeSDN reads config from it to populate canonical state but does not push arbitrary mutations back. Changes to the brain’s running config go through the gateway live-write endpoints, not orchestration.
Site role map
Section titled “Site role map”The role map records which device is the brain and which are limbs for a site. Exactly one brain is required for orchestration to function; the validator enforces this.
| Method | Path | Purpose | Permission |
|---|---|---|---|
| GET | /api/v1/firewall/topology/{site_id} |
Get role map (brain/limb assignments + authority_map) | gateway.view |
| PUT | /api/v1/firewall/topology/{site_id} |
Upsert role map | gateway.manage_topology |
| DELETE | /api/v1/firewall/topology/{site_id} |
Remove role map | gateway.manage_topology |
| POST | /api/v1/firewall/topology/{site_id}/validate |
Validate (exactly one brain, capability checks) | gateway.manage_topology |
The authority_map JSONB in the role map records per-resource authority defaults: vlan_interface, dhcp, dns, firewall_rule, nat, vpn default to brain; vlan_l2 defaults to FreeSDN; port_profile, ssid, poe default to limb.
Canonical VLANs
Section titled “Canonical VLANs”A canonical VLAN is the site-wide desired state: one definition, independent of any specific controller. It lives in gateway.gw_canonical_vlans - separate from the per-controller VLAN records in network.vlans. You define it once, then distribute to any device that needs it.
| Method | Path | Purpose | Permission |
|---|---|---|---|
| GET | /api/v1/firewall/vlans |
List canonical VLANs | gateway.view |
| GET | /api/v1/firewall/vlans/{id} |
VLAN detail (+ DHCP scope + reservations) | gateway.view |
| POST | /api/v1/firewall/vlans |
Create canonical VLAN | gateway.manage_vlans |
| PATCH | /api/v1/firewall/vlans/{id} |
Update VLAN | gateway.manage_vlans |
| DELETE | /api/v1/firewall/vlans/{id} |
Delete VLAN | gateway.manage_vlans |
Canonical DHCP scopes and reservations for a VLAN:
| Method | Path | Permission |
|---|---|---|
| GET | /api/v1/firewall/dhcp/scopes |
gateway.view |
| POST | /api/v1/firewall/dhcp/scopes |
gateway.manage_dhcp |
| POST | /api/v1/firewall/dhcp/reservations |
gateway.manage_dhcp |
| DELETE | /api/v1/firewall/dhcp/reservations/{id} |
gateway.manage_dhcp |
Canonical DNS overrides:
| Method | Path | Permission |
|---|---|---|
| GET | /api/v1/firewall/dns/records |
gateway.view |
| POST | /api/v1/firewall/dns/records |
gateway.manage_dns |
| PATCH | /api/v1/firewall/dns/records/{id} |
gateway.manage_dns |
| DELETE | /api/v1/firewall/dns/records/{id} |
gateway.manage_dns |
VLAN templates
Section titled “VLAN templates”Templates are org-level blueprints for standard VLAN configurations (IoT network, guest VLAN, management VLAN, etc.). Apply a template to a site to instantiate a canonical VLAN from it.
| Method | Path | Purpose | Permission |
|---|---|---|---|
| GET | /api/v1/firewall/templates |
List templates | gateway.view |
| POST | /api/v1/firewall/templates |
Create template | gateway.manage_vlans |
| PATCH | /api/v1/firewall/templates/{id} |
Update template | gateway.manage_vlans |
| DELETE | /api/v1/firewall/templates/{id} |
Delete template | gateway.manage_vlans |
| POST | /api/v1/firewall/templates/{id}/apply/{site_id} |
Instantiate template at site | gateway.manage_vlans |
Distribution engine
Section titled “Distribution engine”The distribution engine pushes canonical VLANs to one or more devices at a site. It follows a Saga pattern: per-tier compensating rollback is available if a distribution fails partway through.
One distribution runs per site at a time - a DistributionLock row in the database prevents concurrent distributions. The lock auto-expires after ~5 minutes to recover from worker crashes.
Plan structure:
- Tier 0 prerequisites - brain + each limb verified reachable
- Tier 1 - L3 VLAN config pushed to brain
- Limb tiers - L2 VLAN config pushed to each limb in turn
| Method | Path | Purpose | Permission |
|---|---|---|---|
| GET | /api/v1/firewall/distribution |
List distribution records | gateway.view |
| GET | /api/v1/firewall/distribution/{id} |
Detail (plan + per-step results) | gateway.view |
| POST | /api/v1/firewall/distribution/trigger |
Trigger distribution (vlan_id + site_id) |
gateway.distribute |
| POST | /api/v1/firewall/distribution/{id}/retry |
Retry a failed distribution | gateway.distribute |
| POST | /api/v1/firewall/distribution/{id}/rollback |
Saga rollback | gateway.distribute |
Drift detection
Section titled “Drift detection”FreeSDN polls connected brain devices every 15 minutes (configurable via drift_check_interval_minutes) and compares running config against canonical desired state. When a divergence is found it creates a DriftEvent.
Drift types: resource_missing, resource_modified, resource_added, suppression_violated, tag_removed. Severities: critical, warning, info.
| Method | Path | Purpose | Permission |
|---|---|---|---|
| GET | /api/v1/firewall/drift/events |
List drift events (paginated) | gateway.drift |
| GET | /api/v1/firewall/drift/summary |
Counts by severity + pending/resolved | gateway.drift |
| POST | /api/v1/firewall/drift/check/{site_id} |
Run a drift check now (enqueues task) | gateway.drift |
| POST | /api/v1/firewall/drift/events/{id}/resolve |
Resolve: reapply, accept, or ignore |
gateway.drift |
| GET | /api/v1/firewall/drift/suppressions |
List suppression rules | gateway.drift |
| POST | /api/v1/firewall/drift/suppressions |
Create suppression rule | gateway.drift |
| DELETE | /api/v1/firewall/drift/suppressions/{id} |
Deactivate suppression rule | gateway.drift |
Celery beat runs gateway.check_all_sites_drift every 15 minutes and gateway.sync_all_gateways every 5 minutes (both on the sync queue).
Brownfield import
Section titled “Brownfield import”If you are adding an existing site to FreeSDN that already has config running on devices, use the import wizard. It reads the current running state from the brain and limbs and populates canonical objects from it.
The wizard runs as a 6-step session:
| Step | Name | What happens |
|---|---|---|
| 1 | Discover | Auto-discover devices at the site |
| 2 | Assign roles | Submit { gateway_id: "brain" | "limb" } for each discovered device |
| 3 | Scan | Full config pull from brain and all limbs |
| 4 | Reconcile | Submit decisions per resource: adopt, skip, merge, or ignore |
| 5 | Distribute | Push adopted resources to canonical state + devices |
| 6 | Verify | Confirm actual device state matches canonical |
Steps 2 and 4 require a POST to /import/{session_id}/step with a payload. Steps 1, 3, 5, and 6 are triggered automatically when the session advances.
| Method | Path | Purpose | Permission |
|---|---|---|---|
| POST | /api/v1/firewall/import/start |
Start import session | gateway.import |
| GET | /api/v1/firewall/import/{id} |
Session state (current step + per-step payloads) | gateway.import |
| POST | /api/v1/firewall/import/{id}/step |
Advance wizard step | gateway.import |
| POST | /api/v1/firewall/import/{id}/cancel |
Cancel session | gateway.import |
Reconciliation (direct, no wizard)
Section titled “Reconciliation (direct, no wizard)”For ongoing operations - after the initial import - you can re-import from the brain, check alignment, and distribute to limbs without using the wizard:
| Method | Path | Purpose | Permission |
|---|---|---|---|
| POST | /api/v1/firewall/reconciliation/{site_id}/import |
Import VLAN interfaces from brain → create/update canonical (flags orphans) | gateway.import |
| GET | /api/v1/firewall/reconciliation/{site_id}/alignment |
Compare canonical vs actual device state | gateway.view |
| POST | /api/v1/firewall/reconciliation/{site_id}/distribute |
Push L2 VLAN config to limb devices | gateway.distribute |
The alignment endpoint returns per-resource status: aligned, missing, modified, extra, or error.
Orchestration dashboard
Section titled “Orchestration dashboard”GET /api/v1/firewall/dashboard/overview returns a rollup: gateway counts, sites with orchestration, canonical VLAN count, and drift summary. The dashboard also exposes read-only imported snapshots of firewall rules, NAT rules, VPN tunnels, IDS events, interfaces, and DHCP leases from brain devices - these are cached snapshots for visibility, not live reads.
Module settings
Section titled “Module settings”Settings are per-org, stored in JSONB, and configurable through the UI at Firewall > Settings.
| Setting | Type | Default | Description |
|---|---|---|---|
default_policy |
allow | deny | deny |
Default packet policy for new rules |
log_blocked |
bool | true |
Log blocked connections to firewall.logs (primary PostgreSQL) |
ids_enabled |
bool | true |
Enable IDS globally |
ids_mode |
detect | prevent | detect |
Detect (log) or prevent (block) |
log_retention_days |
int 7-365 | 30 |
Firewall log retention |
sync_interval_minutes |
int 1-1440 | 5 |
Gateway sync frequency |
drift_check_interval_minutes |
int 5-1440 | 15 |
Drift check frequency |
distribution_lock_ttl_seconds |
int 60-3600 | 300 |
Max time a distribution lock is held |
auto_remediate_drift |
bool | false |
Automatically push fixes for detected drift |
import_retention_days |
int 1-365 | 90 |
Import session retention |
Permissions
Section titled “Permissions”| Permission code | Purpose | Notes |
|---|---|---|
firewall.view |
View rules, NAT, VPN, IDS alerts, logs, gateway status | Gate for almost all GET routes |
firewall.manage_rules |
Create/edit/delete rules; also gates gateway CRUD and most live writes | Required for all write operations on gateway connections |
firewall.manage_nat |
NAT rule CRUD (FreeSDN tables) | |
firewall.manage_vpn |
VPN tunnel CRUD (FreeSDN tables) | |
firewall.manage_ids |
Acknowledge IDS alerts (FreeSDN tables) | |
firewall.view_logs |
View traffic logs | |
firewall.manage_gateways |
Declared in manifest; not used by any route - grant firewall.manage_rules instead |
|
firewall.admin |
Reboot / halt device; apply firmware update | Not declared in the module manifest; must be in the global role catalog |
controller:write |
Download running config | Core-level permission; not in the module manifest |
gateway.view |
View orchestration topology, canonical resources, drift | |
gateway.manage_topology |
Edit brain/limb role maps | |
gateway.manage_vlans |
Canonical VLAN + template CRUD | |
gateway.manage_dhcp |
DHCP scope and reservation CRUD | |
gateway.manage_dns |
DNS override CRUD | |
gateway.distribute |
Trigger distribution, retry, rollback | |
gateway.import |
Run brownfield import wizard and reconciliation import | |
gateway.drift |
View, resolve, and suppress drift events | |
gateway.diagnostics |
Run orchestration-tier diagnostics (ping, traceroute, backup, restart-service) |
Site scoping
Section titled “Site scoping”Both FirewallService and GatewayService enforce per-user site grants. When a user has site-limited access, the service filters by accessible_site_ids so a site-limited operator cannot see or modify firewall devices or gateway connections outside their assigned sites.
Fabric integration
Section titled “Fabric integration”The Firewall module participates in the Fabric (universal app-interconnect) as both a source of operations and an emitter of events.
Operation
Section titled “Operation”| ID | Tier | Description | Permission |
|---|---|---|---|
firewall.search_alerts |
Native | Recent IDS/IPS + security alerts (1-200 results, org-scoped) | firewall.view_logs |
Emitted events
Section titled “Emitted events”| Event | Trigger |
|---|---|
gateway.sync.completed |
Gateway sync finishes successfully |
gateway.brain.offline |
Brain device stops responding |
firewall.event.ids_critical |
Critical IDS signature fires |
firewall.event.wan_down |
WAN gateway goes down |
firewall.event.wan_up |
WAN gateway comes back up |
firewall.event.gateway_unreachable |
Gateway stops responding |
firewall.event.gateway_online |
Gateway becomes reachable again |
Events are emitted on state transitions only - the health monitor (firewall.poll_health, every 2 minutes) diffs each gateway’s current state against the stored fabric_health snapshot and fires an event only when the state changes.
Background tasks
Section titled “Background tasks”| Task | Schedule | Queue | Purpose |
|---|---|---|---|
gateway.sync_all_gateways |
Every 5 min | sync | Sync all enabled gateway connections (rate-limited 2/min) |
gateway.check_all_sites_drift |
Every 15 min | sync | Drift check all sites with orchestration |
gateway.cleanup_distribution_locks |
Every 10 min | default | Clean up expired distribution locks |
firewall.poll_health |
Every 2 min | metrics | Health poll; emit Fabric events on transitions |
Configuration backup scope
Section titled “Configuration backup scope”The Firewall module contributes to the Configuration Backup portable snapshot (.fsdn archive):
Included: FirewallDevice, FirewallRule, NATRule, VPNTunnel, GatewayConnection (credentials stripped).
Excluded: firewall.ids_alerts, firewall.logs (traffic logs), firewall.gateway_sync_logs, and GatewayConnection credentials.
Restore order: FirewallDevice → rules / NAT / VPN → GatewayConnection.
Key honesty caveats
Section titled “Key honesty caveats”Next steps
Section titled “Next steps”- OPNsense adapter reference - detailed coverage of all 13 OPNsense domains, API key setup, and known limitations
- pfSense adapter reference - pfSense-specific credential model and supported operations
- MikroTik adapter reference - RouterOS v7 REST API requirements, API user setup, and tab coverage
- OpenWRT adapter reference - preview-tier notes and current limitations
- Network Management - switch ports, VLANs, APs, topology (Omada/UniFi)
- Roles and permissions - full 5-tier role hierarchy and permission grant model
- Secrets and credentials - how credentials are stored, encrypted, and redacted
- Fabric connections - build automations that trigger on Firewall events
- Configuration Backup - portable snapshot scope and restore order
All product names, logos, and brands are property of their respective owners. FreeSDN is an independent project and is not affiliated with or endorsed by the vendors it integrates with. See Trademarks.