Skip to content

External Plugin mTLS Setup Guideยถ

This guide covers how to set up mutual TLS (mTLS) authentication between the MCP Gateway and external plugin servers.

Port Configurationยถ

Standard port convention: - Port 8000: Main plugin service (HTTP or HTTPS/mTLS) - Port 9000: Health check endpoint (automatically starts on port+1000 when mTLS is enabled)

When mTLS is enabled, the plugin runtime automatically starts a separate HTTP-only health check server on port 9000 (configurable via port + 1000 formula). This allows health checks without requiring mTLS client certificates.

Certificate Generationยถ

The MCP Gateway includes Makefile targets to manage the complete certificate infrastructure for plugin mTLS.

Quick Startยถ

# Generate complete mTLS infrastructure (recommended)
make certs-mcp-all

# This automatically:
# 1. Creates a Certificate Authority (CA)
# 2. Generates gateway client certificate
# 3. Reads plugins/external/config.yaml and generates server certificates for all external plugins

Certificate validity: Default is 825 days (~2.25 years)

Output structure:

certs/mcp/
โ”œโ”€โ”€ ca/                           # Certificate Authority
โ”‚   โ”œโ”€โ”€ ca.key                   # CA private key (protect!)
โ”‚   โ””โ”€โ”€ ca.crt                   # CA certificate
โ”œโ”€โ”€ gateway/                      # Gateway client certificates
โ”‚   โ”œโ”€โ”€ client.key               # Client private key
โ”‚   โ”œโ”€โ”€ client.crt               # Client certificate
โ”‚   โ””โ”€โ”€ ca.crt                   # Copy of CA cert
โ””โ”€โ”€ plugins/                      # Plugin server certificates
    โ””โ”€โ”€ PluginName/
        โ”œโ”€โ”€ server.key           # Server private key
        โ”œโ”€โ”€ server.crt           # Server certificate
        โ””โ”€โ”€ ca.crt               # Copy of CA cert

Makefile Targetsยถ

make certs-mcp-allยถ

Generate complete mTLS infrastructure. This is the recommended command for setting up mTLS.

What it does: 1. Calls certs-mcp-ca to create the CA (if not exists) 2. Calls certs-mcp-gateway to create gateway client certificate (if not exists) 3. Reads plugins/external/config.yaml and generates certificates for all plugins with kind: external

Usage:

# Use default config file (plugins/external/config.yaml)
make certs-mcp-all

# Use custom config file
make certs-mcp-all MCP_PLUGIN_CONFIG=path/to/custom-config.yaml

# Custom certificate validity (in days)
make certs-mcp-all MCP_CERT_DAYS=365

# Combine both options
make certs-mcp-all MCP_PLUGIN_CONFIG=config.yaml MCP_CERT_DAYS=730

Config file format (plugins/external/config.yaml):

plugins:
  - name: "MyPlugin"           # Certificate will be created for this plugin
    kind: "external"           # Must be "external"
    mcp:
      proto: STREAMABLEHTTP
      url: http://127.0.0.1:8000/mcp

  - name: "AnotherPlugin"
    kind: "external"
    mcp:
      proto: STREAMABLEHTTP
      url: http://127.0.0.1:8001/mcp

Fallback behavior: If the config file doesn't exist or PyYAML is not installed, example certificates are generated for example-plugin-a and example-plugin-b.

make certs-mcp-caยถ

Generate the Certificate Authority (CA) for plugin mTLS. This is typically called automatically by other targets.

What it does: - Creates certs/mcp/ca/ca.key (4096-bit RSA private key) - Creates certs/mcp/ca/ca.crt (CA certificate) - Sets file permissions: 600 for .key, 644 for .crt

Usage:

# Generate CA (one-time setup)
make certs-mcp-ca

# Custom validity
make certs-mcp-ca MCP_CERT_DAYS=1825

Safety: Won't overwrite existing CA. To regenerate, delete certs/mcp/ca/ first.

โš ๏ธ Warning: The CA private key (ca.key) is critical. Protect it carefully!

