Guide · AWS Networking · NLB TLS

TLS at NLB for MCP Servers: Termination, Pass-Through, and mTLS

NLB gives you three TLS options ALB doesn't: TCP pass-through (NLB never sees the TLS handshake), TLS termination with ALPN policy for HTTP/2 multiplexing, and TLS re-encryption (terminate at NLB, re-encrypt to target). For MCP servers requiring mutual TLS (mTLS) client certificate authentication — common in enterprise agent platforms where the MCP caller must prove identity — pass-through is the only option that preserves the client certificate. For most public MCP servers, NLB TLS termination with ACM is the right choice: AWS manages cert renewal, TLS 1.3 is enforced, and AliveMCP's probes can validate TLS config without special cert pinning. For NLB overview see AWS NLB for MCP Servers; for health check config see NLB Health Checks for MCP Endpoints; for SSE connection handling see WebSocket and SSE over NLB.

TL;DR

Use NLB TLS termination (TLS listener + ACM cert + ELBSecurityPolicy-TLS13-1-2-2021-06) for public MCP servers — AWS handles cert renewal, TLS 1.3 is enforced, and ALPN policy HTTP2Preferred enables HTTP/2 multiplexing. Use TCP pass-through (TCP listener on port 443) when you need mTLS client certificate auth at the MCP container. Use TLS re-encryption when compliance requires no plaintext traffic inside the VPC (terminate at NLB, re-encrypt to target on port 8443).

Three TLS modes for NLB

ModeListenerTarget PortWho terminates TLSmTLS supportUse case
TLS termination TLS/443 Plain TCP (8080) NLB (ACM cert) No — client cert stripped at NLB Public MCP APIs; AWS manages cert renewal; targets see plain HTTP
TCP pass-through TCP/443 TLS target (8443) MCP container Yes — full TLS handshake reaches container mTLS client cert auth; non-standard TLS (QUIC, custom protocol); compliance that forbids NLB seeing plaintext
TLS re-encryption TLS/443 TLS target (8443) NLB (outer) + container (inner) No — client cert stripped at NLB; container-to-NLB cert is separate Zero-trust VPC policy requiring in-transit encryption; compliance audits; regulated data workloads

Mode 1: TLS termination at NLB (most common)

NLB handles the TLS handshake with the client. The connection from NLB to your Fargate task is plain TCP. Your MCP server code sees plain HTTP on port 8080 and doesn't need to handle TLS at all.

import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
import * as acm from 'aws-cdk-lib/aws-certificatemanager';
import * as route53 from 'aws-cdk-lib/aws-route53';
import { Stack } from 'aws-cdk-lib';

// Request or import an ACM cert for your MCP domain
const cert = new acm.Certificate(scope, 'McpCert', {
  domainName: 'api.alivemcp.com',
  subjectAlternativeNames: ['*.alivemcp.com'],
  validation: acm.CertificateValidation.fromDns(hostedZone),
});

// TLS listener with recommended TLS 1.2/1.3 policy
const tlsListener = nlb.addListener('McpTls', {
  port: 443,
  protocol: elbv2.Protocol.TLS,
  certificates: [elbv2.ListenerCertificate.fromCertificateManager(cert)],
  defaultTargetGroups: [targetGroup],
  // ELBSecurityPolicy-TLS13-1-2-2021-06:
  //   - TLS 1.2 and 1.3 only (drops TLS 1.0/1.1)
  //   - Forward-secrecy cipher suites (ECDHE)
  //   - No RC4, no 3DES
  sslPolicy: elbv2.SslPolicy.RECOMMENDED_TLS,
  // ALPN policy: prefer HTTP/2 to enable multiplexed MCP tool calls over a single connection;
  // fall back to HTTP/1.1 for clients that don't support HTTP/2
  alpnPolicy: elbv2.AlpnPolicy.HTTP2_PREFERRED,
});

The ALPN policy matters for MCP SSE connections. HTTP/2 multiplexing allows a single TCP connection to carry multiple SSE streams simultaneously — relevant if the same client agent subscribes to multiple MCP tool result streams. Without HTTP2_PREFERRED, each SSE stream consumes its own TCP connection (HTTP/1.1 limitation), which increases connection count and NLB cost at high subscriber counts.

