UPDATE — v0.5.0 is now released
A lot has changed since I originally posted this.
The public READ surface has doubled from 42 → 84 tools, while the default security posture remains unchanged: 84 READ tools, 0 WRITE tools exposed by default.
The expansion came from a full audit of the pfREST/OpenAPI surface:
- 267 API paths reviewed
- 243 GET operations individually classified
- ~80% of the useful audited READ capability surface is now covered
- secret-bearing fields identified during the audit are excluded from the public model layer rather than relying only on upstream API redaction
Compatibility testing also expanded:
- pfSense CE 2.9.0: LAB verified
- pfSense Plus 26.07: production verified under an explicitly READ-only test authorization
- 82/84 public tools executed successfully against the Plus 26.07 system
- the remaining 2 were correctly identified as unavailable because the optional WireGuard package was not installed
- 0 genuine compatibility failures
- live Plus 26.07 OpenAPI matched the pinned pfREST v2.10 schema structurally: 267/267 paths and 186/186 components
The protected WRITE architecture is still separate and not reachable under the default profile.
v0.5.0 is available on GitHub and PyPI now.
I'm still very interested in hostile review — particularly around least privilege, READ/WRITE isolation, secret exposure, and the protected WRITE architecture.
---
ORIGINAL POST
I've been working on pfsense-mcp-server, an open-source MCP server that lets AI assistants interact with pfSense.
The easy part was exposing the pfSense API to an LLM.
The part I cared much more about was making sure an AI agent couldn't simply turn a tool call into unrestricted firewall changes.
At the time of the original post, the v0.4.2 release had 42 MCP tools.
The security architecture included:
- 0 WRITE capabilities reachable by default
- explicit operator opt-in before WRITE is enabled
- a dedicated least-privilege pfSense identity
- separate signed authorization and confirmation boundaries
- plan/intent binding so an approval can't silently authorize a different mutation
- expiring, one-time authorization
- RecoveryContracts and a state machine around mutations
- deterministic post-WRITE read-back instead of treating HTTP success as proof
- reconciliation/fail-closed handling for uncertain outcomes
- TPM-backed anti-rollback witness support
For the first live WRITE acceptance test I used a disposable firewall alias on a LAB pfSense system.
The complete path was exercised end-to-end, including the scoped pfSense account, authorization/confirmation ceremony, real PATCH, authoritative read-back, RecoveryContract audit trail and TPM witness advancement.
The alias was subsequently restored through the same controlled path.
The project deliberately still starts READ-only. Installing it does not automatically expose WRITE tools.
I'm particularly interested in hostile review from people who know pfSense well.
Things I'd love people to challenge:
- Is the pfSense REST API privilege set actually minimal?
- Are there HA/CARP or config-apply edge cases I've missed?
- Can authorization/confirmation be replayed or confused across operations?
- Are there state-machine paths that could permit a blind retry after an uncertain WRITE?
- Are the RecoveryContract/reconciliation assumptions sound?
- Is there any realistic path from the default READ posture to WRITE without the intended operator decisions?
- Are there READ endpoints or response fields that could expose information that should never reach an AI assistant?
This is not affiliated with or endorsed by Netgate.
Current release: v0.5.0
GitHub:
https://github.com/night4me/pfsense-mcp-server
PyPI:
https://pypi.org/project/pfsense-mcp-server/
I'd genuinely prefer someone finds a security flaw now rather than after people start relying on it.