make certs-mcp-gatewayยถ

Generate the gateway client certificate used by the MCP Gateway to authenticate to plugin servers.

What it does: - Depends on certs-mcp-ca (creates CA if needed) - Creates certs/mcp/gateway/client.key (4096-bit RSA private key) - Creates certs/mcp/gateway/client.crt (client certificate signed by CA) - Copies ca.crt to certs/mcp/gateway/

Usage:

# Generate gateway client certificate
make certs-mcp-gateway

# Custom validity
make certs-mcp-gateway MCP_CERT_DAYS=365

Safety: Won't overwrite existing certificate.

make certs-mcp-pluginยถ

Generate a server certificate for a specific plugin.

What it does: - Depends on certs-mcp-ca (creates CA if needed) - Creates certs/mcp/plugins/<PLUGIN_NAME>/server.key - Creates certs/mcp/plugins/<PLUGIN_NAME>/server.crt with Subject Alternative Names (SANs): - DNS:<PLUGIN_NAME> - DNS:mcp-plugin-<PLUGIN_NAME> - DNS:localhost - Copies ca.crt to plugin directory

Usage:

# Generate certificate for specific plugin
make certs-mcp-plugin PLUGIN_NAME=MyCustomPlugin

# Custom validity
make certs-mcp-plugin PLUGIN_NAME=MyPlugin MCP_CERT_DAYS=365

Required: PLUGIN_NAME parameter must be provided.

Use case: Add a new plugin after running certs-mcp-all, or generate certificates manually.

make certs-mcp-checkยถ

Check expiry dates of all MCP certificates.

What it does: - Displays expiry dates for CA, gateway client, and all plugin certificates - Shows remaining validity period

Usage:

make certs-mcp-check

Output example:

๐Ÿ”  Checking MCP certificate expiry dates...

๐Ÿ“‹ CA Certificate:
   Expires: Jan 15 10:30:45 2027 GMT

๐Ÿ“‹ Gateway Client Certificate:
   Expires: Jan 15 10:31:22 2027 GMT

๐Ÿ“‹ Plugin Certificates:
   MyPlugin: Jan 15 10:32:10 2027 GMT
   AnotherPlugin: Jan 15 10:32:45 2027 GMT

Certificate Propertiesยถ

All certificates generated include: - Algorithm: RSA with SHA-256 - CA Key Size: 4096 bits - Client/Server Key Size: 4096 bits - Default Validity: 825 days - Subject Alternative Names (plugins): DNS entries for plugin name and localhost

Important Notesยถ

  1. All ca.crt files are identical - They are copies of the root CA certificate distributed to each location for convenience

  2. Safety features - Commands won't overwrite existing certificates. To regenerate, delete the target directory first

  3. File permissions - Automatically set to secure values:

  4. Private keys (.key): 600 (owner read/write only)
  5. Certificates (.crt): 644 (world-readable)

  6. Configuration variables:

  7. MCP_CERT_DAYS: Certificate validity in days (default: 825)
  8. MCP_PLUGIN_CONFIG: Path to plugin config file (default: plugins/external/config.yaml)

Configuration Optionsยถ

You can configure mTLS using either YAML files or environment variables.

Option 1: YAML Configurationยถ

Server Configuration (Plugin)ยถ

In your plugin config file (e.g., plugins/test.yaml):

plugins:
  - name: "ReplaceBadWordsPlugin"
    kind: "plugins.regex_filter.search_replace.SearchReplacePlugin"
    # ... plugin config ...

server_settings:
  host: "127.0.0.1"
  port: 8000
  tls:
    certfile: certs/mcp/plugins/ReplaceBadWordsPlugin/server.crt
    keyfile: certs/mcp/plugins/ReplaceBadWordsPlugin/server.key
    ca_bundle: certs/mcp/plugins/ReplaceBadWordsPlugin/ca.crt
    ssl_cert_reqs: 2  # 2 = CERT_REQUIRED (enforce client certificates)

Start the server (for testing):

