Security reference · Transport Security · HTTPS

MCP server HTTP Strict Transport Security

An MCP server that serves only HTTPS but does not send a Strict-Transport-Security header is still vulnerable to transport downgrade attacks. An active network attacker intercepting the first HTTP connection — before the user agent has cached any HTTPS preference — can redirect tool calls to an HTTP endpoint, where bearer tokens and tool arguments are transmitted in cleartext. HSTS closes this window by instructing the browser or HTTP client to never connect over plain HTTP, even if an http:// URL is explicitly requested. This reference covers the correct HSTS directive set, the preload requirements, subdomain protection, and how to verify your configuration.

The HTTP downgrade window

HTTPS guarantees confidentiality and integrity only for connections that actually use TLS. Before a client has ever connected to your MCP server, it has no knowledge of whether the server requires HTTPS. An active network attacker (on a coffee shop Wi-Fi or via ARP spoofing) can exploit this:

  1. Client requests http://api.skillaudit.dev/mcp/tools (common if the URL was shared without scheme, or if the client follows a redirect that was injected)
  2. Attacker intercepts and serves a valid-looking response
  3. Bearer token from the Authorization header is captured in plaintext
  4. Tool arguments — potentially including file paths, secrets passed as parameters, or PII — are captured

Your server-side 301 redirect from HTTP to HTTPS doesn't help here — the redirect itself is sent over HTTP and can be intercepted. The only defenses are HSTS (which tells the client to refuse HTTP before connecting) and HSTS preloading (which installs the HSTS policy before the first connection, via a browser-shipped list).

Minimum correct HSTS header

# Caddy — add to your site block
header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
# nginx
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# Node.js / Express (via helmet)
import helmet from 'helmet';
app.use(helmet.hsts({
  maxAge: 31536000,
  includeSubDomains: true,
  preload: true,
}));

Directive breakdown

DirectiveRequired?What it does
max-age=31536000Yes (minimum)Client caches the HSTS policy for 1 year (31,536,000 seconds). The preload list minimum is 1 year. Short max-age values (days, weeks) provide weak protection — an attacker just waits for the policy to expire.
includeSubDomainsStrongly recommendedApplies HSTS to all subdomains (api.skillaudit.dev, staging.skillaudit.dev, etc.). Without this, an attacker can downgrade a subdomain connection and inject a cookie or redirect that affects the main domain.
preloadRequired for preload list submissionSignals that the domain owner consents to preload list inclusion. Does not itself add the domain to the list — you must submit to hstspreload.org separately.

HSTS preloading: before the first connection

HSTS from the response header only protects clients that have already connected to your server successfully over HTTPS. The first-connection window remains open. HSTS preloading closes this window by shipping your domain in browser-bundled lists that are checked before any network connection is made.

Preload requirements (from hstspreload.org, stable as of 2026):

  1. Serve a valid HTTPS certificate on the root domain (not just www)
  2. Redirect all HTTP traffic to HTTPS on the root domain and all subdomains
  3. Send Strict-Transport-Security: max-age=31536000; includeSubDomains; preload on the HTTPS root domain
  4. Submit the domain at hstspreload.org

Preloading is a one-way door. Once your domain is on the preload list, removing it requires submitting a removal request and waiting for browsers to update — which can take months. Browsers will refuse all HTTP connections to your domain, including subdomains, during that window. Test your entire subdomain inventory before submitting: development, staging, health-check, and any internal subdomains must all be HTTPS-capable.

Common HSTS misconfigurations

MisconfigurationRiskFix
Short max-age (60, 3600, 86400)Policy expires; downgrade window reopens after expirySet max-age=31536000 (1 year minimum)
Missing includeSubDomainsSubdomains can be downgraded; cookies set on subdomains leakAdd includeSubDomains after auditing all subdomains for HTTPS readiness
HSTS on HTTP response (not HTTPS)Browsers ignore HSTS headers sent over HTTP — they must come from HTTPSEnsure only the HTTPS virtual host sends the header
HSTS only on specific pathsHSTS is a per-host policy; it must be set on the root or any responseApply globally, not conditionally on path
Behind a TLS terminator that strips headersThe TLS terminator handles HTTPS but the backend doesn't send HSTS — the header never reaches the clientAdd the header at the TLS terminator (Caddy, nginx), not the backend MCP server

Verifying your HSTS configuration

# Check that the header is present and correct
curl -sI https://api.skillaudit.dev | grep -i strict-transport

# Expected output:
# strict-transport-security: max-age=31536000; includeSubDomains; preload

# Check the preload eligibility
# Visit https://hstspreload.org/?domain=skillaudit.dev in a browser

Test with an HTTP client, not a browser. Browsers cache HSTS aggressively — if you've visited the site before, the browser will enforce HTTPS regardless of whether the header is currently being sent. Use curl with -I from a fresh environment (or with --hsts "" to clear the HSTS cache) to verify the header is actually being returned by the server.

MCP server specifics

Most MCP clients are not browsers — they're Claude Code, Cursor, Windsurf, or custom agent harnesses. These clients typically use Node.js fetch, undici, or axios, which implement HSTS via a per-process HSTS cache. The cache does not persist across process restarts, and most agents restart frequently.

This means the first-connection window is broader for MCP clients than for browsers with long-lived HSTS caches. The practical implication: HSTS preloading matters more for MCP servers than for web applications, because MCP clients reset their HSTS state regularly. If your server's domain is on the preload list, the protection is embedded in the client binary and persists regardless of the client's connection history.

For internal MCP servers not accessible over the public internet (VPN-only, localhost), HSTS is irrelevant — but ensure the server does not inadvertently accept external HTTP connections via firewall misconfiguration.

SkillAudit findings for missing HSTS

HIGH −18 No Strict-Transport-Security header on any response — bearer tokens and tool arguments are exposed to network downgrade attacks on every new client connection.
HIGH −14 max-age below 86400 (1 day) — policy expires too quickly; clients that restart frequently (common in agent harnesses) re-enter the unprotected first-connection window daily.
MEDIUM −10 Missing includeSubDomains — subdomain connections can be downgraded; cookies or redirects set on subdomains can affect the main MCP server session.
MEDIUM −8 HSTS header sent from the backend application rather than the TLS terminator — risk of header being stripped if the terminator is reconfigured or replaced.

Run a free HSTS check and full transport security audit on your MCP server at SkillAudit. The audit checks TLS configuration, certificate validity, HSTS presence and correct directives, and HTTP→HTTPS redirect behavior as part of the standard report.

Related references: TLS certificate pinning for MCP servers · session fixation and hijacking · zero-trust MCP architecture