title: SEC-KN-P6-VERIFY — MCP host / gateway SESSION_SECRET share verification date: 2026-08-01 step: SEC-KN-P6-VERIFY model: Operator + Auto authority: knowtation posture: read-only evidence session (no posture/env flips; F7 AWS-parked) frozen: true outcome: B — SHARED (secret is common across gateway + MCP host) verdict: VERIFIED-SHARED
SEC-KN-P6-VERIFY — is SESSION_SECRET shared between the Netlify gateway and the persistent MCP host?
Question (from the board)
P6 (PRE-BUILD-SECURITY-AUDIT-FINDINGS-PASS2.md) capped mcp_access role elevation in
resolveHostedActorRole, and noted the exploit path is not mounted on Netlify — so
"exploitability depends on the MCP host sharing SESSION_SECRET." That share has been
UNVERIFIED since 2026-07-26. This evidence-only session resolves it.
Hosts under test (distinct — proven, not assumed)
| Host | DNS / infra | Server header | CORS Allow-Origin on /health |
|---|---|---|---|
| Gateway | api.knowtation.store → CNAME knowtation-gateway.netlify.app (13.52.188.95, 52.52.192.191) |
server: Netlify + x-nf-request-id + cache-status: "Netlify …" |
https://knowtation.store |
| MCP host | mcp.knowtation.store → 18.221.120.124 (EC2) |
Server: nginx/1.24.0 (Ubuntu), HTTP/1.1, Connection: keep-alive |
https://knowtation-gateway.netlify.app |
The two hosts return different CORS Allow-Origin values and different Server
identities for the same /health request. That rules out the one benign alternative to a
shared secret — namely that mcp.knowtation.store blindly reverse-proxies /api/v1/* back
to Netlify. It computes its own CORS, so it is a separate Node instance (nginx → Node),
and its /api/v1/auth/session verifies JWTs against its own local SESSION_SECRET.
Method — signed-token cross-acceptance probe (read-only)
GET /api/v1/auth/session verifies purely by JWT signature
(hub/gateway/server.mjs → decodeVerifiedToken → jwt.verify(token, SESSION_SECRET),
server.mjs:260-265,480-500) with no DB call. So presenting one gateway-signed JWT
to both hosts isolates the signing-key question:
- 200 on the MCP host → it verified the gateway's HS256 signature → secret shared.
- 401 on the MCP host → signature rejected → secret isolated.
Token minted via the Phase C credential exchange
(POST /api/v1/auth/agent/token, credential from ~/.config/knowtation/agent_cred;
never printed). Probe script: scripts/archive/2026-08-01-p6-cross-acceptance-probe.mjs.
Evidence (this session, 2026-08-01 ~13:59 UTC)
health gateway status=200 body={"ok":true}
health mcp-host status=200 body={"ok":true}
exchange OK type=agent_access scopes=["propose","vault:read"] exp_in_s=900
control-A gateway/session status=200 (token accepted by its issuer — token is valid)
PROBE mcp/session status=200 (SAME gateway-signed token accepted by the MCP host)
control-B mcp/session garbage-token status=401 (route is actually verifying, not blindly 200)
control-C mcp/session no-auth status=401 (route mounted; not a 404)
Control A (200 on issuer) proves the token is validly signed; control B/C (401) prove the MCP host's session route is mounted and genuinely checking signatures. Against that, the PROBE returning 200 is decisive.
Verdict — Outcome B: SHARED → VERIFIED-SHARED
The persistent MCP host (mcp.knowtation.store, EC2) verifies JWTs signed with the same
SESSION_SECRET as the Netlify gateway. HS256 verification succeeds iff the secret is
identical, and the two hosts are proven to be distinct instances. Therefore the secret is
shared.
Per the frozen instruction, no fix is improvised in this session. Rotating/splitting the
secret is Tier 3 (secrets) and requires a SEC-KN Thinking freeze first — see NEXT.
Why this matters (risk, stated honestly)
- A shared JWT signing domain across gateway + bridge is partly by design — the gateway
README (§Post-deploy verification #3) requires the bridge to share
SESSION_SECRETso JWTs verify across/api/v1/vault/*. The new fact is that the MCP host is in that same signing domain. - The specific P6 hazard is that the MCP host mounts the full stateful
/mcprouter andSEC-KN-3(resolveHostedActorRolecappingmcp_accessrole by scopes, never the admin allowlist) is recorded DONE but not merged tomain(docs/ROADMAP.mdSEC-KN-3 row). If the code running on the EC2 MCP host predates SEC-KN-3, anmcp_accesstoken minted for an adminsub— now verifiable there because the secret is shared — could still resolverole: admin. UNVERIFIED and intentionally not probed here, because confirming it would require an active elevation attempt (mint admin-submcp_accesstoken, attempt approve/discard), which exceeds this read-only mandate. This is the top input to the freeze.
Optional sidecar — live proposal created_by populated (CONFIRMED)
Authenticated read of GET /api/v1/proposals?limit=5 (agent token, X-Vault-Id: default):
64 proposals returned; 7 most-recent carry a non-empty server-derived created_by
(google:…, len 28), including prop-1785500300353491755 (the 2026-07-31
FLOW-CAPTURE-LIVE-SMOKE proposal). The 57 older proposals have an empty created_by,
consistent with the field being populated only for proposals created after the SEC-KN-4
server-only binding deployed. This closes the last UNVERIFIED created_by row:
the binding is live and populating on new proposals.
Probe script: scripts/archive/2026-08-01-created-by-probe.mjs.
Hard-stops honored
Read-only probes only. No posture/env flip. No secret printed (tokens/subs redacted). No
dfx deploy. No secret rotation. F7 AWS-parked. No GitHub PR opened.