PYTHONPATH=. PLUGINS_CONFIG_PATH="plugins/test.yaml" \
  python3 mcpgateway/plugins/framework/external/mcp/server/runtime.py

Client Configuration (Gateway)ยถ

In your gateway plugin config file (e.g., plugins/external/config-client.yaml):

plugins:
  - name: "ReplaceBadWordsPlugin"
    kind: "external"
    mcp:
      proto: STREAMABLEHTTP
      url: https://127.0.0.1:8000/mcp
      tls:
        certfile: certs/mcp/gateway/client.crt
        keyfile: certs/mcp/gateway/client.key
        ca_bundle: certs/mcp/gateway/ca.crt
        verify: true
        check_hostname: false

Option 2: Environment Variablesยถ

Server Environment Variablesยถ

# Server configuration
export PLUGINS_SERVER_HOST="127.0.0.1"
export PLUGINS_SERVER_PORT="8000"
export PLUGINS_SERVER_SSL_ENABLED="true"

# TLS/mTLS configuration
export PLUGINS_SERVER_SSL_KEYFILE="certs/mcp/plugins/ReplaceBadWordsPlugin/server.key"
export PLUGINS_SERVER_SSL_CERTFILE="certs/mcp/plugins/ReplaceBadWordsPlugin/server.crt"
export PLUGINS_SERVER_SSL_CA_CERTS="certs/mcp/plugins/ReplaceBadWordsPlugin/ca.crt"
export PLUGINS_SERVER_SSL_CERT_REQS="2"  # 2 = CERT_REQUIRED

Start the server (YAML without server_settings section for testing):

PYTHONPATH=. PLUGINS_CONFIG_PATH="plugins/test.yaml" \
  python3 mcpgateway/plugins/framework/external/mcp/server/runtime.py

Client Environment Variablesยถ

export PLUGINS_CLIENT_MTLS_CERTFILE="certs/mcp/gateway/client.crt"
export PLUGINS_CLIENT_MTLS_KEYFILE="certs/mcp/gateway/client.key"
export PLUGINS_CLIENT_MTLS_CA_BUNDLE="certs/mcp/gateway/ca.crt"
export PLUGINS_CLIENT_MTLS_VERIFY="true"
export PLUGINS_CLIENT_MTLS_CHECK_HOSTNAME="false"

Run your gateway code (YAML without tls section in mcp config).

Environment Variable Referenceยถ

Server Variables (Plugin)ยถ

Variable Description Example
PLUGINS_SERVER_HOST Server bind address 127.0.0.1
PLUGINS_SERVER_PORT Server bind port 8000
PLUGINS_SERVER_SSL_ENABLED Enable SSL/TLS true
PLUGINS_SERVER_SSL_KEYFILE Path to server private key certs/.../server.key
PLUGINS_SERVER_SSL_CERTFILE Path to server certificate certs/.../server.crt
PLUGINS_SERVER_SSL_CA_CERTS Path to CA bundle certs/.../ca.crt
PLUGINS_SERVER_SSL_CERT_REQS Client cert requirement (0-2) 2
PLUGINS_SERVER_SSL_KEYFILE_PASSWORD Password for encrypted key password

ssl_cert_reqs values: - 0 = CERT_NONE - No client certificate required - 1 = CERT_OPTIONAL - Client certificate requested but not required - 2 = CERT_REQUIRED - Client certificate required (mTLS)

Client Variables (Gateway)ยถ

Variable Description Example
PLUGINS_CLIENT_MTLS_CERTFILE Path to client certificate certs/.../client.crt
PLUGINS_CLIENT_MTLS_KEYFILE Path to client private key certs/.../client.key
PLUGINS_CLIENT_MTLS_CA_BUNDLE Path to CA bundle certs/.../ca.crt
PLUGINS_CLIENT_MTLS_VERIFY Verify server certificate true
PLUGINS_CLIENT_MTLS_CHECK_HOSTNAME Verify server hostname false
PLUGINS_CLIENT_MTLS_KEYFILE_PASSWORD Password for encrypted key password