NLB TLS security policies from most to least restrictive:

PolicyTLS versionsForward secrecyRecommended for
ELBSecurityPolicy-TLS13-1-3-2021-06 TLS 1.3 only Always New MCP deployments; drops old clients (pre-2018 browsers/runtimes)
ELBSecurityPolicy-TLS13-1-2-2021-06 (RECOMMENDED_TLS) TLS 1.2 + 1.3 All ciphers Most MCP servers — wide client compatibility with modern security
ELBSecurityPolicy-FS-1-2-Res-2020-10 TLS 1.2 Yes (ECDHE only) If TLS 1.3 causes issues with specific MCP client libraries
ELBSecurityPolicy-2016-08 TLS 1.0/1.1/1.2 Some ciphers Legacy compatibility only — AliveMCP flags this as a security finding

AliveMCP's TLS probe connects to monitored endpoints and records the negotiated TLS version and cipher suite. Endpoints using TLS 1.0, TLS 1.1, RC4, or 3DES are flagged in the status dashboard with a security advisory badge alongside their uptime metrics.

Mode 2: TCP pass-through for mTLS MCP servers

mTLS requires the server to validate a client certificate during the TLS handshake. If NLB terminates TLS, the client certificate never reaches the MCP container — NLB strips it. Use a TCP listener (port 443) to pass bytes unmodified to the container's TLS stack.

// TCP listener — NLB passes raw TLS bytes to the container
const tcpListener = nlb.addListener('McpMtls', {
  port: 443,
  protocol: elbv2.Protocol.TCP,    // not TLS — NLB doesn't open the TLS envelope
  defaultTargetGroups: [targetGroup],
});

// Target group must use TCP protocol and target port 8443 (container handles TLS)
const mtlsTargetGroup = new elbv2.NetworkTargetGroup(scope, 'McpMtlsTg', {
  vpc,
  port: 8443,
  protocol: elbv2.Protocol.TCP,
  targetType: elbv2.TargetType.IP,
  healthCheck: {
    // Can't use HTTPS health check in pass-through mode without the cert
    // Use a separate plain HTTP port (8080) for health checks only
    protocol: elbv2.Protocol.HTTP,
    path: '/health',
    port: '8080',
    interval: Duration.seconds(10),
    unhealthyThresholdCount: 2,
    healthyThresholdCount: 2,
  },
  deregistrationDelay: Duration.seconds(120),
});

In the MCP container, handle TLS with a Node.js https.createServer or a Bun TLS server that validates client certificates against a CA bundle:

import * as https from 'node:https';
import * as fs from 'node:fs';
import { createMcpHandler } from './mcp-handler';

const server = https.createServer({
  key: fs.readFileSync('/run/secrets/server-key.pem'),
  cert: fs.readFileSync('/run/secrets/server-cert.pem'),
  // mTLS: require and validate client certificate
  requestCert: true,
  rejectUnauthorized: true,
  ca: fs.readFileSync('/run/secrets/client-ca.pem'),
}, (req, res) => {
  // Client certificate is available on req.socket.getPeerCertificate()
  const clientCert = req.socket.getPeerCertificate();
  if (!clientCert.subject?.CN) {
    res.writeHead(401);
    res.end('Client certificate required');
    return;
  }
  createMcpHandler(req, res, clientCert.subject.CN);
});

server.listen(8443, '0.0.0.0');
// Separate plain HTTP server for NLB health checks
require('node:http').createServer((req, res) => {
  res.writeHead(200); res.end('OK');
}).listen(8080, '127.0.0.1');

