Topic: TLS configuration security

MCP server TLS configuration security — minimum TLS 1.2, cipher suite restriction, and HSTS for HTTP transport

An MCP server's TLS configuration determines the security of every byte that crosses the network: tool call arguments, authentication tokens, upstream API credentials forwarded in responses, and the LLM's tool results. The defaults in Node.js's https module are not maximally secure — they include legacy TLS versions and weak cipher suites for compatibility reasons. Five TLS hardening patterns that match the threat model of a production MCP server carrying sensitive tool traffic.

1. Minimum TLS version — reject TLS 1.0 and 1.1

Node.js's https.createServer uses OpenSSL's default TLS configuration, which historically negotiated TLS 1.0 and 1.1 for backward compatibility. TLS 1.0 is vulnerable to the BEAST attack (CBC padding oracle) and POODLE downgrade. TLS 1.1 removes BEAST but shares TLS 1.0's other limitations and has no modern cipher support. Both were deprecated by RFC 8996 in 2021 and are disabled by default in modern browsers. For MCP servers, backward compatibility with TLS 1.0/1.1 clients is not a valid reason to accept these risks — no production MCP client should be using deprecated TLS versions. Set minVersion explicitly and let old clients fail to connect rather than silently accepting a broken handshake.

import { createServer } from 'https'
import { readFileSync } from 'fs'

const server = createServer({
  key:  readFileSync('/etc/ssl/private/mcp-server.key'),
  cert: readFileSync('/etc/ssl/certs/mcp-server.crt'),
  ca:   readFileSync('/etc/ssl/certs/ca-chain.crt'),

  // Reject TLS 1.0 and TLS 1.1 — TLS 1.2 minimum
  minVersion: 'TLSv1.2',

  // Also set maxVersion to prevent TLS 1.3 negotiation issues
  // on very old intermediaries — generally leave unset to allow TLS 1.3
  // maxVersion: 'TLSv1.3',  // Default: highest supported

  // Prefer the server's cipher order over the client's
  honorCipherOrder: true,
})

server.listen(443)

2. Cipher suite restriction to ECDHE with AES-GCM

Even with TLS 1.2 enforced, the default cipher suite list includes weak options: 3DES (vulnerable to Sweet32 birthday attack at sufficient traffic volumes), AES-CBC mode ciphers without AEAD (vulnerable to padding oracle attacks), and RSA key exchange ciphers (no forward secrecy — a stolen private key decrypts all past sessions). The correct cipher list for a production MCP server restricts to ECDHE for key exchange (forward secrecy) and AES-256-GCM or AES-128-GCM for encryption (AEAD authenticated encryption). This explicit list should be treated as a allowlist: anything not on the list is rejected, regardless of what the client proposes.

const ALLOWED_CIPHERS = [
  // TLS 1.3 cipher suites (automatically used when TLS 1.3 is negotiated)
  'TLS_AES_256_GCM_SHA384',
  'TLS_AES_128_GCM_SHA256',
  'TLS_CHACHA20_POLY1305_SHA256',

  // TLS 1.2 cipher suites — ECDHE + AES-GCM only
  'ECDHE-ECDSA-AES256-GCM-SHA384',
  'ECDHE-RSA-AES256-GCM-SHA384',
  'ECDHE-ECDSA-AES128-GCM-SHA256',
  'ECDHE-RSA-AES128-GCM-SHA256',
  'ECDHE-ECDSA-CHACHA20-POLY1305',
  'ECDHE-RSA-CHACHA20-POLY1305',
].join(':')

const server = createServer({
  key:  readFileSync('/etc/ssl/private/mcp-server.key'),
  cert: readFileSync('/etc/ssl/certs/mcp-server.crt'),
  minVersion: 'TLSv1.2',
  ciphers: ALLOWED_CIPHERS,
  honorCipherOrder: true,  // Use our cipher order, not the client's preference

  // ECDH curve — P-256 is well-audited and widely supported
  ecdhCurve: 'P-256',
})

// Verify actual negotiated cipher on each connection (dev/audit logging)
server.on('secureConnection', (socket) => {
  const { cipher, protocol } = socket.getCipher()
  if (protocol === 'TLSv1' || protocol === 'TLSv1.1') {
    socket.destroy()  // Should not reach here due to minVersion, but be explicit
  }
})

3. Certificate validation — never NODE_TLS_REJECT_UNAUTHORIZED=0

Setting NODE_TLS_REJECT_UNAUTHORIZED=0 in the environment — or equivalently, passing rejectUnauthorized: false to https.request — disables certificate validation for all outbound HTTPS requests from the Node.js process. This includes requests made by MCP tool handlers to upstream APIs. A developer sets it once to work around a local TLS issue, commits it to the .env file, and it reaches production. Now every tool handler is making API calls with no TLS verification — every upstream request is vulnerable to a man-in-the-middle attack that can read credentials, inject responses, and impersonate the API. There is no legitimate production use case for this setting. Detect it, remove it, and block it from returning via a startup assertion.

// Startup assertion: fail fast if TLS validation is disabled
function assertTlsValidationEnabled(): void {
  if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0') {
    console.error([
      'FATAL: NODE_TLS_REJECT_UNAUTHORIZED=0 is set.',
      'This disables TLS certificate validation for ALL outbound HTTPS requests.',
      'Remove this variable from your environment. There is no safe production use.',
      'If you need to trust a custom CA, use NODE_EXTRA_CA_CERTS instead.',
    ].join('\n'))
    process.exit(1)
  }
}