Testing mTLSยถ

Test without TLSยถ

# Server
PYTHONPATH=. PLUGINS_CONFIG_PATH="plugins/test.yaml" \
  PLUGINS_SERVER_HOST="127.0.0.1" \
  PLUGINS_SERVER_PORT="8000" \
  PLUGINS_SERVER_SSL_ENABLED="false" \
  python3 mcpgateway/plugins/framework/external/mcp/server/runtime.py &

# Client config should use: url: http://127.0.0.1:8000/mcp

Test with mTLS (YAML)ยถ

# Server (config has server_settings.tls section)
PYTHONPATH=. PLUGINS_CONFIG_PATH="plugins/test.mtls.yaml" \
  python3 mcpgateway/plugins/framework/external/mcp/server/runtime.py &

# Client (config has mcp.tls section)
python3 your_client.py

Test with mTLS (Environment Variables)ยถ

# Server (config has no server_settings section)
# Note: When mTLS is enabled, a health check server automatically starts on port 9000 (port+1000)
PYTHONPATH=. \
  PLUGINS_CONFIG_PATH="plugins/test.yaml" \
  PLUGINS_SERVER_HOST="127.0.0.1" \
  PLUGINS_SERVER_PORT="8000" \
  PLUGINS_SERVER_SSL_ENABLED="true" \
  PLUGINS_SERVER_SSL_KEYFILE="certs/mcp/plugins/ReplaceBadWordsPlugin/server.key" \
  PLUGINS_SERVER_SSL_CERTFILE="certs/mcp/plugins/ReplaceBadWordsPlugin/server.crt" \
  PLUGINS_SERVER_SSL_CA_CERTS="certs/mcp/plugins/ReplaceBadWordsPlugin/ca.crt" \
  PLUGINS_SERVER_SSL_CERT_REQS="2" \
  python3 mcpgateway/plugins/framework/external/mcp/server/runtime.py &

# Client (config has no mcp.tls section)
PLUGINS_CLIENT_MTLS_CERTFILE="certs/mcp/gateway/client.crt" \
  PLUGINS_CLIENT_MTLS_KEYFILE="certs/mcp/gateway/client.key" \
  PLUGINS_CLIENT_MTLS_CA_BUNDLE="certs/mcp/gateway/ca.crt" \
  PLUGINS_CLIENT_MTLS_VERIFY="true" \
  PLUGINS_CLIENT_MTLS_CHECK_HOSTNAME="false" \
  python3 your_client.py

How mTLS Worksยถ

  1. Certificate Authority (CA): A single root CA (ca.crt) signs both client and server certificates
  2. Server Certificate: Plugin server presents its certificate (server.crt) to clients
  3. Client Certificate: Gateway presents its certificate (client.crt) to the plugin server
  4. Mutual Verification: Both parties verify each other's certificates against the CA bundle
  5. Secure Channel: After mutual authentication, all communication is encrypted

Configuration Priorityยถ

Environment variables take precedence over YAML configuration: - If PLUGINS_SERVER_SSL_ENABLED=true, env vars override server_settings.tls - If client env vars are set, they override mcp.tls in YAML

Hostname Verification (check_hostname)ยถ

Overviewยถ

check_hostname is a client-side only setting that verifies the server's certificate matches the hostname/IP you're connecting to.

How It Worksยถ

The client checks if the URL hostname matches entries in the server certificate's: - Common Name (CN): CN=mcp-plugin-ReplaceBadWordsPlugin - Subject Alternative Names (SANs): DNS names or IP addresses

Checking Certificate SANsยถ

# View DNS and IP SANs in server certificate
openssl x509 -in certs/mcp/plugins/ReplaceBadWordsPlugin/server.crt -text -noout | grep -A 5 "Subject Alternative Name"

# Example output:
# X509v3 Subject Alternative Name:
#     DNS:ReplaceBadWordsPlugin, DNS:mcp-plugin-ReplaceBadWordsPlugin, DNS:localhost

Configuration Examplesยถ

