TLS and Hardening

The HTTP transports (sse and streamable-http) enforce four independent network-layer defenses:

  1. TLS enforcement — plaintext HTTP is rejected on non-localhost binds unless explicitly allowed.

  2. Host header validation — every request must match the ZSCALER_MCP_ALLOWED_HOSTS allowlist.

  3. Source IP ACL — optional ZSCALER_MCP_ALLOWED_SOURCE_IPS allowlist for upstream filtering.

  4. CORS — controlled by ZSCALER_MCP_ALLOWED_ORIGINS.

TLS

When the bind address is anything other than 127.0.0.1 / ::1 / localhost, the server requires TLS by default. The expected configuration:

export ZSCALER_MCP_TLS_CERTFILE=/path/to/server.crt
export ZSCALER_MCP_TLS_KEYFILE=/path/to/server.key

zscaler-mcp --transport streamable-http --host 0.0.0.0 --port 8443

If you start with a non-localhost bind and no TLS configured, the server refuses to start and prints a security warning. This is the right default for any deployment that isn’t a managed platform with its own TLS termination.

When TLS termination is upstream

Cloud Run, Azure Container Apps, AWS ALB, and most managed Kubernetes ingresses terminate TLS before the request reaches your container. The server still believes it’s on plaintext HTTP from inside, so you need to opt out of the enforcement:

export ZSCALER_MCP_ALLOW_HTTP=true

zscaler-mcp --transport streamable-http --host 0.0.0.0

Setting ZSCALER_MCP_ALLOW_HTTP=true does not disable TLS — it acknowledges that someone else is terminating it. This is the supported configuration for:

  • Google Cloud Run

  • Azure Container Apps

  • AWS Bedrock AgentCore (ALB)

  • Any Kubernetes deployment with an Ingress (NGINX, Traefik, AWS LB Controller) terminating TLS

For self-managed deployments (a single container or a single VM), keep TLS on by default and provide your own certificate.

Host header validation

Every HTTP request’s Host header is checked against an allowlist. This is a defense against DNS rebinding and reverse-tabnabbing attacks — the server only responds to requests addressed to a hostname you authorized.

The default allowlist is the bind address (host:port). To accept other hostnames:

export ZSCALER_MCP_ALLOWED_HOSTS="mcp.acme.com,mcp.internal.acme.com,*.cloud.run.app"

Comma-separated, wildcards supported via shell-style globbing.

To disable host validation entirely (development only):

export ZSCALER_MCP_DISABLE_HOST_VALIDATION=true

Source-IP ACL

An optional layer for deployments where the MCP server is reachable but should only respond to a known set of source addresses (typically an upstream proxy or NAT egress):

# Single IP
export ZSCALER_MCP_ALLOWED_SOURCE_IPS="203.0.113.5"

# Multiple IPs / CIDRs
export ZSCALER_MCP_ALLOWED_SOURCE_IPS="203.0.113.0/24,198.51.100.5,2001:db8::/32"

Requests from outside the allowlist receive 403 Forbidden before any auth check runs. Useful as an air-gap layer in front of MCP client authentication.

CORS

For browser-based MCP clients, the server’s CORS policy is controlled by:

export ZSCALER_MCP_ALLOWED_ORIGINS="https://app.example.com,https://internal.example.com"

Comma-separated origins. The default is empty (no cross-origin requests allowed). The MCP spec does not require browser clients — most MCP clients today are desktop apps that don’t trigger CORS checks.

Output sanitization

Independent of network-layer hardening, the server runs every string in every tool response through a three-stage sanitizer before serialization. This is the defense-in-depth layer against prompt injection embedded in admin-editable fields (rule descriptions, label names, location names, custom URL category names). See Output Sanitization for the full mechanism.

Defense in depth summary

A production deployment typically combines several of these:

# ~/zscaler-mcp.env
ZSCALER_MCP_HOST=0.0.0.0
ZSCALER_MCP_PORT=8443

# TLS (or ZSCALER_MCP_ALLOW_HTTP=true if upstream terminates)
ZSCALER_MCP_TLS_CERTFILE=/etc/ssl/zscaler-mcp.crt
ZSCALER_MCP_TLS_KEYFILE=/etc/ssl/zscaler-mcp.key

# Host header allowlist
ZSCALER_MCP_ALLOWED_HOSTS=mcp.acme.com

# Source-IP ACL (corporate egress NAT range only)
ZSCALER_MCP_ALLOWED_SOURCE_IPS=203.0.113.0/24

# Authentication
ZSCALER_MCP_AUTH_ENABLED=true
ZSCALER_MCP_AUTH_MODE=zscaler

# Writes disabled by default; opt in if needed
ZSCALER_MCP_WRITE_ENABLED=false

Security posture banner

On startup, the server prints a security posture summary that captures the resolved configuration of every hardening layer:

[SECURITY] transport=streamable-http  host=0.0.0.0:8443  tls=on
           auth=zscaler (cache_ttl=3600s)  host_validation=on
           source_ip_acl=203.0.113.0/24  write_tools=disabled
           sanitization=on  audit_logging=off

The banner is logged regardless of log level — it gives the operator a single line to confirm what’s actually live without rereading the config.

See also