Topic: mcp server TLS configuration

MCP server TLS configuration — transport security for MCP HTTP and SSE servers

Transport security for MCP servers depends almost entirely on which transport you're using. For the stdio transport — by far the most common in community MCP servers — TLS is not relevant because the communication is local IPC between the client process and the server process. For HTTP and SSE transports, TLS is critical and the misconfiguration risks are specific and actionable. This page covers both cases and the outbound TLS discipline that applies to any MCP server regardless of transport.

TL;DR

For MCP servers using stdio transport: TLS is not applicable — the communication is local process IPC. For HTTP/SSE transport: require TLS 1.2+ with modern cipher suites, use a terminating reverse proxy (nginx, Caddy, Traefik) rather than configuring TLS in Node.js/Python directly, and never expose an HTTP endpoint without TLS in a production deployment. For outbound calls from any MCP server: never disable certificate validation (rejectUnauthorized: false, verify=False, NODE_TLS_REJECT_UNAUTHORIZED=0), because an MCP server fetching from an invalid-cert endpoint is either connecting to the wrong host or accepting a man-in-the-middle.

Transport mode determines TLS relevance

The MCP specification supports three transports: stdio, HTTP with SSE, and HTTP with streamable responses (the newer transport introduced in the 2025-11 version of the spec). The stdio transport dominates the community MCP server landscape — it's the transport used by virtually all servers distributed via npm, pip, or similar package managers, because it requires no network configuration and works transparently on the user's local machine.

For stdio transport, TLS is structurally irrelevant. The JSON-RPC messages flow over stdin/stdout between the agent client (Claude Desktop, Claude Code, Cursor, or similar) and the server process. This is local IPC — the communication never leaves the machine, is protected by the operating system's process isolation, and doesn't traverse any network path where a man-in-the-middle attack could be mounted. Adding TLS to stdio transport would be meaningless.

For HTTP and SSE transport, the communication is a network protocol. The client sends JSON-RPC requests to an HTTP endpoint the server exposes. If that endpoint is unencrypted HTTP, any network observer — on the same network segment, at an intermediate router, at the ISP — can read and modify the tool call traffic. If the server is operating in a remote or cloud deployment, that's a serious risk.

Inbound TLS: the reverse proxy pattern

The recommended pattern for HTTP/SSE MCP servers is to handle TLS termination at a reverse proxy rather than in the MCP server process itself. Node.js and Python can handle TLS directly, but maintaining certificate lifecycle (renewal, rotation, OCSP stapling) and keeping cipher suites current is operational overhead that's better absorbed by a purpose-built proxy.

# Caddy reverse proxy — automatic TLS via Let's Encrypt
# Caddy handles certificate issuance, renewal, and TLS configuration

mcp.example.com {
    reverse_proxy localhost:3000
    # Caddy defaults: TLS 1.2+, ECDHE cipher suites, HSTS enabled
    # No additional TLS configuration needed for standard security
}

# nginx equivalent (requires manual cert management)
server {
    listen 443 ssl http2;
    server_name mcp.example.com;

    ssl_certificate /etc/letsencrypt/live/mcp.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mcp.example.com/privkey.pem;

    # Mozilla Intermediate TLS configuration
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;
    ssl_session_timeout 1d;
    ssl_session_cache shared:MozSSL:10m;

    add_header Strict-Transport-Security "max-age=63072000" always;

    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";  # Required for SSE
        proxy_set_header Host $host;
    }
}

The Mozilla SSL Configuration Generator (at mozilla.github.io/server-side-tls/ssl-config-generator/) is the reference for cipher suite selection. For new deployments in 2026, the "Modern" profile (TLS 1.3 only) is appropriate if you control both client and server. The "Intermediate" profile (TLS 1.2+) is appropriate for public-facing servers where some clients may not support TLS 1.3.

Outbound TLS: certificate validation is not optional

This is the anti-pattern that appears in MCP server codebases and represents a genuine security issue regardless of which transport the server uses for inbound communication: disabling TLS certificate validation for outbound HTTP calls.

// UNSAFE — disables certificate validation entirely
const response = await fetch(url, {
  agent: new https.Agent({ rejectUnauthorized: false })
});

// UNSAFE — Node.js environment flag that disables validation globally
// (sometimes set in development, accidentally left in production)
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';

// Python equivalent
import requests
response = requests.get(url, verify=False)  # disables cert verification

Why this matters in the MCP context: an MCP server that fetches from an URL that fails certificate validation has two possible explanations. First, the server is connecting to the wrong host — either through DNS spoofing, BGP hijacking, or a misconfigured environment where the intended hostname resolves to an unintended IP. Disabling cert validation silently accepts this condition and proceeds with a connection to the wrong endpoint. Second, the certificate is genuinely invalid or expired, which means the server or its configuration has a maintenance issue that should be visible, not silently bypassed.

In both cases, the right action is to fix the underlying condition — use a valid certificate, fix the DNS configuration, update the certificate chain — not to suppress the validation error. An SSRF finding combined with disabled certificate validation is especially concerning: the server will follow attacker-controlled URLs to any host, including ones with self-signed certificates that might be set up specifically to intercept MCP server outbound traffic.

// SAFE — respect certificate validation, handle errors explicitly
try {
  const response = await fetch(url);
  // ...
} catch (err) {
  if (err instanceof TypeError && err.message.includes('certificate')) {
    // Log the cert error, return a clear failure
    logger.error({ event: 'tls_cert_error', hostname: new URL(url).hostname });
    throw new McpError(ErrorCode.InternalError, 'TLS certificate validation failed');
  }
  throw err;
}

Client certificate authentication for restricted deployments

For enterprise or team deployments of HTTP/SSE MCP servers where the server should only accept connections from known clients, mutual TLS (mTLS) is a stronger control than bearer tokens or API keys. The server presents its certificate to the client (standard TLS), and the client presents its certificate to the server (the "mutual" part). Only clients with a certificate signed by the server's trusted CA can connect.

This is particularly relevant for MCP servers that expose sensitive internal capabilities — database access, internal API calls, filesystem operations on shared infrastructure. Bearer token authentication can be stolen from the client's environment; mTLS authentication requires possession of a specific certificate file that can be managed with stronger access controls.

The reverse proxy pattern handles mTLS cleanly: configure the proxy to require client certificates and forward a verified-client header to the MCP server process. The MCP server itself doesn't need to implement the TLS layer.

What SkillAudit checks for TLS

The SkillAudit security axis currently checks for the most common outbound TLS anti-pattern: disabled certificate validation. The patterns it scans for:

These patterns are flagged as HIGH when found in production code paths (not in test fixtures). Inbound TLS configuration is not scored — the engine doesn't have visibility into the deployment's reverse proxy configuration. The outbound validation check is the tractable signal available from static analysis of the server code.

For the full static + LLM-probe coverage across all security classes, see the static analysis limits post. For the full grading rubric, see the methodology page.

Further reading