Skip to main content
Glama
README.md
# pfSense MCP Server

An [MCP](https://modelcontextprotocol.io) server that lets an AI agent (Claude Code, Claude Desktop, Kimi Code) manage a pfSense router through the [pfSense REST API package](https://github.com/pfrest/pfSense-pkg-RESTAPI) (pfrest).

```
┌─────────────┐   MCP (stdio)   ┌──────────────────┐   HTTPS   ┌─────────────────────┐
│ AI Agent    │ ──────────────► │  pfsense-mcp     │ ────────► │ pfSense router      │
│ (Claude)    │                 │  (Python server) │           │  REST API v2 pkg    │
└─────────────┘                 └──────────────────┘           └─────────────────────┘
```

## Features

- **236 hand-written tools across 17 modules** covering most of the pfSense configuration surface: system, interfaces, firewall, NAT, aliases, DHCP, DNS, routing, diagnostics, services, WireGuard, certificates, users, NTP, IPsec, OpenVPN, and status
- **v2 API**: targets the REST API **v2** contract (pfSense CE 2.8+/24.x, pfSense-pkg-RESTAPI 2.x) at `/api/v2/`
- **Safety first**:
  - `confirm=True` required for every destructive operation (delete, reboot, restart, halt)
  - Automatic config backup before destructive changes
  - Read-only mode (`PFSENSE_READ_ONLY=true`) removes all write tools entirely
  - Audit log with secret redaction
- **Resilience**: retry with exponential backoff (3 retries), generous 90 s timeout
- **API-key auth** (preferred) or basic auth

## Prerequisites

1. **pfSense CE 2.8+ / 24.x** with the **REST API package (v2.x)** installed:
   - The package serves the **v2** API at `/api/v2/`. The v2 API requires pfSense 2.8.1+/24.03+; earlier 2.7.x only supports the v1 API.
   - Install: System → Package Manager → search "API", or from the shell:
     `pkg-static add https://github.com/pfrest/pfSense-pkg-RESTAPI/releases/download/v2.10.0/pfSense-2.8.1-pkg-RESTAPI.pkg` (match the exact asset for your pfSense version at implementation time)
   - Enable **KeyAuth** under System → REST API → Settings (fresh installs often have only BasicAuth; without KeyAuth the `X-API-Key` header returns 401)
   - Create an API key: System → REST API → Keys (or `POST /api/v2/auth/key`). The v2 key is a **single opaque string**, e.g. `abc123...` (never commit a real one)
2. **Python ≥ 3.11** with [uv](https://docs.astral.sh/uv/) (or pip)

## Install

```bash
uv sync          # install dependencies
uv run pytest    # run the unit tests (mocked HTTP, no router needed)
```

## Configure

Copy `.env.example` and fill in the values, or export them in your shell:

| Variable | Required | Description |
|---|---|---|
| `PFSENSE_URL` | yes | Base URL of the REST API, e.g. `https://192.168.1.1` |
| `PFSENSE_API_KEY` | yes* | v2 API key — a single opaque string (System → REST API → Keys) |
| `PFSENSE_USERNAME` / `PFSENSE_PASSWORD` | yes* | Basic auth alternative (*either API key or these two*) |
| `PFSENSE_VERIFY_SSL` | no | `true`/`false`; default `false` (self-signed cert) |
| `PFSENSE_READ_ONLY` | no | `true` registers only read tools |
| `PFSENSE_AUDIT_LOG` | no | Path to audit log (JSON lines, secrets redacted) |
| `PFSENSE_TIMEOUT` | no | HTTP timeout seconds (default 90; config writes on slow routers may need more) |
| `PFSENSE_MAX_PAGES` | no | Max pages for paginated lists (default 20) |

## Run

```bash
uv run pfsense-mcp
```

The server speaks MCP over stdio. Point Claude Code / Claude Desktop at it with `mcp.json` (edit the API key first), or test it with the MCP inspector:

```bash
npx @modelcontextprotocol/inspector uv run pfsense-mcp
```

### SSH tunnel (router behind a host)

```bash
ssh -L 8443:192.168.1.1:443 user@host
# then PFSENSE_URL=https://127.0.0.1:8443
```

## Tools (236 across 17 modules)

- **System (28)** — info, version, uptime, hostname (get/update), DNS settings (get/update, server add/remove via PATCH), tunables CRUD, packages (list/install/remove, available), ARP table, system tables, API settings (get/update, errors, version), email notifications, console settings, reboot (confirm), halt (confirm)
- **Interfaces (21)** — list/get, statistics, status, available, create/update/delete, apply, VLANs CRUD, bridges CRUD, interface groups CRUD
- **Firewall (22)** — rules CRUD + apply, flush, sort, states, states size, schedules CRUD + time ranges, virtual IPs CRUD
- **NAT (13)** — port forwards CRUD, one-to-one CRUD, outbound mode get/update, outbound mappings CRUD
- **Aliases (9)** — CRUD + advanced settings, alias entry add/delete
- **DHCP (10)** — leases, reservations CRUD, dhcpd settings get/update, dhcpd options, start/stop/restart
- **DNS (23)** — system DNS, resolver settings, unbound host overrides CRUD + aliases + flush, unbound access lists CRUD + rows, applies, forwarder host overrides CRUD
- **Routing (12)** — gateways CRUD + status + detail, default gateway, static routes CRUD, apply
- **Diagnostics (5)** — ping, traceroute, DNS lookup, WOL, run command (confirm)
- **Services (15)** — list/status, start/stop/restart (confirm), watchdog, sshd settings, syslogd/dpinger control
- **WireGuard (10)** — tunnels CRUD, peers CRUD via the **native v2 endpoints** (`/vpn/wireguard/...`)
- **Certificates (10)** — CAs CRUD, certificates CRUD, CRLs CRUD + revoked certs
- **Users (19)** — users CRUD, groups CRUD, members, privileges, auth servers, LDAP/RADIUS servers
- **NTP (8)** — ntpd settings, time servers CRUD, start/stop/restart
- **IPsec (11)** — phase 1 CRUD + encryption entries, phase 2 CRUD, apply, status
- **OpenVPN (13)** — servers CRUD, clients CRUD, client-specific overrides CRUD, status
- **Status (7)** — CARP, config history, DHCP log, firewall log, system log, log settings

The full per-tool inventory lives in `docs/API_V2_REFERENCE.md` (§8).

> **Dropped tools (no v2 endpoint):** `get_ddns_settings`, `get_dns_forwarder`, `update_dns_forwarder`, `get_upgrade_status`. DDNS/forwarder settings can still be managed via `run_command` if needed.

## Safety model

- **Confirm gates**: `delete_*`, `reboot_system`, `halt_system`, `restart_service`, `install_package`, `delete_package`, `run_command`, etc. refuse to run unless `confirm=True` is passed. The gate also covers operations that are destructive *by omission*: privilege escalation (`add_group_member`, `add_user_privilege`), and `update_api_settings` when disabling the API or enabling read-only (self-lockout).
- **Config backup**: before any destructive call, the current config is fetched (v2 has no `/system/config/` endpoint, so the tools run the read-only shell command `cat /cf/conf/config.xml` via `/api/v2/diagnostics/command_prompt`) and saved to `~/.pfsense-mcp/backups/config-<timestamp>.json`.
- **Read-only mode**: with `PFSENSE_READ_ONLY=true` the write tools are *removed from the server*, not just blocked — the agent cannot even see them.
- **Audit log**: every write-tool call appends one JSON line (`ts`, `tool`, `args` redacted, `ok`, `detail`).

## Security model

- **Transport**: HTTPS to the pfSense REST API. TLS verification is **off by default** (`PFSENSE_VERIFY_SSL=false`) because pfSense ships a self-signed certificate — set it to `true` and trust the router's CA when you can, and always keep the API reachable only over a private network or SSH tunnel.
- **Authentication**: API-key auth (`X-API-Key: <key>`) is preferred over basic auth. The key comes from the environment/config file and is never logged or echoed by the server.
- **Least privilege**: `PFSENSE_READ_ONLY=true` removes every write tool from the server — the agent cannot even see them, let alone call them.
- **Destructive operations**: all `delete_*`, `reboot_system`, `halt_system`, `restart_service`, package install/remove, and `run_command` require `confirm=True` and take a config backup first.
- **Audit trail**: every mutation is appended to the audit log (JSON lines) with secrets redacted; point `PFSENSE_AUDIT_LOG` at a file to enable it. The log is written with `O_NOFOLLOW` and `0600` permissions, and each entry is fsynced before the call returns.
- **Command execution surface**: `run_command` executes arbitrary shell commands on the router (root) — it is gated by `confirm=True` and should be treated as root shell access. `ping`, `traceroute`, and `dns_lookup` build shell commands from their `host` argument; the argument is validated to reject shell metacharacters so a crafted host cannot inject additional commands.
- **Secrets in config**: WireGuard private keys, preshared keys, user passwords, and certificate private keys are written to the router's config.xml (as pfSense itself does) and may appear in tool responses — the audit log redacts known secret fields.

## Known issues / operational notes

- **No offline ISO for 2.8.x**: Netgate moved to an online installer, so a clean 2.8.x install requires either a 2.7.2 ISO + built-in upgrade, or the Netgate Installer (store account). See `docs/PLAN.md` for the upgrade path.
- **v2 item targeting**: v2 lists are targeted by the item's `id` (or a name where the API supports it). DELETE uses `?id=`, PATCH puts `id` in the body, POST creates, plural paths GET lists. No trailing slashes. NAT rules and DHCP reservations use 0-based config indices (`id: 0` is valid — treat it as a real id).
- **Service control by id**: v2 restarts/stops a service by its numeric `id`; the tools resolve the name to an id first.
- **WireGuard native endpoints**: v2 has first-class `/vpn/wireguard/...` endpoints, so the v1 config read-modify-write machinery is gone. The tools apply changes immediately.
- **Bounded diagnostics**: `command_prompt` runs commands synchronously, so the diagnostics tools bound runtime — `traceroute` uses `-n -q 1 -w 2` (no per-hop DNS lookups, one probe per hop, 2 s probe wait), keeping even a fully unresponsive 20-hop trace under ~40 s. `ping` caps each probe at 2 s.

## Development

```
src/pfsense_mcp/
├── server.py        # MCPServer entry point, tool registration
├── config.py        # env config
├── client.py        # REST API v2 HTTP client (httpx, retry/backoff)
├── safety.py        # read-only, confirm gates, backups, audit
└── tools/           # 17 modules, one per pfSense subsystem
```

Run tests: `uv run pytest` (mocked HTTP, no router needed) and
`python scripts/live_integration_test.py` against a real router (see the script header for env setup).

**CI**: `.github/workflows/ci.yml` runs the unit tests on every push/PR
(Python 3.11–3.13). `.github/workflows/live-test.yml` runs the live
integration suite against a real router — manual trigger only, needs a
runner that can reach the router and `PFSENSE_URL` / `PFSENSE_API_KEY`
repository secrets. Dependabot (`.github/dependabot.yml`) opens weekly
update PRs for Python deps and GitHub Actions.

## Test environment

The project is developed against a disposable pfSense CE 2.8.1 lab VM on a
Proxmox host. See `docs/PLAN.md` for the full setup walkthrough (including
the 2.7.2 → 2.8.1 upgrade path) and `docs/API_V2_REFERENCE.md` for the
generated v2 endpoint contract.