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.