The separate plain HTTP listener on 8080 for health checks is important — NLB's TCP pass-through mode means NLB can't do HTTPS health checks against the mTLS-protected port (it doesn't have the client cert). Binding the health check listener to 127.0.0.1 prevents external clients from bypassing mTLS via the health check port.

Mode 3: TLS re-encryption (end-to-end encryption in the VPC)

Some compliance frameworks (PCI DSS, HIPAA BAA, FedRAMP) require encryption in transit not just at the perimeter but throughout the VPC. TLS re-encryption puts a TLS listener on the NLB (client-to-NLB TLS) and a second TLS connection from NLB to the target (NLB-to-container TLS).

// TLS listener on NLB (client-to-NLB TLS termination)
const reencryptListener = nlb.addListener('McpReencrypt', {
  port: 443,
  protocol: elbv2.Protocol.TLS,
  certificates: [elbv2.ListenerCertificate.fromCertificateManager(publicCert)],
  sslPolicy: elbv2.SslPolicy.RECOMMENDED_TLS,
});

// Target group: TLS protocol to encrypt NLB-to-container traffic
const reencryptTg = new elbv2.NetworkTargetGroup(scope, 'McpReencryptTg', {
  vpc,
  port: 8443,
  protocol: elbv2.Protocol.TLS,    // NLB opens TLS connection to target
  targetType: elbv2.TargetType.IP,
  // NLB validates target's TLS cert against its trusted CA store (ACM Private CA or public CAs)
  // For self-signed certs on targets, you must disable cert validation via L1 override
  healthCheck: {
    protocol: elbv2.Protocol.HTTPS,
    path: '/health',
    port: '8443',
    interval: Duration.seconds(10),
    unhealthyThresholdCount: 2,
    healthyThresholdCount: 2,
  },
  deregistrationDelay: Duration.seconds(120),
});

For re-encryption, the container's TLS certificate must be valid from NLB's perspective. Use ACM Private CA to issue internal certs — NLB trusts ACM Private CA by default when the target group protocol is TLS. Self-signed certs require an L1 CfnTargetGroup override to disable certificate validation on the NLB-to-target leg.

AliveMCP TLS probe: what it checks on monitored MCP endpoints

For every monitored MCP endpoint, AliveMCP runs a TLS handshake check before the protocol probe:

CheckPass conditionFail → dashboard status
Certificate validity Not expired, issued by trusted CA, CN/SAN matches hostname TLS error — cert expired or hostname mismatch
TLS version TLS 1.2 or TLS 1.3 Security warning — TLS 1.0 or 1.1 negotiated
Certificate expiry warning Expiry > 14 days away Expiry warning badge — cert expires in N days
Cipher suite Forward-secrecy cipher (ECDHE or DHE key exchange) Security advisory — static RSA key exchange (no forward secrecy)
HSTS header Strict-Transport-Security present with max-age >= 31536000 Advisory — HSTS not configured

NLB TLS termination with ELBSecurityPolicy-TLS13-1-2-2021-06 and ACM cert auto-renewal satisfies all five checks automatically. The most common TLS finding AliveMCP surfaces on monitored endpoints is certificate expiry — authors forget to renew certs when running their MCP server on a VPS without Caddy or cert-manager. Author-tier customers receive email alerts 14 days before their monitored endpoint's cert expires.

Proxy Protocol v2: forwarding TLS metadata downstream

When NLB terminates TLS, the MCP container receives plain HTTP and loses visibility into the client's TLS version and cipher suite. Proxy Protocol v2 adds a binary header to the TCP connection with metadata including source IP, destination IP, and — in TLS mode — the TLS version and cipher suite negotiated at the NLB.

// Enable Proxy Protocol v2 on target group to receive TLS metadata at container
const cfnTg = targetGroup.node.defaultChild as elbv2.CfnTargetGroup;
cfnTg.addPropertyOverride('TargetGroupAttributes', [
  { Key: 'proxy_protocol_v2.enabled', Value: 'true' },
]);

// In the MCP container (Node.js), parse Proxy Protocol v2 header:
import { ProxyProtocolV2Parser } from 'proxy-protocol-v2'; // community library
server.on('connection', (socket) => {
  const ppParser = new ProxyProtocolV2Parser(socket);
  ppParser.on('header', (header) => {
    const clientIp = header.sourceAddress;
    const tlsVersion = header.tlvs.find(t => t.type === 0x90)?.value; // PP2_TYPE_AWS_VPCE
    // Log client IP and TLS metadata per MCP session
  });
});

Proxy Protocol v2 with TLS metadata is only available on NLB (ALB doesn't surface TLS metadata via Proxy Protocol). It's useful for MCP servers that want to log per-client TLS version for audit purposes without requiring TLS pass-through.