OPNsense Guard
Safety-first OPNsense firewall MCP: read-only by default, planned and auto-reverting writes.
Installation
Ajoutez ceci à la configuration MCP de votre client (Claude Desktop : claude_desktop_config.json ; Cursor : .cursor/mcp.json ; VS Code et les autres utilisent la même forme « mcpServers »).
opnsense-guard-mcpclaude mcp add opnsense-guard -e 'OPNSENSE_URL=<OPNSENSE_URL>' -e 'OPNSENSE_API_KEY=<your opnsense_api_key>' -e 'OPNSENSE_API_SECRET=<your opnsense_api_secret>' -e 'OPNSENSE_CA_BUNDLE=<OPNSENSE_CA_BUNDLE>' -- uvx opnsense-guard-mcp==0.1.1
{
"mcpServers": {
"opnsense-guard": {
"command": "uvx",
"args": [
"opnsense-guard-mcp==0.1.1"
],
"env": {
"OPNSENSE_URL": "<OPNSENSE_URL>",
"OPNSENSE_API_KEY": "<your opnsense_api_key>",
"OPNSENSE_API_SECRET": "<your opnsense_api_secret>",
"OPNSENSE_CA_BUNDLE": "<OPNSENSE_CA_BUNDLE>"
}
}
}
}
Variables d'environnement
OPNSENSE_URLobligatoire Firewall base URL, e.g. https://firewall.example.internalOPNSENSE_API_KEYobligatoire secret API key of a least-privilege read userOPNSENSE_API_SECRETobligatoire secret API secret of the read userOPNSENSE_CA_BUNDLECA file for a self-signed firewall certificate
Documentation
Tirée du README du projet sur GitHub, actualisée chaque semaine. Les liens ouvrent GitHub.
Sommaire
opnsense-guard-mcp
A safety-first MCP server for OPNsense 26.7+.
Read-only by default. Every write is planned, policy-checked, logged, and auto-reverted unless you confirm it.
Status: alpha. Tested against a live OPNsense 26.7.4 with least-privilege API users: read tools, policy file, change planning with diffs, applying changes with verification and automatic revert, crash recovery, and a hash-chained audit log, over stdio or authenticated HTTPS. See DESIGN.md.
Not affiliated with Deciso B.V. "OPNsense" is a trademark of Deciso B.V.
What makes it different
| opnsense-guard-mcp | |
|---|---|
| Default mode | read-only |
| Read path | GET, plus POST to exactly two search endpoints (states, rules) with a fixed three-field body; path allowlist, verb denylist, traversal/encoding rejected, redirects not followed |
| Writes | separate API user, exact endpoint allowlist, plan → policy check → human approval → apply → verify → auto-revert unless confirmed |
| Rollback on 26.7+ | own object-level snapshots + revert (the savepoint API is gone in 26.7); survives restarts |
| Guardrails | allowlist policy file: interfaces, networks, rule limits, anti-lockout |
| Audit | hash-chained JSONL (audit verify detects edits); syslog planned |
| Transport | stdio, or Streamable HTTP that always needs a bearer token; optional TLS / mTLS |
| Dangerous tools | none exist: no reboot, firmware, service control, SSH or XML edits |
Quick start (read-only)
In OPNsense (System → Access → Users), create a user with a scrambled password, no group (not
admins) and these privileges, then create an API key for it. Skip any that aren't installed; the matching tool just reports "forbidden".Privilege Used by Firewall: Rules [new] firewall_rules,firewall_rule_get, planningFirewall: Aliases (view-only) firewall_aliases, planningStatus: Interfaces interfacesStatus: Services service_listDiagnostics: ARP Table arp_tableDiagnostics: Routing tables routing_tableDiagnostics: Show States firewall_statesDiagnostics: Logs: Firewall: Live View firewall_logDiagnostics: Logs: System system_logLobby: Dashboard system_information,system_resources, health probeServices: Intrusion Detection suricata_*Services: DHCP: Kea(v4) / Services: Dnsmasq DNS/DHCP: Settings / Status: DHCP leases (ISC) dhcp_leases: only the one you useSystem: Firmware (optional) system_status. It also allows installing updates, so leave it out unless you need it.Most OPNsense privileges are not view-only (Firewall: Aliases is an exception): several of these also allow edits through the API. The server only sends GET requests with this key, except a search POST to two exact endpoints (
diagnostics/firewall/query_states,firewall/filter/searchRule) with a body it builds itself (current,rowCount,searchPhrase); OPNsense answers those lists properly only over POST. Still keep the list short: the privileges are what OPNsense itself enforces.- Install:
pipx install opnsense-guard-mcp - Configure (copy
.env.exampleto.env, or set env vars):OPNSENSE_URL=https://firewall.example.internal OPNSENSE_API_KEY=... OPNSENSE_API_SECRET=... OPNSENSE_CA_BUNDLE=/path/to/firewall-ca.pem # or OPNSENSE_VERIFY_SSL=false - Check the config (does not contact the firewall):
opnsense-guard-mcp --check - Add to your MCP client, e.g. Claude Code:
claude mcp add opnsense -- opnsense-guard-mcp --env-file /path/to/.env
HTTP mode (shared server)
HTTP never runs without authentication. The server refuses to start if:
- there are no tokens, or
- it binds to a non-loopback address without TLS
(override with
MCP_ALLOW_PLAINTEXT=trueonly behind a TLS proxy).
Create a token per client. It is printed once; only its SHA-256 is stored:
opnsense-guard-mcp token new --name laptop --scope readtoken listshows names and scopes;token revoke --name laptoptakes effect on the next request, no restart needed.- Start the server:
MCP_TRANSPORT=http MCP_HOST=0.0.0.0 MCP_PORT=8848 \ MCP_TLS_CERT=server.pem MCP_TLS_KEY=server.key \ opnsense-guard-mcp - Point the client at
https://<host>:8848/mcpwith headerAuthorization: Bearer <token>.
| Variable | Default | Purpose |
|---|---|---|
MCP_HOST / MCP_PORT | 127.0.0.1 / 8848 | listen address |
OPNSENSE_GUARD_TOKENS_FILE | ./tokens.json | token hashes |
MCP_TLS_CERT / MCP_TLS_KEY | – | serve HTTPS |
MCP_TLS_CLIENT_CA | – | require client certificates (mTLS) |
MCP_ALLOWED_HOSTS | localhost/127.0.0.1/MCP_HOST on the port | accepted Host headers (DNS-rebinding guard) |
MCP_ALLOWED_ORIGINS | none | browser origins allowed; any other Origin is refused |
MCP_RATE_LIMIT_PER_MIN | 120 | requests per token per minute |
MCP_ALLOW_PLAINTEXT | false | allow plain HTTP on a non-loopback address |
Repeated failed logins from one IP (10 a minute) are throttled with HTTP 429.
Claude Code and a private CA: Claude Code (Node) ignores the Windows/macOS
certificate store for MCP servers. Point NODE_EXTRA_CA_CERTS at your CA file
in the env block of ~/.claude/settings.json, then fully quit and reopen
Claude, or the connection fails with UNABLE_TO_VERIFY_LEAF_SIGNATURE.
Running in Docker
The image runs as a non-root user and keeps its audit log, change journal and
tokens in /data.
docker build -t opnsense-guard-mcp .
# stdio, for a local MCP client
docker run -i --rm --env-file .env -v guard-data:/data opnsense-guard-mcp
# HTTP: create a token, then serve (put the TLS files in the volume or mount them)
docker run --rm -v guard-data:/data opnsense-guard-mcp token new --name laptop --scope read
docker run -d --name opnsense-guard --env-file .env -v guard-data:/data -p 8848:8848 \
-e MCP_TRANSPORT=http -e MCP_HOST=0.0.0.0 -e MCP_ALLOWED_HOSTS=mcp.example.internal:8848 \
-e MCP_TLS_CERT=/data/tls/server.pem -e MCP_TLS_KEY=/data/tls/server.key \
opnsense-guard-mcp
Mount a policy file and set OPNSENSE_GUARD_POLICY to enable writes. Stop the
container with docker stop (not kill) so unconfirmed changes are reverted.
Policy and planning
Without a policy.yaml the server is read-only. To enable planning, copy
policy.example.yaml to policy.yaml (or point
OPNSENSE_GUARD_POLICY at it) and edit it. Unknown keys are an error.
Planning tools (rule_plan, alias_plan) read the current state, apply every
policy check, and return a diff plus the reasons for any refusal. They never
change the firewall. Example result for a refused rule:
+ [wan] pass in inet any from any to any '[mcp]'
interface 'wan' is not in the allowed interfaces
any -> any pass rules are forbidden
inbound pass rules on WAN are forbidden
every rule written by this server must have logging on
Built-in guardrails: only listed interfaces/actions/directions; every IP inside
allow_networks; no any→any pass, WAN inbound pass, floating rules or rules
without logging; edit only rules tagged [mcp] (unless only_modify_own: false);
anti-lockout (no block rule that matches this server's own IP, and only new
pass rules on the interface it reaches the API through).
Applying changes
Change tools appear only when both the policy is in write mode and a second, write-capable API user is configured:
OPNSENSE_WRITE_API_KEY=... # a different OPNsense user than the read one
OPNSENSE_WRITE_API_SECRET=...
Give that user exactly two privileges: Firewall: Rules [new] and
Firewall: Alias: Edit, no group. (Plain Firewall: Aliases is view-only:
it allows search/get but not addItem/setItem/delItem/reconfigure.) Over HTTP, the
change tools need a token with --scope write.
The flow:
rule_plan/alias_plan→ diff +plan_id(changes nothing).change_apply(plan_id)→ asks the human to approve the diff (MCP elicitation where the client supports it; otherwise the tool returns the diff and must be called again withuser_confirmed=true). Then it re-reads the firewall (refuses if anything drifted since planning), stores the before-state, writes, applies, reads everything back to verify, and checks the API is still reachable. Any failure → everything is reverted.- The change is pending: it is reverted automatically after
confirm_timeout_s(default 60 s) unlesschange_confirmis called. change_rollbackundoes a pending or confirmed change. A revert never overwrites an object someone else edited meanwhile (statusconflict).
Unconfirmed changes are also reverted when the server stops and, after a
crash, when it starts again. Only one change can be open at a time, and
max_changes_per_hour is enforced.
Limits, stated plainly
- The revert timer runs on the MCP host. If a change cuts that host off from the firewall API, the revert cannot reach it. The anti-lockout policy and the health probe right after apply exist to prevent and catch this, but they are not the same as the firewall-side savepoint older OPNsense versions had.
- Without elicitation,
user_confirmed=trueis the model's word that the user agreed. The audit log records which method was used.
Audit log
Every plan (allowed or refused) and server start is written to
$OPNSENSE_GUARD_DATA_DIR/audit.jsonl (default ./data). Each line holds the
hash of the previous one, so edits, deletions and reordering are detectable:
opnsense-guard-mcp audit verify
opnsense-guard-mcp audit tail -n 20
Credentials are never written to it.
Tools (read-only)
system_status, system_information, system_resources, service_list,
posture_check, suricata_status, suricata_settings, suricata_alert_status,
firewall_rules, firewall_rule_get, firewall_aliases, firewall_log,
firewall_states, system_log, interfaces, arp_table, routing_table,
dhcp_leases, raw_get, policy_show, security_audit.
In write mode also: rule_plan, alias_plan, plan_get, audit_tail.
With write credentials also: change_apply, change_confirm, change_rollback,
change_list, change_get.
firewall_rules returns one line per rule (50 by default) so a full ruleset
fits in a chat; pass detail=true for the raw OPNsense rows, or use
firewall_rule_get for one rule.
Over HTTP, a read token still sees the change tools in the tool list, but
calling one is refused (needs a token with scope 'write').
Development
python -m venv .venv && . .venv/bin/activate
pip install -e ".[dev]"
ruff check . && mypy && pytest
Security
See SECURITY.md. Please report vulnerabilities privately.
Licence
MIT. See LICENSE.
Questions fréquentes
Qu'est-ce que OPNsense Guard ?
Safety-first OPNsense firewall MCP: read-only by default, planned and auto-reverting writes.
Comment installer OPNsense Guard ?
Il s'exécute sur votre machine. Dans Claude Code : claude mcp add opnsense-guard -e 'OPNSENSE_URL=<OPNSENSE_URL>' -e 'OPNSENSE_API_KEY=<your opnsense_api_key>' -e 'OPNSENSE_API_SECRET=<your opnsense_api_secret>' -e 'OPNSENSE_CA_BUNDLE=<OPNSENSE_CA_BUNDLE>' -- uvx opnsense-guard-mcp==0.1.1. Pour les autres clients, collez la configuration JSON de cette page dans leurs réglages MCP.
Faut-il une clé API pour OPNsense Guard ?
Oui. Il attend OPNSENSE_URL, OPNSENSE_API_KEY, OPNSENSE_API_SECRET, à renseigner dans sa configuration.
OPNsense Guard est-il open source ?
Oui. Son code est sur GitHub sous licence MIT.
Qui maintient OPNsense Guard ?
Il est publié par d3ltasecurity. AI.tn le référence sans le maintenir ni le recommander.
Issu du registre MCP officiel. Être listé n'est pas une recommandation : vérifiez le code et les permissions d'un serveur avant de lui donner accès à vos données.