Skip to content

Security model

Trust boundary

This server is intended for local stdio MCP use. The process that launches and controls the MCP channel is the caller-authentication boundary. The server is not a multi-tenant network service and does not authenticate individual MCP messages.

The configured PFSENSE_IDENTITY identifies the shared upstream pfSense API credential. It does not identify an MCP caller. Audit records call this field upstream_identity for that reason.

Data classes

Credentials and secrets

Passwords, pre-shared keys, private keys, API-key plaintext, stored credential hashes, and symmetric authentication keys must never appear in public models, MCP schemas, serialized tool output, logs, errors, or approved fixtures. Upstream values in these fields are ignored unconditionally.

Optional sensitive metadata

IP/MAC addresses, internal hostnames, persistent device identifiers, firewall endpoints, account attribution, internal service-account names, email addresses, and public SSH authorized keys are omitted by default. Tools may expose them through include_identifying_metadata=True. Audit records capture only whether this disclosure was requested, never values.

Public cryptographic material

Public certificates and certificate signing requests may be returned by their explicit inventory tool. Private keys and passphrases are never returned. Public SSH keys are treated as optional sensitive metadata because they identify accounts and hosts.

Ordinary operational data

Service state, resource counters, versions, non-secret policy flags, timeouts, and similar operational information are returned normally under the active READ capability profile.

Authorization

The default auditor profile exposes the accepted READ capability set and grants zero WRITE capabilities. The engineer profile also has zero capabilities. Under these defaults no WRITE tool is registered and the production bootstrap does not construct a write client — the public MCP contract remains exactly the accepted READ-only tool set (42 tools, 0 WRITE) unless an operator explicitly opts in (see below).