Option 1: Use localhost with check_hostname: trueยถ

# Client config
mcp:
  url: https://localhost:8000/mcp
  tls:
    check_hostname: true  # Works because "localhost" is in DNS SANs

Or with environment variables:

export PLUGINS_CLIENT_MTLS_CHECK_HOSTNAME="true"
# Connect to: https://localhost:8000/mcp

Option 2: Use IP address with check_hostname: falseยถ

# Client config
mcp:
  url: https://127.0.0.1:8000/mcp
  tls:
    check_hostname: false  # Required because 127.0.0.1 is not in SANs

Or with environment variables:

export PLUGINS_CLIENT_MTLS_CHECK_HOSTNAME="false"
# Connect to: https://127.0.0.1:8000/mcp

Option 3: Add IP SANs to certificate (Advanced)ยถ

If you need check_hostname: true with IP addresses, regenerate certificates with IP SANs:

# Modify Makefile to add IP SANs when generating certificates
# Add to server.ext or openssl command:
# subjectAltName = DNS:localhost, DNS:plugin-name, IP:127.0.0.1, IP:0.0.0.0

Server-Side Hostname Verificationยถ

There is no check_hostname setting on the server side. The server only: 1. Verifies the client certificate is signed by the trusted CA 2. Checks if ssl_cert_reqs=2 (CERT_REQUIRED) to enforce client certificates

Testing Hostname Verificationยถ

Test 1: Valid hostname (should succeed)ยถ

# Server bound to 0.0.0.0 (accepts all interfaces)
PLUGINS_SERVER_HOST="0.0.0.0" ...

# Client connecting to localhost with hostname check
export PLUGINS_CLIENT_MTLS_CHECK_HOSTNAME="true"
# URL: https://localhost:8000/mcp
# Result: โœ… Success (localhost is in DNS SANs)

Test 2: IP address with hostname check (should fail)ยถ

# Client connecting to IP with hostname check
export PLUGINS_CLIENT_MTLS_CHECK_HOSTNAME="true"
# URL: https://127.0.0.1:8000/mcp
# Result: โŒ Fails with "IP address mismatch, certificate is not valid for '127.0.0.1'"

Troubleshootingยถ

Connection Refusedยถ

  • Ensure server is running: lsof -i :8000
  • Check server logs for startup errors
  • Verify server is bound to correct interface (0.0.0.0 for all, 127.0.0.1 for localhost only)
  • Note: When mTLS is enabled, a health check server also runs on port 9000 (port+1000)

Certificate Verification Failedยถ

  • Verify CA bundle matches on both sides: md5 certs/**/ca.crt
  • Check certificate paths are correct
  • Ensure certificates haven't expired: openssl x509 -in cert.crt -noout -dates

Hostname Verification Failedยถ

Error: certificate verify failed: IP address mismatch or Hostname mismatch

Solutions: 1. Use hostname from SANs: Connect to https://localhost:8000 instead of https://127.0.0.1:8000 2. Disable hostname check: Set check_hostname: false or PLUGINS_CLIENT_MTLS_CHECK_HOSTNAME="false" 3. Add IP to SANs: Regenerate certificates with IP SANs included

mTLS Deployment Hardening Guidelinesยถ

For production deployments, follow these security best practices to ensure robust mTLS configuration:

Category Recommendation Configuration / Option Notes
Certificate Verification Keep hostname and certificate chain verification enabled. YAML: check_hostname: true and valid ca_bundle
Environment: PLUGINS_CLIENT_MTLS_CHECK_HOSTNAME="true" and valid PLUGINS_CLIENT_MTLS_CA_BUNDLE or PLUGINS_SERVER_SSL_CA_CERTS
Only disable in trusted, local test setups.
CA Management Use a dedicated CA for gateway โ†” plugin certificates. YAML: ca_bundle: certs/mcp/gateway/ca.crt
Environment: PLUGINS_SERVER_SSL_CA_CERTS or PLUGINS_CLIENT_MTLS_CA_BUNDLE
Ensures trust is limited to your deployment's CA.
Certificate Rotation Regenerate and redeploy certificates periodically. Local/Docker: Use Makefile targets: make certs-mcp-all, make certs-mcp-check
Kubernetes: Use cert-manager for automated certificate lifecycle management
Recommended: short-lived certs (e.g. 90โ€“180 days). Configure with MCP_CERT_DAYS variable for Makefile targets.
Key Protection Limit read access to private key files. YAML: keyfile paths (e.g., server.key, client.key)
Environment: PLUGINS_SERVER_SSL_KEYFILE or PLUGINS_CLIENT_MTLS_KEYFILE
File permissions: 600 (owner read/write only)
Keys should be owned and readable only by the service account.
TLS Version Enforcement Enforce TLS 1.2 or newer. Controlled by Python's ssl defaults or runtime settings. No additional configuration required; defaults are secure.
Health Endpoint Exposure Bind health endpoints to localhost only. YAML: server_settings.host: 127.0.0.1
Environment: PLUGINS_SERVER_HOST="127.0.0.1"
Prevents unauthenticated HTTP access from external hosts. Health check server (port+1000) is HTTP-only.
Logging & Diagnostics Enable debug logs for TLS handshake troubleshooting. LOG_LEVEL=DEBUG or --verbose Logs cert subjects and handshake results (safe to enable temporarily).
Insecure Mode Control Disable insecure (non-TLS) connections in production. Environment: PLUGINS_SERVER_SSL_ENABLED="true"
Set ssl_cert_reqs: 2 (CERT_REQUIRED) for mTLS enforcement
Guarantees all plugin communications use mTLS.
Configuration Validation Fail fast on missing or invalid TLS configuration. Enabled automatically at startup. Ensures system won't silently downgrade to HTTP.

Implementation Checklistยถ

When deploying plugin mTLS in production:

  1. Generate Certificates:
  2. Local/Docker: Use make certs-mcp-all to create complete certificate infrastructure
  3. Kubernetes: Deploy cert-manager and configure Certificate resources for automated issuance and renewal
  4. Verify Expiration:
  5. Local/Docker: Run make certs-mcp-check regularly to monitor certificate validity
  6. Kubernetes: cert-manager automatically monitors and renews certificates before expiration
  7. Secure Private Keys: Ensure all .key files have 600 permissions and are owned by service accounts (or stored in Kubernetes Secrets with appropriate RBAC)
  8. Enable Hostname Verification: Set check_hostname: true or PLUGINS_CLIENT_MTLS_CHECK_HOSTNAME="true" unless using IP addresses
  9. Configure Health Checks: Bind health servers to 127.0.0.1 to prevent external access
  10. Enforce mTLS: Set PLUGINS_SERVER_SSL_CERT_REQS="2" to require client certificates
  11. Monitor Logs: Enable LOG_LEVEL=DEBUG temporarily during initial deployment to verify handshakes
  12. Plan Rotation:
  13. Local/Docker: Schedule certificate rotation every 90-180 days using MCP_CERT_DAYS parameter
  14. Kubernetes: Configure cert-manager Certificate resources with appropriate renewBefore duration (typically 30 days before expiration)

Security Validationยถ

After deployment, verify your mTLS configuration:

# 1. Check certificate expiration dates
make certs-mcp-check

# 2. Verify file permissions on private keys
find certs/mcp -name "*.key" -exec ls -la {} \;

# 3. Test certificate verification
openssl verify -CAfile certs/mcp/ca/ca.crt certs/mcp/gateway/client.crt

# 4. Confirm TLS version enforcement
openssl s_client -connect localhost:8000 -tls1_1 < /dev/null
# Should fail with "no protocols available" or similar

# 5. Test hostname verification (should succeed)
curl --cert certs/mcp/gateway/client.pem \
     --cacert certs/mcp/gateway/ca.crt \
     https://localhost:8000/health

# 6. Test without client cert (should fail if ssl_cert_reqs=2)
curl --cacert certs/mcp/gateway/ca.crt \
     https://localhost:8000/health