AID-H-028.001: MCP Server Authenticity Validation & Connection Pinning
server/discover or any MCP request is sent, verify remote server identity beyond endpoint discovery. Enforce an allowlisted HTTPS origin or approved local stdio command, transport identity verification, and risk-based SPKI, certificate, or SPIFFE pinning with fail-closed mismatch behavior.Pillars: infra, app
Lifecycle: building, operation
Scope boundary
Owns the client's pre-request trust decision for the selected remote server or local stdio command, including origin allowlisting, transport authentication, and risk-based certificate, SPKI, or workload-identity pinning. It creates no MCP protocol session.
AID-H-024: AID-H-028.001 authenticates the transport endpoint selected for connection; AID-H-024 resolves the intended logical server and tool contract before transport trust is evaluated. A certificate-valid endpoint can still be the wrong resolved service.
AID-H-034.001: AID-H-028.001 enforces the client's server-authentication decision; AID-H-034.001 configures the server's own transport and exposure baseline. Client pinning and server hardening are complementary endpoints of the connection.
Implementation guidance
- AID-H-028.001-G001: Implement per-server allowlisting and optional SPKI or certificate-fingerprint pinning for configured MCP endpoints, with fail-closed behavior on identity mismatch.
- AID-H-028.001-G002: Validate server URI, expected identity, and minimum transport properties before discovery or request dispatch, and reject unknown or downgraded connections.
- AID-H-028.001-G003: Monitor certificate transparency for public-facing MCP domains and alert when unexpected certificates are issued.
AID-H-028.001-G001
Concept:
Store a small trust record for each allowed MCP server. At minimum include the server URL, the expected hostname, and either the expected certificate fingerprint or an SPKI hash. When the client connects, compare what the server presents to what you already trust. If it does not match, stop the connection and log the event.
Example allowlist file
{
"servers": [
{
"name": "finance-mcp",
"url": "https://mcp.finance.internal:8443",
"hostname": "mcp.finance.internal",
"spki_sha256": "ET4Zw38AtZHETHLH25DNB6EDZsy0oi+LOhGWDlXXbkQ=",
"min_tls_version": "TLSv1.2"
}
]
}
Example Node.js verification hook
// File: src/mcp/pinned-agent.mjs
import https from "node:https";
import tls from "node:tls";
import crypto from "node:crypto";
import fs from "node:fs";
const cfg = JSON.parse(fs.readFileSync("./mcp_allowlist.json", "utf8"));
const allowed = new Map(cfg.servers.map(s => [s.url, s]));
function spkiHash(cert) {
const x509 = new crypto.X509Certificate(cert.raw);
const spkiDer = x509.publicKey.export({ type: "spki", format: "der" });
return crypto.createHash("sha256").update(spkiDer).digest("base64");
}
function connectPinned(serverUrl) {
const rule = allowed.get(serverUrl);
if (!rule) throw new Error(`Server not allowlisted: ${serverUrl}`);
const agent = new https.Agent({
minVersion: rule.min_tls_version,
checkServerIdentity: (host, cert) => {
const tlsErr = tls.checkServerIdentity(host, cert);
if (tlsErr) return tlsErr;
const got = spkiHash(cert);
if (got !== rule.spki_sha256) {
return new Error(`SPKI pin mismatch for ${host}`);
}
return undefined;
}
});
return agent;
}
Operational notes: Keep a rotation runbook. During planned certificate rotation, update the pin in a reviewed config change before the new certificate goes live. Keep a short emergency allowlist override path, but require approval and log every use.
Production implementation
At the client connector before DNS/TLS use, normalize the HTTPS origin, match it to the signed server registry, verify the live certificate chain and policy-selected SPKI or certificate pin, and deny unknown, stale or mismatched identity without fallback.
Independent verification
A separately credentialed verifier independently reads back or replays registry selection, DNS result, TLS chain, pin, effective connection and deny receipt for every connection attempt, including wrong-origin, pin-rotation and interception fixtures.
AID-H-028.001-G002
Concept:
Do not let the client connect to arbitrary MCP URLs. Validate the requested endpoint against a local allowlist and enforce basic transport rules such as minimum TLS version and expected subject or SAN hostname.
Example Python pre-connection gate
from urllib.parse import urlparse
ALLOWED = {
"mcp.finance.internal": {
"schemes": {"https"},
"ports": {443, 8443},
"min_tls": "TLSv1.2"
}
}
def validate_endpoint(url: str):
parsed = urlparse(url)
if parsed.hostname not in ALLOWED:
raise ValueError(f"Unknown MCP server: {parsed.hostname}")
rule = ALLOWED[parsed.hostname]
if parsed.scheme not in rule["schemes"]:
raise ValueError("Disallowed scheme")
if parsed.port and parsed.port not in rule["ports"]:
raise ValueError("Disallowed port")
return True
Operational notes: Log every deny with server_url, client_version, user_id, and reason. Review repeated denials because they may indicate typosquatting, phishing, or config drift.
Production implementation
At the transport factory, parse one canonical URI, require policy-approved HTTPS/TLS and hostname, prohibit credential-bearing URLs and unapproved redirects, bind the negotiated server identity and protocol profile, and stop dispatch on downgrade or ambiguity.
Independent verification
A separately credentialed verifier independently reads back or replays the effective socket peer, redirect chain, negotiated TLS/profile and dispatcher decision with malformed URI, HTTP, alternate-port, redirect and downgrade fixtures.
AID-H-028.001-G003
Concept:
This is an advanced control for public or internet-facing MCP domains. It is usually not needed for private internal names that do not use public certificate issuance.
Example daily CT check using Cert Spotter API
import json
import math
import os
import requests
API_TOKEN = os.environ["CERTSPOTTER_TOKEN"]
DOMAIN = os.environ["VERIFIED_MCP_CT_DOMAIN"]
EXPECTED_ISSUERS = set(json.loads(os.environ["VERIFIED_MCP_CT_ISSUERS_JSON"]))
CT_HTTP_TIMEOUT_SECONDS = float(os.environ["VERIFIED_CT_HTTP_TIMEOUT_SECONDS"])
CT_RESPONSE_MAX_BYTES = int(os.environ["VERIFIED_CT_RESPONSE_MAX_BYTES"])
if (
not DOMAIN or not EXPECTED_ISSUERS
or not math.isfinite(CT_HTTP_TIMEOUT_SECONDS)
or CT_HTTP_TIMEOUT_SECONDS <= 0
or CT_RESPONSE_MAX_BYTES <= 0
):
raise RuntimeError("verified CT policy inputs are incomplete or invalid")
with requests.get(
"https://api.certspotter.com/v1/issuances",
params={"domain": DOMAIN, "include_subdomains": "true", "expand": "dns_names"},
headers={"Authorization": f"Bearer {API_TOKEN}", "Accept": "application/json"},
timeout=CT_HTTP_TIMEOUT_SECONDS,
allow_redirects=False,
stream=True,
) as response:
response.raise_for_status()
if response.url != "https://api.certspotter.com/v1/issuances?domain=" + requests.utils.quote(DOMAIN, safe="") + "&include_subdomains=true&expand=dns_names":
raise RuntimeError("Cert Spotter request authority or query differs")
raw = bytearray()
for chunk in response.iter_content(chunk_size=min(65536, CT_RESPONSE_MAX_BYTES + 1)):
if chunk:
raw.extend(chunk)
if len(raw) > CT_RESPONSE_MAX_BYTES:
raise RuntimeError("Cert Spotter response exceeds signed decoded-byte cap")
certificates = json.loads(bytes(raw))
if not isinstance(certificates, list):
raise RuntimeError("Cert Spotter response root must be an array")
for cert in certificates:
issuer = cert.get("issuer", "")
if issuer and not any(ok in issuer for ok in EXPECTED_ISSUERS):
print(f"ALERT unexpected issuer for {DOMAIN}: {issuer}")
Operational notes: Use this as a monitoring signal, not as the only connection control. The primary enforcement should still be allowlisting and identity pinning inside the client.
Production implementation
At the CT monitoring job, query the policy-approved log source with a policy-derived timeout, reconcile all observed issuances against the authorized DNS names, issuers and key identities, and create a signed quarantine/rotation alert for every unexpected certificate.
Independent verification
A separately credentialed verifier independently reads back or replays the complete CT result population, issuer/key comparison, alert delivery and client-registry quarantine readback with expected, unexpected, partial-log and API-failure fixtures.
Open-source tools
- OpenSSL
- BoringSSL
- SPIRE
- Cert Spotter (open-source certificate-transparency monitor)
- Python ssl (connection-level identity verification)
- Node.js tls (connection-level identity verification)
- Go crypto/tls (connection-level identity verification)
- step-ca
Source-available / open-weight tools
- HashiCorp Vault Community (BUSL-1.1; source-available)
Commercial / hosted tools
- Palo Alto Networks Next-Generation Trust Security (certificate lifecycle management)
- DigiCert Trust Lifecycle Manager
- AWS Private Certificate Authority
- Google Cloud Certificate Authority Service
- Azure Key Vault Certificates
Threat mappings
MITRE ATLAS
- AML.T0010 AI Supply Chain Compromise
- AML.T0073 Impersonation (server identity spoofing)
- AML.T0078 Drive-by Compromise (preventing connection to malicious servers)
- AML.T0010.005 AI Supply Chain Compromise: AI Agent Tool
MAESTRO
- Agent Identity Attack (L7) (server identity spoofing)
- Compromised Agent Registry (L7) (redirected client connections)
OWASP LLM Top 10 2026
- LLM01:2026 Prompt Injection
OWASP ML Top 10 2023
- N/A
OWASP Top 10 for Agentic Applications 2026
- ASI04:2026 Agentic Supply Chain Vulnerabilities (server spoofing in the agent supply chain)
- ASI07:2026 Insecure Inter-Agent Communication (MitM or impersonation on client-server channel)
NIST Adversarial Machine Learning 2025
- N/A
Cisco Integrated AI Security and Safety Framework
- AISubtech-14.1.2 Insufficient Access Controls
- AISubtech-9.3.1 Malicious Package / Tool Injection
- AISubtech-9.3.2 Dependency Name Squatting (Tools / Servers)
- AITech-16.1 Eavesdropping
- AITech-4.3 Protocol Manipulation
- AITech-9.3 Dependency / Plugin Compromise
- AISubtech-4.3.3 Server Rebinding Attack (origin and transport-identity pinning constrain endpoint substitution; not universal rebinding prevention)
Google Secure AI Framework 2.0 - Risks
- IIC: Insecure Integrated Component (server authenticity verification secures the MCP integration point)
Databricks AI Security Framework 3.0
- Agents - Tools MCP Client 13.26: Malicious Server Connection
- Agents - Tools MCP Client 13.29: Insufficient Server Validation
- Agents - Tools MCP Client 13.33: Insecure Communication Handling
- Agents - Tools MCP Server 13.25: Insecure Communication