// Call at the very top of your server entry point
assertTlsValidationEnabled()

// For custom CA certificates (correct alternative to disabling validation):
// NODE_EXTRA_CA_CERTS=/path/to/custom-ca.crt node server.js
// This adds your CA to the trust store without disabling validation globally.

// For individual requests that need a custom CA:
import { Agent } from 'https'

const customCaAgent = new Agent({
  ca: readFileSync('/path/to/custom-ca.crt'),
  rejectUnauthorized: true,  // Explicit — always true
})

fetch('https://internal-api.example.com/endpoint', {
  // @ts-ignore — Node.js fetch accepts agent
  agent: customCaAgent,
})

4. HSTS header with 1-year max-age to prevent downgrade attacks

HTTP Strict Transport Security (HSTS) instructs browsers to refuse HTTP connections to your domain for the duration of max-age. Without HSTS, an attacker performing a network intercept can modify the initial HTTP response (before the redirect to HTTPS fires) to strip the redirect, downgrade the connection, and read plaintext traffic. This is the SSL stripping attack. HSTS prevents it by having the browser enforce HTTPS at the client side — the HTTP request is never sent. For MCP servers accessed via browser-based Claude clients, the HSTS header on the MCP server's domain must be set with a max-age of at least 31,536,000 seconds (1 year) and includeSubDomains.

import { IncomingMessage, ServerResponse } from 'http'

const SECURITY_HEADERS: Record<string, string> = {
  // HSTS: 1 year, include subdomains, preload-eligible
  'Strict-Transport-Security': 'max-age=31536000; includeSubDomains; preload',

  // Prevent MIME type sniffing
  'X-Content-Type-Options': 'nosniff',

  // Prevent clickjacking
  'X-Frame-Options': 'DENY',

  // CSP for any HTML responses from the MCP server
  'Content-Security-Policy': "default-src 'none'; frame-ancestors 'none'",

  // Remove server fingerprinting
  'X-Powered-By': '',
  'Server': '',
}

function addSecurityHeaders(_req: IncomingMessage, res: ServerResponse): void {
  for (const [header, value] of Object.entries(SECURITY_HEADERS)) {
    if (value) res.setHeader(header, value)
    else res.removeHeader(header)
  }
}

// Apply in Express middleware or Node.js http handler
server.on('request', (req, res) => {
  addSecurityHeaders(req, res)
  mcpHttpHandler(req, res)
})

5. Certificate pinning for high-sensitivity MCP server deployments

Certificate pinning defends against a compromised or rogue certificate authority (CA) issuing a fraudulent certificate for your MCP server's domain. Without pinning, any of the 100+ trusted CAs in a system trust store can issue a certificate for your domain — and a nation-state or compromised CA can execute a silent MITM. For MCP servers handling authentication, financial, or healthcare data, pin the server's public key hash (SPKI pin) in the client configuration. Pinning the public key rather than the full certificate means the pin survives certificate renewal, as long as the same keypair is retained. Include a backup pin for the CA's intermediate key as a fallback.

import { createHash } from 'crypto'
import { Agent, request as httpsRequest } from 'https'
import { connect as tlsConnect } from 'tls'

// Compute SPKI pin: SHA-256 of the SubjectPublicKeyInfo DER encoding
// Generate with: openssl x509 -in cert.pem -pubkey -noout | \
//   openssl pkey -pubin -outform DER | openssl dgst -sha256 -binary | base64
const PINNED_KEYS = new Set([
  'abc123...=',  // Current certificate public key pin
  'xyz789...=',  // Backup: intermediate CA public key pin (for renewal safety)
])

function checkPin(cert: { pubkey: Buffer }): void {
  const pin = createHash('sha256').update(cert.pubkey).digest('base64')
  if (!PINNED_KEYS.has(pin)) {
    throw new Error(`TLS pin mismatch: received ${pin}`)
  }
}

// Custom agent that verifies the pin on every connection
const pinnedAgent = new Agent({
  checkServerIdentity: (hostname, cert) => {
    // Standard hostname check first
    const stdError = require('tls').checkServerIdentity(hostname, cert)
    if (stdError) return stdError

    // Then verify the pin
    const pubkeyDer = cert.pubkey
    const pin = createHash('sha256').update(pubkeyDer).digest('base64')
    if (!PINNED_KEYS.has(pin)) {
      return new Error(`Certificate pin mismatch for ${hostname}: ${pin}`)
    }
    return undefined  // Pin verified
  },
})

How TLS configuration maps to SkillAudit sub-scores

TLS misconfigurations generate findings across Security and Credential Exposure sub-scores because weak TLS is both a direct vulnerability and a credential exfiltration path:

For the WebSocket transport equivalent of TLS hardening, see MCP server WebSocket transport security. For the network-level controls that work alongside TLS, see MCP server network segmentation.

Check your MCP server's TLS configuration

SkillAudit detects weak TLS versions, insecure cipher suites, disabled certificate validation, and missing HSTS configuration in MCP server implementations.

See pricing