Skip to content

Security Model

xNetVN Development edited this page Nov 12, 2026 · 1 revision

Security Model

A key delegates an existing WordPress identity and may only reduce what that identity can do. The MCP route is private, the management API is separate, and every tool call is re-evaluated against live permissions, the key scope and site policy (docs/en/adr/0009-origin-and-auth-route.md, docs/en/adr/0005-capability-intersection.md).

Two authentication paths, never interchangeable

Surface Route Accepted identity Rejected identity
MCP /xnetvn-agent-hub-wp/mcp only Bearer key in an approved header Cookie, Application Password, query-string or body credential
Management /xnetvn-agent-hub-wp/v1/* Logged-in auth cookie plus wp_rest nonce Bearer key, Application Password
STDIO Local adapter process Explicitly selected WordPress user No HTTP path exists

The MCP route matcher compares the normalized route exactly and only inside a real REST request; a namespace prefix or a different route is not authenticated (src/Http/RouteMatcher.php). On that route the bearer header is the only credential source; the fallback header is configurable and enabled by default (src/Auth/HeaderExtractor.php). Cookie identity is not a substitute: the authentication filter replaces the resolved user with the key owner and re-runs late enough to override an early-cached cookie user while preserving earlier guard errors (src/Auth/Authenticator.php).

Management calls require a real logged_in auth cookie for the current user and a valid wp_rest nonce. A present but malformed nonce header is authoritative and does not fall back to a query parameter (src/Rest/ManagementGate.php). Every management controller inherits this gate, so no route can skip it (src/Rest/ManagementController.php).

A successful MCP authentication creates a request-local AuthContext binding the key metadata, the owner ID and a random request identifier. The context never contains the token, hash or pepper (src/Domain/AuthContext.php, src/Http/RequestScope.php). HTTP policy evaluation denies a missing context; the registry additionally requires the current WordPress user to equal the context owner (src/Policy/Policy.php, src/Abilities/Registry.php). STDIO is the only transport where a missing context is allowed.

Controls in the request path

Live scope intersection. Every term must pass: site enabled, site tier ceiling, admin-tier switch, key tier, enabled toolset, allow/deny lists, third-party approval and the owner's current visibility capability (src/Policy/Policy.php). The scope cannot raise the owner's authority, and a scope update can only narrow the previous one (src/Domain/Scope.php). Tier extensions can only raise the effective risk, and additional policy filters can only deny; neither can grant authority (src/Policy/TierResolver.php). Role changes therefore apply on the next request without reissuing the key.

Per-request owner recheck. Authentication verifies that the key exists, is not revoked, is not expired, belongs to an existing user and that the owner still holds use_xnetvn_agent_hub_wp. It also applies key and site IP allowlists and the site enabled switch before issuing a context (src/Auth/Authenticator.php). Deleting the owner revokes that owner's keys.

HMAC storage with a separate pepper. Only an HMAC-SHA256 digest of the secret is stored, keyed by a separate random 32-byte per-site pepper kept in a non-autoloaded option (src/Auth/KeyHasher.php, src/Auth/PepperStore.php, docs/en/adr/0004-key-model.md). Verification uses a timing-safe comparison. Secret and digest are held outside exportable object properties and are redacted from JSON (src/Auth/StoredKey.php, src/Auth/Pepper.php). Losing the pepper invalidates every key on that site; it never reveals one.

Rate limits and bounded work. A weighted sliding-minute window applies 120 requests per key and 10 failed authentications per IP per minute by default (src/RateLimit/SlidingWindowLimiter.php, src/Persistence/WpSiteSettings.php). A 429 response carries Retry-After (src/Auth/Authenticator.php). Password reauthentication for high-risk key operations has its own 5-per-minute budget, and costly bulk or upload operations use at most two in-flight slots per key (src/RateLimit/WpExpensiveBudget.php).

Origin and TLS checks before credentials. The exact route is guarded before authentication. A missing Origin is accepted, which supports non-browser clients; an explicit null, malformed origin, origin with a path, credentials, query or fragment, or an origin outside the exact allowlist is refused. The site's home and site URL origins are allowed implicitly (src/Http/RouteGate.php, src/Http/OriginGuard.php). TLS must be real, or asserted by X-Forwarded-Proto: https from a peer inside a configured trusted-proxy CIDR. Arbitrary forwarded headers are ignored (src/Http/TransportGuard.php).

Safe responses and audit. MCP responses carry Cache-Control: no-store and X-Content-Type-Options: nosniff; a 401 carries a Bearer challenge and a 429 carries retry guidance (src/Auth/Authenticator.php). The audit writer stores identity, outcome, timing, transport, IP, truncated user agent and top-level argument names only. It never stores argument values, tokens, passwords, hashes or cookies (src/Audit/AuditLogger.php, src/Audit/AuditEvent.php, ADR-0010).

Fail-closed adapter boundary. Only Mcp/ServerGateway depends on adapter classes. The server is created only for the official adapter version range >=0.7.0 <0.8.0; a missing or incompatible adapter leaves the plugin loadable but closes MCP and shows an administrative notice (src/Bootstrap/Requirements.php, src/Mcp/ServerGateway.php, ADR-0003). Third-party abilities are denied unless explicitly approved, and abilities with missing or malformed annotations default to destructive (src/Policy/TierResolver.php). The output pipeline revalidates the schema, redacts sensitive field names, bounds bytes and fails safely (src/Abilities/OutputPipeline.php).

Threat table

Threat Primary control
Stolen bearer key replayed from another network Immediate revocation, expiry, IP allowlists, per-key limit, last-used metadata and unusual-network notice, audit trail
CSRF or cookie-session abuse against MCP Exact-route bearer-only authentication; cookies and Application Passwords rejected; Origin guard
DNS rebinding or hostile browser origin Exact normalized Origin allowlist; a missing Origin is accepted; null denied
Agent acts beyond its owner's authority, including after a role downgrade Live capability intersection; no snapshots; owner existence and use capability rechecked every request; object-level core permissions still run
Credential database or backup leak HMAC-SHA256 with a separate per-site pepper; no plaintext token; hashes hidden from exports and logs
Password or key brute force Timing-safe verification; uniform 401 messages; 10 failed authentications per IP per minute
Logs become a secondary secret store Top-level argument names only; sensitive-name filter; no headers or bodies; optional IP masking; bounded retention
Compromised or incompatible adapter Version gate; single facade; MCP closed when the adapter is absent or outside range
Malicious third-party ability Private registry; explicit name approval; unknown annotations default destructive; output pipeline
SSRF through remote media import Off by default; public HTTPS only; DNS/address and redirect policy; byte, pixel, MIME and time limits
Transport downgrade or proxy spoofing HTTPS required; proxy header accepted only from a trusted CIDR; local exemption limited to local/development

Invariants and acceptance laws

AGENTS.md lists the repository's security invariants: private abilities only; exact MCP route matching; bearer only on MCP and cookie plus nonce for management; an AuthContext required for HTTP; scope narrows current capabilities; per-request owner recheck; HMAC with a separate pepper; no credentials, hashes or argument values in logs; only Mcp/ServerGateway may depend on adapter classes; every ability has tier, toolset, visibility capability, annotations and a strict schema; and all output passes the output pipeline.

AGENTS.md also requires every relevant original-plan law L-01 through L-20 to be verified before acceptance. The tracked documentation does not reproduce the full list. The one law with an explicit local definition is L-01: registration arguments never set public or show_in_rest, and effective exposure remains false (docs/en/architecture/adapter-notes.md). Treat this page as the control narrative, not as a substitute for the L-01–L-20 acceptance evidence.

See Key-Management for credential lifecycle, Audit-and-Logging for evidence handling, and Troubleshooting-and-FAQ for failure diagnosis.

Clone this wiki locally