Topic: mcp server tls certificate security
MCP server TLS certificate security — certificate validation, pinning, HSTS, and self-signed cert risks in outbound connections
An MCP server holds ambient credentials for the duration of a session: database passwords, third-party API keys, OAuth bearer tokens. Every outbound HTTPS call those credentials make is a potential interception point. A server that disables certificate validation or accepts self-signed certs turns every network path between the server and its upstreams into an attacker-accessible credential stream. TLS security is not a devops concern that comes after launch — it is a core security property of any MCP server that touches real credentials.
The TLS validation bypass pattern
The most common TLS finding in SkillAudit reports is a deliberately disabled certificate check. In Node.js this appears in three forms, all of which disable the same protection:
// Form 1 — process-wide: disables validation for ALL outbound TLS
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
// Form 2 — per-request via https agent
import https from 'https';
const agent = new https.Agent({ rejectUnauthorized: false });
fetch(apiUrl, { agent });
// Form 3 — axios/got/node-fetch equivalents
axios.get(url, { httpsAgent: new https.Agent({ rejectUnauthorized: false }) });
All three were added "temporarily" during development to work around a self-signed internal CA and never removed. The fix for all three is to remove the flag and address the underlying certificate problem directly — either install the correct CA certificate, use a publicly trusted certificate, or implement proper certificate pinning for internal services.
Self-signed certificate risks
Self-signed certificates are not inherently insecure — an internal service using a self-signed cert can be secured correctly by installing the CA certificate in the trust store. What is insecure is bypassing validation entirely because configuring the CA is "too much trouble."
// Correct: install the internal CA certificate — no bypass needed
import https from 'https';
import fs from 'fs';
const agent = new https.Agent({
ca: fs.readFileSync('/etc/ssl/certs/internal-ca.pem')
// rejectUnauthorized defaults to true — do NOT set to false
});
// All connections to internal services using this CA are fully validated
const resp = await fetch('https://internal-service/api', { agent });
For MCP servers deployed to cloud environments, use publicly trusted certificates from Let's Encrypt or a cloud CA for all outbound endpoints. The only legitimate reason to handle a self-signed CA is connecting to an on-premises service that you control and cannot expose to a public CA. Even then, install the CA cert — never bypass validation.
Hostname verification bypass
Separate from certificate validity, hostname verification confirms that the certificate presented is for the domain you intended to connect to — not a valid certificate for a different domain obtained by a MITM attacker. In Node.js, hostname verification is part of rejectUnauthorized — disabling that flag disables both. Some lower-level TLS libraries allow them to be disabled independently; disabling hostname verification while leaving signature verification enabled is equally dangerous and is flagged as a HIGH finding.
// HIGH finding — hostname check disabled
const tlsSocket = tls.connect({
host: 'internal-service',
port: 443,
checkServerIdentity: () => undefined // bypasses hostname verification
});
// Correct — no checkServerIdentity override; default validates hostname
const tlsSocket = tls.connect({
host: 'internal-service',
port: 443,
ca: fs.readFileSync('/etc/ssl/certs/internal-ca.pem')
});
Certificate pinning for high-value upstreams
Certificate pinning goes further than standard validation: instead of trusting any certificate signed by a recognized CA, the server only trusts a specific certificate (or a specific public key hash). This protects against CA compromise scenarios where an attacker obtains a fraudulent certificate for a domain you connect to from a different, trusted CA.
import https from 'https';
import crypto from 'crypto';
import tls from 'tls';
// SHA-256 hash of the SubjectPublicKeyInfo (SPKI) DER bytes — pinned value
const PINNED_SPKI_HASH = 'sha256//AbCdEfGhIjKlMnOpQrStUvWxYz0123456789+/==';
function verifyCertPin(cert: tls.PeerCertificate): boolean {
const raw = cert.raw; // DER-encoded certificate bytes
const certObj = new crypto.X509Certificate(raw);
const spki = certObj.publicKey.export({ type: 'spki', format: 'der' });
const hash = 'sha256//' + crypto
.createHash('sha256')
.update(spki)
.digest('base64');
return hash === PINNED_SPKI_HASH;
}
const agent = new https.Agent({
checkServerIdentity(hostname, cert) {
// Standard hostname check first
const err = tls.checkServerIdentity(hostname, cert);
if (err) throw err;
// Then verify pin
if (!verifyCertPin(cert)) {
throw new Error(`Certificate pin mismatch for ${hostname}`);
}
}
});
Pin the SubjectPublicKeyInfo (SPKI) hash rather than the full certificate hash. SPKI pinning survives certificate renewal (the public key stays the same; only the certificate wrapper changes), while certificate pinning breaks every time the cert is renewed. Always pin at least two keys: the current key and one backup key, so you can rotate without an outage.
HSTS and TLS downgrade prevention
For MCP servers that expose an HTTP interface (many local MCP servers do for development), HTTP Strict Transport Security (HSTS) prevents TLS downgrade attacks where an attacker strips HTTPS to HTTP on a connection that should be encrypted. Set the header with a long max-age and include subdomains and preload eligibility:
// In your MCP server's HTTP response headers (Caddy example — same in Node Express)
// Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
// In Node.js / Express:
app.use((req, res, next) => {
res.setHeader(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains; preload'
);
next();
});
For MCP servers that only accept local loopback connections during development, HSTS is less relevant — but the outbound TLS validation and pinning patterns above apply to all deployment environments, including development, because development environments are where "temporary" bypasses are introduced and forgotten.
SkillAudit detection
- HIGH:
NODE_TLS_REJECT_UNAUTHORIZED=0,rejectUnauthorized: false, orcheckServerIdentity: () => undefined— disables certificate validation process-wide or per-connection. - HIGH: HTTP (not HTTPS) URLs used to transmit authentication headers or API keys in outbound calls.
- MEDIUM: No CA certificate configured for internal services that use non-public certs — likely requires bypass or is currently broken.
- MEDIUM: No certificate pinning on high-value financial or auth upstream endpoints.
- LOW: No HSTS header on the MCP server's own HTTP interface.
- INFO: Certificate pin not backed up (single pin — rotation risk).
See the MCP server security checklist for the full pre-publish verification list, or run a SkillAudit scan to get findings on your specific server's TLS configuration.