Security reference · TLS · Certificate pinning
MCP server TLS certificate pinning security
Standard TLS validation verifies that a server's certificate was signed by a CA in the trust store — but there are hundreds of trusted CAs, any of which could be compromised or coerced. An MCP server calling a payment API or credentials service trusts whatever certificate a MITM attacker presents if they have a valid CA signature. Certificate pinning checks the specific public key of the expected endpoint rather than trusting the entire CA hierarchy. This reference covers when to pin, how to implement SPKI pinning in Node.js, and how to rotate pins without downtime.
When certificate pinning matters for MCP servers
Certificate pinning is not necessary for every outbound connection. It adds operational complexity (pin rotation when the remote server rotates its key) and should be applied selectively to connections where MITM would be catastrophic:
| Connection type | Pin? | Reason |
|---|---|---|
| Payment processing API (Stripe, Adyen) | Yes | MITM = credential theft + financial fraud |
| Anthropic API / Claude API | Yes | MITM = prompt content exposure, credential theft |
| Internal secrets service (Vault, AWS Secrets Manager) | Yes | MITM = credential exfiltration |
| OAuth token endpoint | Yes | MITM = auth code/token theft |
| General web fetch tools (arbitrary URLs) | No | Cannot pin arbitrary endpoints |
| Third-party APIs (analytics, CRM) | Consider | Depends on sensitivity of data sent |
Corporate proxy environments: Many enterprise deployments run a TLS-intercepting proxy that issues its own CA-signed certificates for all HTTPS traffic. Certificate pinning will break in these environments unless the proxy's CA is added to the pin set. This is an acceptable trade-off for the highest-sensitivity connections — the deployment team must explicitly configure the proxy CA as a backup pin.
SPKI fingerprint pinning in Node.js
The recommended approach is Subject Public Key Info (SPKI) pinning — pinning the hash of the public key rather than the full certificate. Public keys remain stable across certificate renewals (assuming the same key pair is reused), while the certificate itself changes on every renewal. SPKI pins survive certificate rotations as long as the underlying key pair is unchanged.
import https from 'node:https';
import crypto from 'node:crypto';
// Extract SPKI fingerprint from a certificate's public key
// Run this against the target server to get the expected pin:
// openssl s_client -connect api.anthropic.com:443 | openssl x509 -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64
const PINS = {
'api.anthropic.com': [
'sha256//aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789abcde=', // primary
'sha256//backup-pin-for-key-rotation-scenario-xyz123=', // backup (next key)
],
'api.stripe.com': [
'sha256//stripes-primary-spki-pin-here=',
'sha256//stripes-backup-spki-pin-here=',
],
};
function createPinnedAgent(hostname) {
const expectedPins = PINS[hostname];
if (!expectedPins) return new https.Agent(); // no pinning for unpinned hosts
return new https.Agent({
checkServerIdentity(host, cert) {
// First, run the default hostname verification
const err = https.checkServerIdentity(host, cert);
if (err) return err;
// Then check the SPKI pin
const publicKey = cert.pubkey; // DER-encoded public key
const fingerprint = 'sha256//' + crypto
.createHash('sha256')
.update(publicKey)
.digest('base64');
const pinMatches = expectedPins.some(pin => pin === fingerprint);
if (!pinMatches) {
const err = new Error('CERT_PIN_MISMATCH: ' + host);
err.code = 'CERT_PIN_MISMATCH';
return err;
}
// Returning undefined = certificate accepted
},
});
}
// Use in fetch calls via a custom dispatcher (Node 18+ undici)
import { Agent as UndiciAgent } from 'undici';
function createPinnedDispatcher(hostname, expectedPins) {
return new UndiciAgent({
connect: {
checkServerIdentity(host, cert) {
const err = https.checkServerIdentity(host, cert);
if (err) throw err;
const fingerprint = 'sha256//' + crypto
.createHash('sha256')
.update(cert.pubkey)
.digest('base64');
if (!expectedPins.includes(fingerprint)) {
const pinErr = new Error('CERT_PIN_MISMATCH: expected pin not found for ' + host);
pinErr.code = 'CERT_PIN_MISMATCH';
throw pinErr;
}
},
},
});
}
Extracting pins from live endpoints
#!/bin/bash
# Extract SPKI SHA-256 pin from a live endpoint
# Usage: ./get-pin.sh api.anthropic.com 443
HOST=$1
PORT=${2:-443}
openssl s_client -connect "$HOST:$PORT" -servername "$HOST" 2>/dev/null \
| openssl x509 -pubkey -noout \
| openssl pkey -pubin -outform der \
| openssl dgst -sha256 -binary \
| base64
Run this against the production endpoint and the backup/recovery endpoint (if the service publishes one). Store both pins in your configuration. When the service rotates its certificate, if they reuse the same key pair, your pin remains valid. If they rotate the key pair, you must update the pin during the overlap window when both the old and new key pairs are active.
Pin rotation without downtime
The highest risk in certificate pinning is pin expiry: if the remote server rotates their key pair and your code only knows the old pin, all connections fail until you deploy new code. The mitigation is the backup pin pattern:
- Always store at least two pins — the current pin and the next expected pin (if the service publishes it) or an intermediate CA pin as a fallback.
- Pin at the intermediate CA level for services that rotate keys frequently. The intermediate CA changes less often than leaf certificates.
- Monitor for pin mismatches in production — log CERT_PIN_MISMATCH errors before they become outages. If you see mismatch logs, the remote server has rotated their key and you need to update the pin in the next deploy.
// Configuration-driven pins — allows pin updates without code changes
const PIN_CONFIG_PATH = process.env.TLS_PIN_CONFIG || './config/tls-pins.json';
async function loadPins() {
const config = JSON.parse(await fs.readFile(PIN_CONFIG_PATH, 'utf-8'));
return config.pins; // { 'api.anthropic.com': ['sha256//...', 'sha256//...'] }
}
// In production: store pin config in your secrets manager
// Rotate by updating the secret, no code redeploy required
SkillAudit findings: TLS certificate pinning
Run a SkillAudit scan to detect TLS misconfigurations, disabled certificate validation, and missing pins for high-value API connections in your MCP server.