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:
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ยถ
-
All
ca.crt
files are identical - They are copies of the root CA certificate distributed to each location for convenience -
Safety features - Commands won't overwrite existing certificates. To regenerate, delete the target directory first
-
File permissions - Automatically set to secure values:
- Private keys (
.key
):600
(owner read/write only) -
Certificates (
.crt
):644
(world-readable) -
Configuration variables:
MCP_CERT_DAYS
: Certificate validity in days (default: 825)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ยถ
- Certificate Authority (CA): A single root CA (
ca.crt
) signs both client and server certificates - Server Certificate: Plugin server presents its certificate (
server.crt
) to clients - Client Certificate: Gateway presents its certificate (
client.crt
) to the plugin server - Mutual Verification: Both parties verify each other's certificates against the CA bundle
- 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:
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:
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:
- Generate Certificates:
- Local/Docker: Use
make certs-mcp-all
to create complete certificate infrastructure - Kubernetes: Deploy cert-manager and configure Certificate resources for automated issuance and renewal
- Verify Expiration:
- Local/Docker: Run
make certs-mcp-check
regularly to monitor certificate validity - Kubernetes: cert-manager automatically monitors and renews certificates before expiration
- Secure Private Keys: Ensure all
.key
files have600
permissions and are owned by service accounts (or stored in Kubernetes Secrets with appropriate RBAC) - Enable Hostname Verification: Set
check_hostname: true
orPLUGINS_CLIENT_MTLS_CHECK_HOSTNAME="true"
unless using IP addresses - Configure Health Checks: Bind health servers to
127.0.0.1
to prevent external access - Enforce mTLS: Set
PLUGINS_SERVER_SSL_CERT_REQS="2"
to require client certificates - Monitor Logs: Enable
LOG_LEVEL=DEBUG
temporarily during initial deployment to verify handshakes - Plan Rotation:
- Local/Docker: Schedule certificate rotation every 90-180 days using
MCP_CERT_DAYS
parameter - 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