As of 2026-08-16, one WRITE endpoint is allow-listed — FIREWALL_ALIAS_DESCRIPTION (PATCH on a single alias's descr field only) — and its verified flag is True, following independent live evidence (ADR-026). Neither fact by itself exposes anything: reaching this tool still requires every one of W3 Slice 4's three independent conditions simultaneously — (1) PFSENSE_PROFILE=write_protected explicitly selected (never the default), (2) the endpoint's allow-list entry (present, but inert without the other two), and (3) a successfully constructed production runtime, which itself requires the full Tier1 security material (pinned authorities, TPM witness connectivity, a provisioned RecoveryContract store) to be configured — absent any one of these three, the tool is not registered at all. Even once registered, every actual mutation still passes through the full authorization → one-time-consumption → RecoveryContract → confirmation → MutationExecutor ceremony described in the Tier1 specs; verified=True only removed one additional, independent refusal inside WriteApiClient that existed on top of that ceremony — it does not replace or shortcut any part of it.

PFSENSE_ALLOWED_TOOLS is an optional exact-name restriction applied as an intersection after profile authorization. An absent value preserves the profile; an explicitly empty value registers nothing. Unknown names and wildcard forms fail closed. It can remove exposure but cannot grant a capability, bypass endpoint verification, or activate WRITE.

All current tools publish MCP readOnlyHint=true and openWorldHint=true. These values help clients describe the tool and recognize that its data comes from a dynamic external appliance. MCP ToolAnnotations are untrusted hints, not security decisions. Server-side capability profiles, exact-name restriction, GET-only enforcement, endpoint verification, credential policy, audit logging, and independent WRITE-inactivity checks remain authoritative.

Credentials and transport

The pfSense API key is loaded from a configured local file and sent only in the X-API-Key header. HTTPS is mandatory. Strict system trust is the default; an explicit CA file can be used for an internal CA. TLS verification can be disabled only by explicit startup configuration.

HTTP redirects are not followed and every non-2xx status is rejected. Timeout, connection, TLS, protocol, and other HTTP transport failures are normalized to sanitized project exceptions without propagating upstream exception messages or response bodies. Response/decompression size limits remain a future policy requiring representative non-production evidence; no arbitrary bound is guessed for this release.

Linux is the supported production platform for credential loading. The key file is opened read-only with O_NOFOLLOW, then its type, effective-user ownership, permissions, and size are validated with fstat() before a bounded first line is read from that same descriptor. The descriptor is closed on every path. This binds validation and reading to one inode and prevents path replacement from substituting a different file. Platforms without the required safe-open primitive are rejected with a clear configuration error; they do not silently use weaker path-based validation.

Configuration values that can enter URLs, registration policy, diagnostics, or logs reject ASCII, percent-encoded, and Unicode control/format/line-separator characters. This keeps each audit/log event structurally single-line.

Audit data

Tool audit records contain the tool name, upstream identity, duration, outcome, whether optional sensitive metadata is supported/requested, and a sanitized exception class on failure. They never contain arguments, responses, exception messages, credential values, or raw pfSense bodies.

Recovery and WRITE status

Historical note: this section originally (v0.3.0) described Tier 0/1 WRITE infrastructure as entirely inert, listing a series of prerequisites ("blocked on an external encryption/key provider, a durable monotonic anti-rollback strategy, operator-confirmation authentication...") that production activation depended on. As of 2026-08-16, every one of those prerequisites has been built and independently live-verified; the section below describes the current state.

pfsense_mcp.tier1 implements the full first-WRITE product surface for exactly one operation, set_firewall_alias_description_v1 (FIREWALL_ALIAS_DESCRIPTION, PATCH on a single alias's descr field, apply=false). Every mutation this package can ever perform passes through, in order: a fresh authoritative preparation (re-reads the target's live state), a security-posture plan with a deterministic, independently-recomputable digest, an off-host-signed PlanAuthorizationV2 (never signed by the same process that would execute it), atomic one-time authorization consumption (a dedicated, MAC-authenticated store distinct from the contract store — see ADR-023), an immutable, encrypted RecoveryContract in a closed state machine, an off-host-signed ConfirmationEvidence bound to the exact same contract/intent digests, and finally MutationExecutor — the only code path in this repository that ever sends a mutating HTTP request to pfSense.

Anti-rollback anchor: a dedicated TPM host-witness daemon (witness_daemon/, a separate process on the Proxmox host, never imported by production bootstrap) provides a monotonic hardware counter that advances exactly once per successfully verified mutation and is checked for evidence-state consistency (ADR-022) before every authorization is considered fresh. A software-only anchor posture is also modeled (AnchorAssurance.SOFTWARE) but has no implemented backend in this codebase — ADR-026's accepted candidate targets hardware_witness specifically. Production activation for a given deployment requires this witness to be reachable and its evidence-state resolved to PROVISIONED_VERIFIED; without it, security-posture discovery reports AnchorAssurance.NONE/UNKNOWN and the plan is not safe_to_proceed.

Off-host signing: both signatures (sign-authorization, sign-confirmation) are produced by a separate CLI (signing/), run on a host that holds neither pfSense credentials nor production runtime code, and which independently re-derives the plan/intent digests it signs rather than trusting a caller-supplied copy (ADR-024's anti-tautology property). Optionally, a second, deliberately read-only witness identity may be provisioned for this signer (distinct pfSense-witness client certificate, explicitly excluded from the daemon's /anchor/advance allow-list) so the signer can independently verify witness evidence state without ever holding advance authority.

pfSense credential scoping: the LAB acceptance evidence for this capability was gathered using a dedicated, least-privilege local pfSense API identity holding exactly the four privileges the production path needs (GET aliases, PATCH the one alias endpoint, GET system status, conditionally GET HA-sync status) — not the administrative account. This is a deployment-time credential-provisioning choice, not a code-level guarantee this repository enforces on every deployment; see reports-ai for the exact provisioning procedure used.

Production runtime construction remains the gate that matters most in practice: build_production_runtime() requires the full Tier1 security material (pinned Ed25519 authorities, a provisioned encrypted store, live witness connectivity) to succeed at all — a deployment missing any of these does not get a partially-working WRITE path, it gets None back from that constructor and the tool is never registered, independent of profile selection or verified.