diff --git a/.env.example b/.env.example
index f17cddd4c..a980aa5c7 100644
--- a/.env.example
+++ b/.env.example
@@ -603,6 +603,15 @@ PLUGINS_ENABLED=false
# Default: plugins/config.yaml
PLUGIN_CONFIG_FILE=plugins/config.yaml
+# Optional defaults for mTLS when connecting to external MCP plugins (STREAMABLEHTTP transport)
+# Provide file paths inside the container. Plugin-specific TLS blocks override these defaults.
+# PLUGINS_MTLS_CA_BUNDLE=/app/certs/plugins/ca.crt
+# PLUGINS_MTLS_CLIENT_CERT=/app/certs/plugins/gateway-client.pem
+# PLUGINS_MTLS_CLIENT_KEY=/app/certs/plugins/gateway-client.key
+# PLUGINS_MTLS_CLIENT_KEY_PASSWORD=
+# PLUGINS_MTLS_VERIFY=true
+# PLUGINS_MTLS_CHECK_HOSTNAME=true
+
#####################################
# Well-Known URI Configuration
#####################################
diff --git a/README.md b/README.md
index 753aa2a0e..c35ded0a4 100644
--- a/README.md
+++ b/README.md
@@ -1581,6 +1581,12 @@ MCP Gateway uses Alembic for database migrations. Common commands:
| ------------------------------ | ------------------------------------------------ | --------------------- | ------- |
| `PLUGINS_ENABLED` | Enable the plugin framework | `false` | bool |
| `PLUGIN_CONFIG_FILE` | Path to main plugin configuration file | `plugins/config.yaml` | string |
+| `PLUGINS_MTLS_CA_BUNDLE` | (Optional) default CA bundle for external plugin mTLS | _(empty)_ | string |
+| `PLUGINS_MTLS_CLIENT_CERT` | (Optional) gateway client certificate for plugin mTLS | _(empty)_ | string |
+| `PLUGINS_MTLS_CLIENT_KEY` | (Optional) gateway client key for plugin mTLS | _(empty)_ | string |
+| `PLUGINS_MTLS_CLIENT_KEY_PASSWORD` | (Optional) password for plugin client key | _(empty)_ | string |
+| `PLUGINS_MTLS_VERIFY` | (Optional) verify remote plugin certificates (`true`/`false`) | `true` | bool |
+| `PLUGINS_MTLS_CHECK_HOSTNAME` | (Optional) enforce hostname verification for plugins | `true` | bool |
| `PLUGINS_CLI_COMPLETION` | Enable auto-completion for plugins CLI | `false` | bool |
| `PLUGINS_CLI_MARKUP_MODE` | Set markup mode for plugins CLI | (none) | `rich`, `markdown`, `disabled` |
diff --git a/charts/mcp-stack/values.yaml b/charts/mcp-stack/values.yaml
index 5d7078677..4c14863f4 100644
--- a/charts/mcp-stack/values.yaml
+++ b/charts/mcp-stack/values.yaml
@@ -267,6 +267,12 @@ mcpContextForge:
# ─ Plugin Configuration ─
PLUGINS_ENABLED: "false" # enable the plugin framework
PLUGIN_CONFIG_FILE: "plugins/config.yaml" # path to main plugin configuration file
+ PLUGINS_MTLS_CA_BUNDLE: "" # default CA bundle for external plugins (optional)
+ PLUGINS_MTLS_CLIENT_CERT: "" # gateway client certificate for plugin mTLS
+ PLUGINS_MTLS_CLIENT_KEY: "" # gateway client key for plugin mTLS (optional)
+ PLUGINS_MTLS_CLIENT_KEY_PASSWORD: "" # password for the plugin client key (optional)
+ PLUGINS_MTLS_VERIFY: "true" # verify remote plugin certificates
+ PLUGINS_MTLS_CHECK_HOSTNAME: "true" # enforce hostname verification when verifying certs
PLUGINS_CLI_COMPLETION: "false" # enable auto-completion for plugins CLI
PLUGINS_CLI_MARKUP_MODE: "" # set markup mode for plugins CLI
diff --git a/docs/docs/architecture/plugin-spec/.pages b/docs/docs/architecture/plugin-spec/.pages
index 9e8f3584a..0fb74117a 100644
--- a/docs/docs/architecture/plugin-spec/.pages
+++ b/docs/docs/architecture/plugin-spec/.pages
@@ -14,4 +14,4 @@ nav:
- Performance: 11-performance.md
- Development: 12-development.md
- Testing: 13-testing.md
- - Conclusion: 14-conclusion.md
\ No newline at end of file
+ - Conclusion: 14-conclusion.md
diff --git a/docs/docs/architecture/plugin-spec/01-architecture.md b/docs/docs/architecture/plugin-spec/01-architecture.md
index b5f0b3266..11cce4ad3 100644
--- a/docs/docs/architecture/plugin-spec/01-architecture.md
+++ b/docs/docs/architecture/plugin-spec/01-architecture.md
@@ -75,4 +75,3 @@ mcpgateway/plugins/framework/
- Communicate via MCP protocol over various transports
- 10-100ms latency depending on service and network
- Examples: LlamaGuard, OpenAI Moderation, custom AI services
-
diff --git a/docs/docs/architecture/plugin-spec/02-core-components.md b/docs/docs/architecture/plugin-spec/02-core-components.md
index 536bd3c66..39b28b3ed 100644
--- a/docs/docs/architecture/plugin-spec/02-core-components.md
+++ b/docs/docs/architecture/plugin-spec/02-core-components.md
@@ -3,7 +3,7 @@
### 3.1 Plugin Base Class
-The base plugin class, of which developers subclass and implement the hooks that are important for their plugins. Hook points are functions that appear interpose on existing MCP and agent-based functionality.
+The base plugin class, of which developers subclass and implement the hooks that are important for their plugins. Hook points are functions that appear interpose on existing MCP and agent-based functionality.
```python
class Plugin:
@@ -122,4 +122,3 @@ class PluginInstanceRegistry:
async def shutdown(self) -> None:
"""Shutdown all registered plugins"""
```
-
diff --git a/docs/docs/architecture/plugin-spec/03-plugin-types.md b/docs/docs/architecture/plugin-spec/03-plugin-types.md
index 8b58a598a..9616bcf6b 100644
--- a/docs/docs/architecture/plugin-spec/03-plugin-types.md
+++ b/docs/docs/architecture/plugin-spec/03-plugin-types.md
@@ -37,7 +37,7 @@ The configuration system supports both **native plugins** (running in-process) a
### 4.2 Plugin Configuration Schema
-Below is an example of a plugin configuration file. A plugin configuration file can configure one or more plugins in a prioritized list as below. Each individual plugin is an instance of the of a plugin class that subclasses the base `Plugin` object and implements a set of hooks as listed in the configuration.
+Below is an example of a plugin configuration file. A plugin configuration file can configure one or more plugins in a prioritized list as below. Each individual plugin is an instance of the of a plugin class that subclasses the base `Plugin` object and implements a set of hooks as listed in the configuration.
```yaml
# plugins/config.yaml
@@ -408,4 +408,3 @@ The manifest enables development tools to provide:
- Follow established tag conventions within your organization
The plugin manifest system provides a foundation for plugin ecosystem management, enabling better development workflows, automated tooling, and enhanced discoverability while maintaining consistency across plugin implementations.
-
diff --git a/docs/docs/architecture/plugin-spec/04-hook-architecture.md b/docs/docs/architecture/plugin-spec/04-hook-architecture.md
index 2ccdffe23..09b3327bf 100644
--- a/docs/docs/architecture/plugin-spec/04-hook-architecture.md
+++ b/docs/docs/architecture/plugin-spec/04-hook-architecture.md
@@ -186,7 +186,7 @@ return PluginResult(
**Processing Model**:
-Plugin processing uses short circuiting to abort evaluation in the case of a violation and `continue_processing=False`. If the plugin needs to record side effects, such as the bookkeeping, these plugins should be executed first with the highest priority.
+Plugin processing uses short circuiting to abort evaluation in the case of a violation and `continue_processing=False`. If the plugin needs to record side effects, such as the bookkeeping, these plugins should be executed first with the highest priority.
### 5.2 HTTP Header Hook Integration Example
@@ -477,4 +477,3 @@ async def process_elicitation_response(self, response: ElicitationResponse) -> b
return True
```
-
diff --git a/docs/docs/architecture/plugin-spec/05-hook-system.md b/docs/docs/architecture/plugin-spec/05-hook-system.md
index 07913e25c..cbadba56d 100644
--- a/docs/docs/architecture/plugin-spec/05-hook-system.md
+++ b/docs/docs/architecture/plugin-spec/05-hook-system.md
@@ -184,4 +184,3 @@ This document covers administrative operation hooks:
- Gateway Federation Hooks - Peer gateway management *(Future)*
- A2A Agent Hooks - Agent-to-Agent integration management *(Future)*
- Entity Lifecycle Hooks - Tool, resource, and prompt registration *(Future)*
-
diff --git a/docs/docs/architecture/plugin-spec/06-gateway-hooks.md b/docs/docs/architecture/plugin-spec/06-gateway-hooks.md
index 604ac5c9a..51411c86f 100644
--- a/docs/docs/architecture/plugin-spec/06-gateway-hooks.md
+++ b/docs/docs/architecture/plugin-spec/06-gateway-hooks.md
@@ -1667,4 +1667,3 @@ The gateway administrative hooks are organized into the following categories:
- Implement proper timeout handling for elicitations
- Cache frequently accessed data (permissions, quotas)
- Use background tasks for non-critical operations
-
diff --git a/docs/docs/architecture/plugin-spec/07-security-hooks.md b/docs/docs/architecture/plugin-spec/07-security-hooks.md
index bc6d65475..cd789c9d3 100644
--- a/docs/docs/architecture/plugin-spec/07-security-hooks.md
+++ b/docs/docs/architecture/plugin-spec/07-security-hooks.md
@@ -758,4 +758,3 @@ async def resource_post_fetch(self, payload: ResourcePostFetchPayload, context:
- Resource post-fetch may take longer due to content processing
- Plugin execution is sequential within priority bands
- Failed plugins don't affect other plugins (isolation)
-
diff --git a/docs/docs/architecture/plugin-spec/08-external-plugins.md b/docs/docs/architecture/plugin-spec/08-external-plugins.md
index 6150b999d..3c318d667 100644
--- a/docs/docs/architecture/plugin-spec/08-external-plugins.md
+++ b/docs/docs/architecture/plugin-spec/08-external-plugins.md
@@ -112,6 +112,11 @@ plugins:
mcp:
proto: "STREAMABLEHTTP"
url: "http://localhost:8000/mcp"
+ # Optional TLS block when the remote server requires mTLS
+ # tls:
+ # ca_bundle: /app/certs/plugins/ca.crt
+ # client_cert: /app/certs/plugins/gateway-client.pem
+ # client_key: /app/certs/plugins/gateway-client.key
```
### 7.2 MCP Protocol Integration
@@ -178,10 +183,21 @@ plugins:
mcp:
proto: "STREAMABLEHTTP" # Transport protocol
url: "http://openai-plugin:3000/mcp" # Server URL
+ # Optional mutual TLS configuration
+ # tls:
+ # ca_bundle: /app/certs/plugins/ca.crt
+ # client_cert: /app/certs/plugins/gateway-client.pem
+ # verify: true
# Optional authentication
auth:
type: "bearer"
token: "${OPENAI_API_KEY}"
+
+If you prefer centralised defaults, set the environment variables
+`PLUGINS_MTLS_CA_BUNDLE`, `PLUGINS_MTLS_CLIENT_CERT`, and related
+settings. These values apply whenever a plugin omits its own `tls`
+section, allowing a single gateway-wide certificate bundle to be reused
+across multiple external plugins.
```
### 7.5 MCP Transport Types
diff --git a/docs/docs/architecture/plugin-spec/09-security.md b/docs/docs/architecture/plugin-spec/09-security.md
index eca593942..937678659 100644
--- a/docs/docs/architecture/plugin-spec/09-security.md
+++ b/docs/docs/architecture/plugin-spec/09-security.md
@@ -65,4 +65,3 @@ except Exception as e:
raise PluginError(f"Plugin error: {plugin.name}")
# Continue with next plugin in permissive mode
```
-
diff --git a/docs/docs/architecture/plugin-spec/10-error-handling.md b/docs/docs/architecture/plugin-spec/10-error-handling.md
index de053265f..fa301066c 100644
--- a/docs/docs/architecture/plugin-spec/10-error-handling.md
+++ b/docs/docs/architecture/plugin-spec/10-error-handling.md
@@ -414,4 +414,3 @@ async def execute(self, plugins: list[PluginRef], ...) -> tuple[PluginResult[T],
raise PluginError(f"Plugin error: {plugin.name}")
# Continue with next plugin
```
-
diff --git a/docs/docs/architecture/plugin-spec/11-performance.md b/docs/docs/architecture/plugin-spec/11-performance.md
index 0e502dc07..34486714e 100644
--- a/docs/docs/architecture/plugin-spec/11-performance.md
+++ b/docs/docs/architecture/plugin-spec/11-performance.md
@@ -14,4 +14,3 @@
- **Context management**: Handle 10,000+ concurrent request contexts
- **Memory usage**: Base framework overhead <5MB
- **Plugin loading**: Initialize plugins in <10 seconds
-
diff --git a/docs/docs/architecture/plugin-spec/12-development.md b/docs/docs/architecture/plugin-spec/12-development.md
index 3dd281706..8179d5ed0 100644
--- a/docs/docs/architecture/plugin-spec/12-development.md
+++ b/docs/docs/architecture/plugin-spec/12-development.md
@@ -286,4 +286,3 @@ class TestMyPlugin:
- Include execution metrics
- Provide health check endpoints
- Support debugging modes
-
diff --git a/docs/docs/architecture/plugin-spec/13-testing.md b/docs/docs/architecture/plugin-spec/13-testing.md
index a7dede5bc..4fbe55f89 100644
--- a/docs/docs/architecture/plugin-spec/13-testing.md
+++ b/docs/docs/architecture/plugin-spec/13-testing.md
@@ -22,4 +22,3 @@ The plugin framework provides comprehensive testing support across multiple leve
- Validate external plugin communication
- Performance and load testing
- Security validation
-
diff --git a/docs/docs/architecture/plugin-spec/14-conclusion.md b/docs/docs/architecture/plugin-spec/14-conclusion.md
index 8f4caccf3..e8dd58b8f 100644
--- a/docs/docs/architecture/plugin-spec/14-conclusion.md
+++ b/docs/docs/architecture/plugin-spec/14-conclusion.md
@@ -36,4 +36,3 @@ This specification defines a comprehensive, production-ready plugin framework fo
This specification serves as the definitive guide for developing, deploying, and operating plugins within the MCP Context Forge ecosystem, ensuring consistency, security, and performance across all plugin implementations.
**Document Version**: 1.0
-
diff --git a/docs/docs/architecture/plugin-spec/plugin-framework-specification.md b/docs/docs/architecture/plugin-spec/plugin-framework-specification.md
index deb2cf1e5..0dc10f525 100644
--- a/docs/docs/architecture/plugin-spec/plugin-framework-specification.md
+++ b/docs/docs/architecture/plugin-spec/plugin-framework-specification.md
@@ -57,4 +57,3 @@ This specification covers:
- **Plugin Manager**: Core service managing plugin lifecycle and execution
- **Plugin Context**: Request-scoped state shared between plugins
- **Plugin Configuration**: YAML-based plugin setup and parameters
-
diff --git a/docs/docs/architecture/plugins.md b/docs/docs/architecture/plugins.md
index c874fc313..b4d63668b 100644
--- a/docs/docs/architecture/plugins.md
+++ b/docs/docs/architecture/plugins.md
@@ -839,6 +839,9 @@ plugins:
mcp:
proto: "STREAMABLEHTTP"
url: "http://nodejs-plugin:3000/mcp"
+ # tls:
+ # ca_bundle: /app/certs/plugins/ca.crt
+ # client_cert: /app/certs/plugins/gateway-client.pem
# Go plugin
- name: "HighPerformanceFilter"
@@ -853,6 +856,12 @@ plugins:
mcp:
proto: "STREAMABLEHTTP"
url: "http://rust-plugin:8080/mcp"
+ # tls:
+ # verify: true
+
+Gateway-wide defaults for these TLS options can be supplied via the
+`PLUGINS_MTLS_*` environment variables when you want every external
+plugin to share the same client certificate and CA bundle.
```
## Remote Plugin MCP Server Integration
diff --git a/docs/docs/index.md b/docs/docs/index.md
index 379b4c88d..3bc72c995 100644
--- a/docs/docs/index.md
+++ b/docs/docs/index.md
@@ -1435,6 +1435,12 @@ MCP Gateway uses Alembic for database migrations. Common commands:
| ------------------------------ | ------------------------------------------------ | --------------------- | ------- |
| `PLUGINS_ENABLED` | Enable the plugin framework | `false` | bool |
| `PLUGIN_CONFIG_FILE` | Path to main plugin configuration file | `plugins/config.yaml` | string |
+| `PLUGINS_MTLS_CA_BUNDLE` | (Optional) default CA bundle for external plugin mTLS | _(empty)_ | string |
+| `PLUGINS_MTLS_CLIENT_CERT` | (Optional) gateway client certificate for plugin mTLS | _(empty)_ | string |
+| `PLUGINS_MTLS_CLIENT_KEY` | (Optional) gateway client key for plugin mTLS | _(empty)_ | string |
+| `PLUGINS_MTLS_CLIENT_KEY_PASSWORD` | (Optional) password for plugin client key | _(empty)_ | string |
+| `PLUGINS_MTLS_VERIFY` | (Optional) verify remote plugin certificates (`true`/`false`) | `true` | bool |
+| `PLUGINS_MTLS_CHECK_HOSTNAME` | (Optional) enforce hostname verification for plugins | `true` | bool |
| `PLUGINS_CLI_COMPLETION` | Enable auto-completion for plugins CLI | `false` | bool |
| `PLUGINS_CLI_MARKUP_MODE` | Set markup mode for plugins CLI | (none) | `rich`, `markdown`, `disabled` |
diff --git a/docs/docs/manage/mtls.md b/docs/docs/manage/mtls.md
new file mode 100644
index 000000000..e02ed047f
--- /dev/null
+++ b/docs/docs/manage/mtls.md
@@ -0,0 +1,943 @@
+# mTLS (Mutual TLS) Configuration
+
+Configure mutual TLS authentication for MCP Gateway to enable certificate-based client authentication and enhanced security.
+
+## Overview
+
+Mutual TLS (mTLS) provides bidirectional authentication between clients and servers using X.509 certificates. While native mTLS support is in development ([#568](https://github.com/IBM/mcp-context-forge/issues/568)), MCP Gateway can leverage reverse proxies for production-ready mTLS today.
+
+## Current Status
+
+- **Native mTLS**: 🚧 In Progress - tracked in [#568](https://github.com/IBM/mcp-context-forge/issues/568)
+- **Proxy-based mTLS**: ✅ Available - using Nginx, Caddy, or other reverse proxies
+- **Container Support**: ✅ Ready - lightweight containers support proxy deployment
+
+## Architecture
+
+```mermaid
+sequenceDiagram
+ participant Client
+ participant Proxy as Reverse Proxy
(Nginx/Caddy)
+ participant Gateway as MCP Gateway
+ participant MCP as MCP Server
+
+ Client->>Proxy: TLS Handshake
+ Client Certificate
+ Proxy->>Proxy: Verify Client Cert
+ Proxy->>Gateway: HTTP + X-SSL Headers
+ Gateway->>Gateway: Extract User from Headers
+ Gateway->>MCP: Forward Request
+ MCP-->>Gateway: Response
+ Gateway-->>Proxy: Response
+ Proxy-->>Client: TLS Response
+```
+
+## Quick Start
+
+### Option 1: Docker Compose with Nginx mTLS
+
+1. **Generate certificates** (for testing):
+
+```bash
+# Create certificates directory
+mkdir -p certs/mtls
+
+# Generate CA certificate
+openssl req -x509 -newkey rsa:4096 -days 365 -nodes \
+ -keyout certs/mtls/ca.key -out certs/mtls/ca.crt \
+ -subj "/C=US/ST=State/L=City/O=MCP-CA/CN=MCP Root CA"
+
+# Generate server certificate
+openssl req -newkey rsa:4096 -nodes \
+ -keyout certs/mtls/server.key -out certs/mtls/server.csr \
+ -subj "/CN=gateway.local"
+
+openssl x509 -req -in certs/mtls/server.csr \
+ -CA certs/mtls/ca.crt -CAkey certs/mtls/ca.key \
+ -CAcreateserial -out certs/mtls/server.crt -days 365
+
+# Generate client certificate
+openssl req -newkey rsa:4096 -nodes \
+ -keyout certs/mtls/client.key -out certs/mtls/client.csr \
+ -subj "/CN=admin@example.com"
+
+openssl x509 -req -in certs/mtls/client.csr \
+ -CA certs/mtls/ca.crt -CAkey certs/mtls/ca.key \
+ -CAcreateserial -out certs/mtls/client.crt -days 365
+
+# Create client bundle for testing
+cat certs/mtls/client.crt certs/mtls/client.key > certs/mtls/client.pem
+```
+
+2. **Create Nginx configuration** (`nginx-mtls.conf`):
+
+```nginx
+events {
+ worker_connections 1024;
+}
+
+http {
+ upstream mcp_gateway {
+ server gateway:4444;
+ }
+
+ server {
+ listen 443 ssl;
+ server_name gateway.local;
+
+ # Server certificates
+ ssl_certificate /etc/nginx/certs/server.crt;
+ ssl_certificate_key /etc/nginx/certs/server.key;
+
+ # mTLS client verification
+ ssl_client_certificate /etc/nginx/certs/ca.crt;
+ ssl_verify_client on;
+ ssl_verify_depth 2;
+
+ # Strong TLS settings
+ ssl_protocols TLSv1.2 TLSv1.3;
+ ssl_ciphers HIGH:!aNULL:!MD5;
+ ssl_prefer_server_ciphers on;
+
+ location / {
+ proxy_pass http://mcp_gateway;
+ proxy_http_version 1.1;
+
+ # Pass client certificate info to MCP Gateway
+ proxy_set_header X-SSL-Client-Cert $ssl_client_escaped_cert;
+ proxy_set_header X-SSL-Client-S-DN $ssl_client_s_dn;
+ proxy_set_header X-SSL-Client-S-DN-CN $ssl_client_s_dn_cn;
+ proxy_set_header X-SSL-Client-Verify $ssl_client_verify;
+ proxy_set_header X-Authenticated-User $ssl_client_s_dn_cn;
+
+ # Standard proxy headers
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
+ # WebSocket support
+ location /ws {
+ proxy_pass http://mcp_gateway;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ proxy_set_header X-SSL-Client-S-DN-CN $ssl_client_s_dn_cn;
+ proxy_set_header X-Authenticated-User $ssl_client_s_dn_cn;
+ }
+
+ # SSE support
+ location ~ ^/servers/.*/sse$ {
+ proxy_pass http://mcp_gateway;
+ proxy_http_version 1.1;
+ proxy_set_header X-SSL-Client-S-DN-CN $ssl_client_s_dn_cn;
+ proxy_set_header X-Authenticated-User $ssl_client_s_dn_cn;
+ proxy_set_header Connection "";
+ proxy_buffering off;
+ proxy_cache off;
+ }
+ }
+}
+```
+
+3. **Create Docker Compose file** (`docker-compose-mtls.yml`):
+
+```yaml
+version: '3.8'
+
+services:
+ nginx-mtls:
+ image: nginx:alpine
+ ports:
+ - "443:443"
+ volumes:
+ - ./nginx-mtls.conf:/etc/nginx/nginx.conf:ro
+ - ./certs/mtls:/etc/nginx/certs:ro
+ networks:
+ - mcpnet
+ depends_on:
+ - gateway
+
+ gateway:
+ image: ghcr.io/ibm/mcp-context-forge:latest
+ environment:
+ - HOST=0.0.0.0
+ - PORT=4444
+ - DATABASE_URL=sqlite:////app/data/mcp.db
+
+ # Disable JWT auth and trust proxy headers
+ - MCP_CLIENT_AUTH_ENABLED=false
+ - TRUST_PROXY_AUTH=true
+ - PROXY_USER_HEADER=X-SSL-Client-S-DN-CN
+
+ # Keep admin UI protected
+ - AUTH_REQUIRED=true
+ - BASIC_AUTH_USER=admin
+ - BASIC_AUTH_PASSWORD=changeme
+
+ # Enable admin features
+ - MCPGATEWAY_UI_ENABLED=true
+ - MCPGATEWAY_ADMIN_API_ENABLED=true
+ networks:
+ - mcpnet
+ volumes:
+ - ./data:/app/data # persists SQLite database at /app/data/mcp.db
+
+networks:
+ mcpnet:
+ driver: bridge
+```
+> 💾 Run `mkdir -p data` before `docker-compose up` so the SQLite database survives restarts.
+
+
+4. **Test the connection**:
+
+```bash
+# Start the services
+docker-compose -f docker-compose-mtls.yml up -d
+
+# Test with client certificate
+curl --cert certs/mtls/client.pem \
+ --cacert certs/mtls/ca.crt \
+ https://localhost/health
+
+# Test without certificate (should fail)
+curl https://localhost/health
+# Error: SSL certificate problem
+```
+
+### Option 2: Caddy with mTLS
+
+1. **Create Caddyfile** (`Caddyfile.mtls`):
+
+```caddyfile
+{
+ # Global options
+ debug
+}
+
+gateway.local {
+ # Enable mTLS
+ tls {
+ client_auth {
+ mode require_and_verify
+ trusted_ca_cert_file /etc/caddy/certs/ca.crt
+ }
+ }
+
+ # Reverse proxy to MCP Gateway
+ reverse_proxy gateway:4444 {
+ # Pass certificate info as headers
+ header_up X-SSL-Client-Cert {http.request.tls.client.certificate_pem_escaped}
+ header_up X-SSL-Client-S-DN {http.request.tls.client.subject}
+ header_up X-SSL-Client-S-DN-CN {http.request.tls.client.subject_cn}
+ header_up X-Authenticated-User {http.request.tls.client.subject_cn}
+
+ # WebSocket support
+ @websocket {
+ header Connection *Upgrade*
+ header Upgrade websocket
+ }
+ transport http {
+ versions 1.1
+ }
+ }
+}
+```
+
+2. **Docker Compose with Caddy**:
+
+```yaml
+version: '3.8'
+
+services:
+ caddy-mtls:
+ image: caddy:alpine
+ ports:
+ - "443:443"
+ volumes:
+ - ./Caddyfile.mtls:/etc/caddy/Caddyfile:ro
+ - ./certs/mtls:/etc/caddy/certs:ro
+ - caddy_data:/data
+ - caddy_config:/config
+ networks:
+ - mcpnet
+ depends_on:
+ - gateway
+
+ gateway:
+ # Same configuration as Nginx example
+ image: ghcr.io/ibm/mcp-context-forge:latest
+ environment:
+ - MCP_CLIENT_AUTH_ENABLED=false
+ - TRUST_PROXY_AUTH=true
+ - PROXY_USER_HEADER=X-SSL-Client-S-DN-CN
+ # ... rest of config ...
+ networks:
+ - mcpnet
+
+volumes:
+ caddy_data:
+ caddy_config:
+
+networks:
+ mcpnet:
+ driver: bridge
+```
+
+## Production Configuration
+
+### Enterprise PKI Integration
+
+For production deployments, integrate with your enterprise PKI:
+
+```nginx
+# nginx.conf - Enterprise PKI
+server {
+ listen 443 ssl;
+
+ # Server certificates from enterprise CA
+ ssl_certificate /etc/pki/tls/certs/gateway.crt;
+ ssl_certificate_key /etc/pki/tls/private/gateway.key;
+
+ # Client CA chain
+ ssl_client_certificate /etc/pki/tls/certs/enterprise-ca-chain.crt;
+ ssl_verify_client on;
+ ssl_verify_depth 3;
+
+ # CRL verification
+ ssl_crl /etc/pki/tls/crl/enterprise.crl;
+
+ # OCSP stapling
+ ssl_stapling on;
+ ssl_stapling_verify on;
+ ssl_trusted_certificate /etc/pki/tls/certs/enterprise-ca-chain.crt;
+
+ location / {
+ proxy_pass http://mcp-gateway:4444;
+
+ # Extract user from certificate DN
+ if ($ssl_client_s_dn ~ /CN=([^\/]+)/) {
+ set $cert_cn $1;
+ }
+ proxy_set_header X-Authenticated-User $cert_cn;
+
+ # Extract organization
+ if ($ssl_client_s_dn ~ /O=([^\/]+)/) {
+ set $cert_org $1;
+ }
+ proxy_set_header X-User-Organization $cert_org;
+ }
+}
+```
+
+### Kubernetes Deployment Options
+
+### Option 1: Helm Chart with TLS Ingress
+
+The MCP Gateway Helm chart (`charts/mcp-stack`) includes built-in TLS support via Ingress:
+
+```bash
+# Install with TLS enabled
+helm install mcp-gateway ./charts/mcp-stack \
+ --set mcpContextForge.ingress.enabled=true \
+ --set mcpContextForge.ingress.host=gateway.example.com \
+ --set mcpContextForge.ingress.tls.enabled=true \
+ --set mcpContextForge.ingress.tls.secretName=gateway-tls \
+ --set mcpContextForge.ingress.annotations."cert-manager\.io/cluster-issuer"=letsencrypt-prod \
+ --set mcpContextForge.ingress.annotations."nginx.ingress.kubernetes.io/auth-tls-secret"=mcp-system/gateway-client-ca \
+ --set mcpContextForge.ingress.annotations."nginx.ingress.kubernetes.io/auth-tls-verify-client"=on \
+ --set mcpContextForge.ingress.annotations."nginx.ingress.kubernetes.io/auth-tls-verify-depth"="2" \
+ --set mcpContextForge.ingress.annotations."nginx.ingress.kubernetes.io/auth-tls-pass-certificate-to-upstream"="true"
+```
+
+
+> ℹ️ The configuration snippet that forwards the client CN is easier to maintain in `values.yaml`; the one-liner above focuses on core flags.
+
+Or configure via `values.yaml`:
+
+```yaml
+# charts/mcp-stack/values.yaml excerpt
+mcpContextForge:
+ ingress:
+ enabled: true
+ className: nginx
+ host: gateway.example.com
+ annotations:
+ cert-manager.io/cluster-issuer: letsencrypt-prod
+ nginx.ingress.kubernetes.io/auth-tls-secret: mcp-system/gateway-client-ca
+ nginx.ingress.kubernetes.io/auth-tls-verify-client: "on"
+ nginx.ingress.kubernetes.io/auth-tls-verify-depth: "2"
+ nginx.ingress.kubernetes.io/auth-tls-pass-certificate-to-upstream: "true"
+ nginx.ingress.kubernetes.io/configuration-snippet: |
+ proxy_set_header X-SSL-Client-S-DN $ssl_client_s_dn;
+ proxy_set_header X-SSL-Client-S-DN-CN $ssl_client_s_dn_cn;
+ proxy_set_header X-Authenticated-User $ssl_client_s_dn_cn;
+ tls:
+ enabled: true
+ secretName: gateway-tls # cert-manager will generate this
+
+ secret:
+ MCP_CLIENT_AUTH_ENABLED: "false"
+ TRUST_PROXY_AUTH: "true"
+ PROXY_USER_HEADER: X-SSL-Client-S-DN-CN
+```
+
+Create the `gateway-client-ca` secret in the same namespace as the release so the Ingress controller can validate client certificates. For example:
+
+```bash
+kubectl create secret generic gateway-client-ca \
+ --from-file=ca.crt=certs/mtls/ca.crt \
+ --namespace mcp-system
+```
+
+### Option 2: Kubernetes with Istio mTLS
+
+Deploy MCP Gateway with automatic mTLS in Istio service mesh:
+
+```yaml
+# gateway-deployment.yaml
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: mcp-gateway
+ namespace: mcp-system
+spec:
+ template:
+ metadata:
+ labels:
+ app: mcp-gateway
+ annotations:
+ sidecar.istio.io/inject: "true"
+ spec:
+ containers:
+ - name: mcp-gateway
+ image: ghcr.io/ibm/mcp-context-forge:latest
+ env:
+ - name: MCP_CLIENT_AUTH_ENABLED
+ value: "false"
+ - name: TRUST_PROXY_AUTH
+ value: "true"
+ - name: PROXY_USER_HEADER
+ value: "X-SSL-Client-S-DN-CN"
+---
+# peer-authentication.yaml
+apiVersion: security.istio.io/v1beta1
+kind: PeerAuthentication
+metadata:
+ name: mcp-gateway-mtls
+ namespace: mcp-system
+spec:
+ selector:
+ matchLabels:
+ app: mcp-gateway
+ mtls:
+ mode: STRICT
+```
+
+Istio does not add `X-SSL-Client-S-DN-CN` automatically. Use an `EnvoyFilter` to extract the client certificate common name and forward it as the header referenced by `PROXY_USER_HEADER`:
+
+```yaml
+# envoy-filter-client-cn.yaml
+apiVersion: networking.istio.io/v1alpha3
+kind: EnvoyFilter
+metadata:
+ name: append-client-cn-header
+ namespace: mcp-system
+spec:
+ workloadSelector:
+ labels:
+ app: mcp-gateway
+ configPatches:
+ - applyTo: HTTP_FILTER
+ match:
+ context: SIDECAR_INBOUND
+ listener:
+ portNumber: 4444
+ filterChain:
+ filter:
+ name: envoy.filters.network.http_connection_manager
+ patch:
+ operation: INSERT_BEFORE
+ value:
+ name: envoy.filters.http.lua
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
+ inlineCode: |
+ function envoy_on_request(handle)
+ local ssl = handle:streamInfo():downstreamSslConnection()
+ if ssl ~= nil and ssl:peerCertificatePresented() then
+ local subject = ssl:subjectPeerCertificate()
+ if subject ~= nil then
+ local cn = subject:match("CN=([^,/]+)")
+ if cn ~= nil then
+ handle:headers():replace("X-SSL-Client-S-DN-CN", cn)
+ end
+ end
+ end
+ end
+ function envoy_on_response(handle)
+ end
+```
+
+The filter runs in the sidecar and ensures the gateway receives the client's common name rather than the full certificate payload.
+
+### HAProxy with mTLS
+
+```haproxy
+# haproxy.cfg
+global
+ ssl-default-bind-options ssl-min-ver TLSv1.2
+ tune.ssl.default-dh-param 2048
+
+frontend mcp_gateway_mtls
+ bind *:443 ssl crt /etc/haproxy/certs/server.pem ca-file /etc/haproxy/certs/ca.crt verify required
+
+ # Extract certificate information
+ http-request set-header X-SSL-Client-Cert %[ssl_c_der,base64]
+ http-request set-header X-SSL-Client-S-DN %[ssl_c_s_dn]
+ http-request set-header X-SSL-Client-S-DN-CN %[ssl_c_s_dn(CN)]
+ http-request set-header X-Authenticated-User %[ssl_c_s_dn(CN)]
+
+ default_backend mcp_gateway_backend
+
+backend mcp_gateway_backend
+ server gateway gateway:4444 check
+```
+
+## Certificate Management
+
+### Certificate Generation Scripts
+
+Create a script for certificate management (`generate-certs.sh`):
+
+```bash
+#!/bin/bash
+set -e
+
+CERT_DIR="${CERT_DIR:-./certs/mtls}"
+CA_DAYS="${CA_DAYS:-3650}"
+CERT_DAYS="${CERT_DAYS:-365}"
+KEY_SIZE="${KEY_SIZE:-4096}"
+
+mkdir -p "$CERT_DIR"
+
+# Generate CA if it doesn't exist
+if [ ! -f "$CERT_DIR/ca.crt" ]; then
+ echo "Generating CA certificate..."
+ openssl req -x509 -newkey rsa:$KEY_SIZE -days $CA_DAYS -nodes \
+ -keyout "$CERT_DIR/ca.key" -out "$CERT_DIR/ca.crt" \
+ -subj "/C=US/ST=State/L=City/O=Organization/CN=MCP CA"
+ echo "CA certificate generated."
+fi
+
+# Function to generate certificates
+generate_cert() {
+ local name=$1
+ local cn=$2
+
+ if [ -f "$CERT_DIR/${name}.crt" ]; then
+ echo "Certificate for $name already exists, skipping..."
+ return
+ fi
+
+ echo "Generating certificate for $name (CN=$cn)..."
+
+ # Generate private key and CSR
+ openssl req -newkey rsa:$KEY_SIZE -nodes \
+ -keyout "$CERT_DIR/${name}.key" -out "$CERT_DIR/${name}.csr" \
+ -subj "/CN=$cn"
+
+ # Sign with CA
+ openssl x509 -req -in "$CERT_DIR/${name}.csr" \
+ -CA "$CERT_DIR/ca.crt" -CAkey "$CERT_DIR/ca.key" \
+ -CAcreateserial -out "$CERT_DIR/${name}.crt" -days $CERT_DAYS \
+ -extfile <(echo "subjectAltName=DNS:$cn")
+
+ # Create bundle
+ cat "$CERT_DIR/${name}.crt" "$CERT_DIR/${name}.key" > "$CERT_DIR/${name}.pem"
+
+ # Clean up CSR
+ rm "$CERT_DIR/${name}.csr"
+
+ echo "Certificate for $name generated."
+}
+
+# Generate server certificate
+generate_cert "server" "gateway.local"
+
+# Generate client certificates
+generate_cert "admin" "admin@example.com"
+generate_cert "user1" "user1@example.com"
+generate_cert "service-account" "mcp-service@example.com"
+
+echo "All certificates generated in $CERT_DIR"
+```
+
+### Certificate Rotation
+
+Implement automatic certificate rotation:
+
+```yaml
+# kubernetes CronJob for cert rotation
+apiVersion: batch/v1
+kind: CronJob
+metadata:
+ name: cert-rotation
+ namespace: mcp-system
+spec:
+ schedule: "0 2 * * *" # Daily at 2 AM
+ jobTemplate:
+ spec:
+ template:
+ spec:
+ serviceAccountName: cert-rotation
+ containers:
+ - name: cert-rotator
+ image: bitnami/kubectl:1.30
+ command:
+ - /bin/sh
+ - -c
+ - |
+ set -euo pipefail
+ SECRET_NAME=${CERT_SECRET:-gateway-tls}
+ CERT_NAME=${CERT_NAME:-gateway-tls-cert}
+ NAMESPACE=${TARGET_NAMESPACE:-mcp-system}
+ TLS_CERT=$(kubectl get secret "$SECRET_NAME" -n "$NAMESPACE" -o jsonpath='{.data.tls\.crt}')
+ if [ -z "$TLS_CERT" ]; then
+ echo "TLS secret $SECRET_NAME missing or empty"
+ exit 1
+ fi
+ echo "$TLS_CERT" | base64 -d > /tmp/current.crt
+ if openssl x509 -checkend 604800 -noout -in /tmp/current.crt; then
+ echo "Certificate valid for more than 7 days"
+ else
+ echo "Certificate expiring soon, requesting renewal"
+ kubectl cert-manager renew "$CERT_NAME" -n "$NAMESPACE" || echo "Install the kubectl-cert_manager plugin inside the job image to enable automatic renewal"
+ fi
+ env:
+ - name: CERT_SECRET
+ value: gateway-tls
+ - name: CERT_NAME
+ value: gateway-tls-cert
+ - name: TARGET_NAMESPACE
+ value: mcp-system
+ volumeMounts:
+ - name: tmp
+ mountPath: /tmp
+ restartPolicy: OnFailure
+ volumes:
+ - name: tmp
+ emptyDir: {}
+```
+
+Create a `ServiceAccount`, `Role`, and `RoleBinding` that grant `get` access to the TLS secret and `update` access to the related `Certificate` resource so the job can request renewals.
+
+
+> 🔧 Install the [`kubectl-cert_manager` plugin](https://cert-manager.io/docs/reference/kubectl-plugin/) or swap the command for `cmctl renew` if you prefer Jetstack's CLI image, and ensure your job image bundles both `kubectl` and `openssl`.
+
+## mTLS for External MCP Plugins
+
+External plugins that use the `STREAMABLEHTTP` transport now support mutual TLS directly from the gateway. This is optional—if you skip the configuration below, the gateway continues to call plugins exactly as before. Enabling mTLS lets you restrict remote plugin servers so they only accept connections from gateways presenting a trusted client certificate.
+
+### 1. Issue Certificates for the Remote Plugin
+
+Reuse the same CA you generated earlier or provision a dedicated one. Create a **server** certificate for the remote plugin endpoint and a **client** certificate for the MCP Gateway:
+
+```bash
+# Server cert for the remote plugin (served by your reverse proxy/mcp server)
+openssl req -newkey rsa:4096 -nodes \
+ -keyout certs/plugins/remote.key -out certs/plugins/remote.csr \
+ -subj "/CN=plugins.internal.example.com"
+
+openssl x509 -req -in certs/plugins/remote.csr \
+ -CA certs/mtls/ca.crt -CAkey certs/mtls/ca.key \
+ -CAcreateserial -out certs/plugins/remote.crt -days 365 \
+ -extfile <(echo "subjectAltName=DNS:plugins.internal.example.com")
+
+# Client cert for the gateway
+openssl req -newkey rsa:4096 -nodes \
+ -keyout certs/plugins/gateway-client.key -out certs/plugins/gateway-client.csr \
+ -subj "/CN=mcpgateway"
+
+openssl x509 -req -in certs/plugins/gateway-client.csr \
+ -CA certs/mtls/ca.crt -CAkey certs/mtls/ca.key \
+ -CAcreateserial -out certs/plugins/gateway-client.crt -days 365
+
+cat certs/plugins/gateway-client.crt certs/plugins/gateway-client.key > certs/plugins/gateway-client.pem
+```
+
+### 2. Protect the Remote Plugin with mTLS
+
+Front the remote MCP plugin with a reverse proxy (Nginx, Caddy, Envoy, etc.) that enforces client certificate verification using the CA above. Example Nginx snippet:
+
+```nginx
+server {
+ listen 9443 ssl;
+ server_name plugins.internal.example.com;
+
+ ssl_certificate /etc/ssl/private/remote.crt;
+ ssl_certificate_key /etc/ssl/private/remote.key;
+ ssl_client_certificate /etc/ssl/private/ca.crt;
+ ssl_verify_client on;
+
+ location /mcp {
+ proxy_pass http://plugin-runtime:8000/mcp;
+ proxy_set_header Host $host;
+ proxy_set_header X-Forwarded-Proto https;
+ }
+}
+```
+
+### 3. Mount Certificates into the Gateway
+
+Expose the CA bundle and gateway client certificate to the gateway container:
+
+```yaml
+# docker-compose override
+ gateway:
+ volumes:
+ - ./certs/plugins:/app/certs/plugins:ro
+
+# Kubernetes deployment (snippet)
+volumeMounts:
+ - name: plugin-mtls
+ mountPath: /app/certs/plugins
+ readOnly: true
+volumes:
+ - name: plugin-mtls
+ secret:
+ secretName: gateway-plugin-mtls
+```
+
+### 4. Configure the Plugin Entry
+
+Use the new `mcp.tls` block in `plugins/config.yaml` (or the Admin UI) to point the gateway at the certificates. Example external plugin definition:
+
+```yaml
+plugins:
+ - name: "LlamaGuardSafety"
+ kind: "external"
+ hooks: ["prompt_pre_fetch", "tool_pre_invoke"]
+ mode: "enforce"
+ priority: 20
+ mcp:
+ proto: STREAMABLEHTTP
+ url: https://plugins.internal.example.com:9443/mcp
+ tls:
+ ca_bundle: /app/certs/plugins/ca.crt
+ client_cert: /app/certs/plugins/gateway-client.pem
+ client_key: /app/certs/plugins/gateway-client.key # optional if PEM already bundles key
+ verify: true
+ check_hostname: true
+
+ config:
+ policy: strict
+```
+
+**Key behavior**
+- `verify` controls whether the gateway validates the remote server certificate. Leave `true` in production; set `false` only for local debugging.
+- `ca_bundle` may point to a custom CA chain; omit it if the remote certificate chains to a system-trusted CA.
+- `client_cert` must reference the gateway certificate. Provide `client_key` only when the key is stored separately.
+- `check_hostname` defaults to `true`. Set it to `false` for scenarios where the certificate CN does not match the URL (not recommended outside testing).
+
+Restart the gateway after updating the config so the external plugin client reloads with the TLS settings. Watch the logs for `Connected to plugin MCP (http) server` to confirm a successful handshake; TLS errors will surface as plugin initialization failures.
+
+> 💡 **Tip:** You can set gateway-wide defaults via `PLUGINS_MTLS_CA_BUNDLE`,
+> `PLUGINS_MTLS_CLIENT_CERT`, `PLUGINS_MTLS_CLIENT_KEY`, and the other
+> `PLUGINS_MTLS_*` environment variables. Any plugin without an explicit
+> `tls` block will inherit these values automatically.
+
+
+## Security Best Practices
+
+### 1. Certificate Validation
+
+```nginx
+# Strict certificate validation
+ssl_verify_client on;
+ssl_verify_depth 2;
+
+# Check certificate validity
+ssl_session_cache shared:SSL:10m;
+ssl_session_timeout 10m;
+
+# Enable OCSP stapling
+ssl_stapling on;
+ssl_stapling_verify on;
+resolver 8.8.8.8 8.8.4.4 valid=300s;
+resolver_timeout 5s;
+```
+
+### 2. Certificate Pinning
+
+```python
+# MCP Gateway plugin for cert pinning
+class CertificatePinningPlugin:
+ def __init__(self):
+ self.pinned_certs = {
+ "admin@example.com": "sha256:HASH...",
+ "service@example.com": "sha256:HASH..."
+ }
+
+ async def on_request(self, request):
+ cert_header = request.headers.get("X-SSL-Client-Cert")
+ if cert_header:
+ cert_hash = self.calculate_hash(cert_header)
+ user = request.headers.get("X-Authenticated-User")
+
+ if user in self.pinned_certs:
+ if self.pinned_certs[user] != cert_hash:
+ raise SecurityException("Certificate pin mismatch")
+```
+
+### 3. Audit Logging
+
+Configure comprehensive audit logging for mTLS connections:
+
+```nginx
+# nginx.conf - Audit logging
+log_format mtls_audit '$remote_addr - $ssl_client_s_dn [$time_local] '
+ '"$request" $status $body_bytes_sent '
+ '"$http_user_agent" cert_verify:$ssl_client_verify';
+
+access_log /var/log/nginx/mtls-audit.log mtls_audit;
+```
+
+### 4. Rate Limiting by Certificate
+
+```nginx
+# Rate limit by certificate CN
+limit_req_zone $ssl_client_s_dn_cn zone=cert_limit:10m rate=10r/s;
+
+location / {
+ limit_req zone=cert_limit burst=20 nodelay;
+ proxy_pass http://mcp-gateway;
+}
+```
+
+## Monitoring & Troubleshooting
+
+### Health Checks
+
+```bash
+# Check mTLS connectivity
+openssl s_client -connect gateway.local:443 \
+ -cert certs/mtls/client.crt \
+ -key certs/mtls/client.key \
+ -CAfile certs/mtls/ca.crt \
+ -showcerts
+
+# Verify certificate
+openssl x509 -in certs/mtls/client.crt -text -noout
+
+# Test with curl
+curl -v --cert certs/mtls/client.pem \
+ --cacert certs/mtls/ca.crt \
+ https://gateway.local/health
+```
+
+### Common Issues
+
+| Issue | Cause | Solution |
+|-------|-------|----------|
+| `SSL certificate verify error` | Missing/invalid client cert | Ensure client cert is valid and signed by CA |
+| `400 No required SSL certificate` | mTLS not configured | Check `ssl_verify_client on` in proxy |
+| `X-Authenticated-User missing` | Header not passed | Verify proxy_set_header configuration |
+| `Connection refused` | Service not running | Check docker-compose logs |
+| `Certificate expired` | Cert past validity | Regenerate certificates |
+
+### Debug Logging
+
+Enable debug logging in your reverse proxy:
+
+```nginx
+# nginx.conf
+error_log /var/log/nginx/error.log debug;
+
+# Log SSL handshake details
+ssl_session_cache shared:SSL:10m;
+ssl_session_timeout 10m;
+```
+
+## Migration Path
+
+### From JWT to mTLS
+
+1. **Phase 1**: Deploy proxy with mTLS alongside existing JWT auth
+2. **Phase 2**: Run dual-mode (both JWT and mTLS accepted)
+3. **Phase 3**: Migrate all clients to certificates
+4. **Phase 4**: Disable JWT, enforce mTLS only
+
+```yaml
+# Dual-mode configuration
+environment:
+ # Accept both methods during migration
+ - MCP_CLIENT_AUTH_ENABLED=true # Keep JWT active
+ - TRUST_PROXY_AUTH=true # Also trust proxy
+ - PROXY_USER_HEADER=X-SSL-Client-S-DN-CN
+```
+
+## Helm Chart Configuration
+
+The MCP Gateway Helm chart in `charts/mcp-stack/` provides extensive configuration options for TLS and security:
+
+### Key Security Settings in values.yaml
+
+```yaml
+mcpContextForge:
+ # JWT Configuration - supports both HMAC and RSA
+ secret:
+ JWT_ALGORITHM: HS256 # or RS256 for asymmetric
+ JWT_SECRET_KEY: my-test-key # for HMAC algorithms
+ # For RSA/ECDSA, mount keys and set:
+ # JWT_PUBLIC_KEY_PATH: /app/certs/jwt/public.pem
+ # JWT_PRIVATE_KEY_PATH: /app/certs/jwt/private.pem
+
+ # Security Headers (enabled by default)
+ config:
+ SECURITY_HEADERS_ENABLED: "true"
+ X_FRAME_OPTIONS: DENY
+ HSTS_ENABLED: "true"
+ HSTS_MAX_AGE: "31536000"
+ SECURE_COOKIES: "true"
+
+ # Ingress with TLS
+ ingress:
+ enabled: true
+ tls:
+ enabled: true
+ secretName: gateway-tls
+```
+
+### Deploying with Helm and mTLS
+
+```bash
+# Create namespace
+kubectl create namespace mcp-gateway
+
+# Install with custom TLS settings
+helm install mcp-gateway ./charts/mcp-stack \
+ --namespace mcp-gateway \
+ --set mcpContextForge.ingress.tls.enabled=true \
+ --set mcpContextForge.secret.JWT_ALGORITHM=RS256 \
+ --values custom-values.yaml
+```
+
+## Future Native mTLS Support
+
+When native mTLS support lands ([#568](https://github.com/IBM/mcp-context-forge/issues/568)), expect:
+
+- Direct TLS termination in MCP Gateway
+- Certificate-based authorization policies
+- Integration with enterprise PKI systems
+- Built-in certificate validation and revocation checking
+- Automatic certificate rotation
+- Per-service certificate management
+
+## Related Documentation
+
+- [Proxy Authentication](./proxy.md) - Configuring proxy-based authentication
+- [Security Features](../architecture/security-features.md) - Overall security architecture
+- [Deployment Guide](../deployment/index.md) - Production deployment options
+- [Authentication Overview](./securing.md) - All authentication methods
diff --git a/llms/plugins-llms.md b/llms/plugins-llms.md
index 2a1543180..c2a16c353 100644
--- a/llms/plugins-llms.md
+++ b/llms/plugins-llms.md
@@ -116,9 +116,12 @@ Plugins: How They Work in MCP Context Forge
- name: "MyFilter"
kind: "external"
priority: 10
- mcp:
- proto: STREAMABLEHTTP
- url: http://localhost:8000/mcp
+ mcp:
+ proto: STREAMABLEHTTP
+ url: http://localhost:8000/mcp
+ # tls:
+ # ca_bundle: /app/certs/plugins/ca.crt
+ # client_cert: /app/certs/plugins/gateway-client.pem
```
- STDIO alternative:
```yaml
@@ -129,7 +132,7 @@ Plugins: How They Work in MCP Context Forge
proto: STDIO
script: path/to/server.py
```
-- Enable framework in gateway: `.env` must set `PLUGINS_ENABLED=true` and optionally `PLUGIN_CONFIG_FILE=plugins/config.yaml`.
+- Enable framework in gateway: `.env` must set `PLUGINS_ENABLED=true` and optionally `PLUGIN_CONFIG_FILE=plugins/config.yaml`. To reuse a gateway-wide mTLS client certificate for multiple external plugins, set `PLUGINS_MTLS_CA_BUNDLE`, `PLUGINS_MTLS_CLIENT_CERT`, and related `PLUGINS_MTLS_*` variables. Individual plugin `tls` blocks override these defaults.
**Built‑in Plugins (Examples)**
- `ArgumentNormalizer` (`plugins/argument_normalizer/argument_normalizer.py`)
diff --git a/mcpgateway/plugins/framework/external/mcp/client.py b/mcpgateway/plugins/framework/external/mcp/client.py
index 7facb160e..6b3d6a810 100644
--- a/mcpgateway/plugins/framework/external/mcp/client.py
+++ b/mcpgateway/plugins/framework/external/mcp/client.py
@@ -14,9 +14,11 @@
import json
import logging
import os
+import ssl
from typing import Any, Optional, Type, TypeVar
# Third-Party
+import httpx
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from mcp.client.streamable_http import streamablehttp_client
@@ -28,6 +30,7 @@
from mcpgateway.plugins.framework.errors import convert_exception_to_error, PluginError
from mcpgateway.plugins.framework.models import (
HookType,
+ MCPTransportTLSConfig,
PluginConfig,
PluginContext,
PluginErrorModel,
@@ -144,8 +147,54 @@ async def __connect_to_http_server(self, uri: str) -> None:
PluginError: if there is an external connection error.
"""
+ plugin_tls = self._config.mcp.tls if self._config and self._config.mcp else None
+ tls_config = plugin_tls or MCPTransportTLSConfig.from_env()
+
+ def _tls_httpx_client_factory(
+ headers: Optional[dict[str, str]] = None,
+ timeout: Optional[httpx.Timeout] = None,
+ auth: Optional[httpx.Auth] = None,
+ ) -> httpx.AsyncClient:
+ """Build an httpx client with TLS configuration for external MCP servers."""
+
+ kwargs: dict[str, Any] = {"follow_redirects": True}
+ if headers:
+ kwargs["headers"] = headers
+ kwargs["timeout"] = timeout or httpx.Timeout(30.0)
+ if auth is not None:
+ kwargs["auth"] = auth
+
+ if not tls_config:
+ return httpx.AsyncClient(**kwargs)
+
+ try:
+ ssl_context = ssl.create_default_context()
+ if not tls_config.verify:
+ ssl_context.check_hostname = False
+ ssl_context.verify_mode = ssl.CERT_NONE
+ else:
+ if tls_config.ca_bundle:
+ ssl_context.load_verify_locations(cafile=tls_config.ca_bundle)
+ if not tls_config.check_hostname:
+ ssl_context.check_hostname = False
+
+ if tls_config.client_cert:
+ ssl_context.load_cert_chain(
+ certfile=tls_config.client_cert,
+ keyfile=tls_config.client_key,
+ password=tls_config.client_key_password,
+ )
+
+ kwargs["verify"] = ssl_context
+ except Exception as exc: # pylint: disable=broad-except
+ raise PluginError(error=PluginErrorModel(message=f"Failed configuring TLS for external plugin: {exc}", plugin_name=self.name)) from exc
+
+ return httpx.AsyncClient(**kwargs)
+
try:
- http_transport = await self._exit_stack.enter_async_context(streamablehttp_client(uri))
+ client_factory = _tls_httpx_client_factory if tls_config else None
+ streamable_client = streamablehttp_client(uri, httpx_client_factory=client_factory) if client_factory else streamablehttp_client(uri)
+ http_transport = await self._exit_stack.enter_async_context(streamable_client)
self._http, self._write, _ = http_transport
self._session = await self._exit_stack.enter_async_context(ClientSession(self._http, self._write))
diff --git a/mcpgateway/plugins/framework/models.py b/mcpgateway/plugins/framework/models.py
index 85950b1ce..344827965 100644
--- a/mcpgateway/plugins/framework/models.py
+++ b/mcpgateway/plugins/framework/models.py
@@ -11,6 +11,7 @@
# Standard
from enum import Enum
+import os
from pathlib import Path
from typing import Any, Generic, Optional, Self, TypeVar
@@ -246,6 +247,88 @@ class AppliedTo(BaseModel):
resources: Optional[list[ResourceTemplate]] = None
+class MCPTransportTLSConfig(BaseModel):
+ """TLS configuration for HTTP-based MCP transports.
+
+ Attributes:
+ verify (bool): Whether to verify the remote certificate chain.
+ ca_bundle (Optional[str]): Path to a CA bundle file used for verification.
+ client_cert (Optional[str]): Path to the PEM-encoded client certificate (can include key).
+ client_key (Optional[str]): Path to the PEM-encoded client private key when stored separately.
+ client_key_password (Optional[str]): Optional password for the private key file.
+ check_hostname (bool): Enable hostname verification (default: True).
+ """
+
+ verify: bool = Field(default=True, description="Verify the upstream server certificate")
+ ca_bundle: Optional[str] = Field(default=None, description="Path to CA bundle for upstream verification")
+ client_cert: Optional[str] = Field(default=None, description="Path to PEM client certificate or bundle")
+ client_key: Optional[str] = Field(default=None, description="Path to PEM client private key (if separate)")
+ client_key_password: Optional[str] = Field(default=None, description="Password for the client key, when encrypted")
+ check_hostname: bool = Field(default=True, description="Enable hostname verification when verify is true")
+
+ @field_validator("ca_bundle", "client_cert", "client_key", mode=AFTER)
+ @classmethod
+ def validate_path(cls, value: Optional[str]) -> Optional[str]:
+ """Expand and validate file paths supplied in TLS configuration."""
+
+ if not value:
+ return value
+ expanded = Path(value).expanduser()
+ if not expanded.is_file():
+ raise ValueError(f"TLS file path does not exist: {value}")
+ return str(expanded)
+
+ @model_validator(mode=AFTER)
+ def validate_client_cert(self) -> Self: # pylint: disable=bad-classmethod-argument
+ """Ensure TLS client certificate options are consistent."""
+
+ if self.client_key and not self.client_cert:
+ raise ValueError("client_key requires client_cert to be specified")
+ return self
+
+ @staticmethod
+ def _parse_bool(value: Optional[str]) -> Optional[bool]:
+ """Convert a string environment value to boolean."""
+
+ if value is None:
+ return None
+ normalized = value.strip().lower()
+ if normalized in {"1", "true", "yes", "on"}:
+ return True
+ if normalized in {"0", "false", "no", "off"}:
+ return False
+ raise ValueError(f"Invalid boolean value: {value}")
+
+ @classmethod
+ def from_env(cls) -> Optional["MCPTransportTLSConfig"]:
+ """Construct a TLS configuration from environment defaults."""
+
+ env = os.environ
+ data: dict[str, Any] = {}
+
+ if env.get("PLUGINS_MTLS_CA_BUNDLE"):
+ data["ca_bundle"] = env["PLUGINS_MTLS_CA_BUNDLE"]
+ if env.get("PLUGINS_MTLS_CLIENT_CERT"):
+ data["client_cert"] = env["PLUGINS_MTLS_CLIENT_CERT"]
+ if env.get("PLUGINS_MTLS_CLIENT_KEY"):
+ data["client_key"] = env["PLUGINS_MTLS_CLIENT_KEY"]
+ if env.get("PLUGINS_MTLS_CLIENT_KEY_PASSWORD") is not None:
+ data["client_key_password"] = env["PLUGINS_MTLS_CLIENT_KEY_PASSWORD"]
+
+ verify_val = cls._parse_bool(env.get("PLUGINS_MTLS_VERIFY"))
+ if verify_val is not None:
+ data["verify"] = verify_val
+
+ check_hostname_val = cls._parse_bool(env.get("PLUGINS_MTLS_CHECK_HOSTNAME"))
+ if check_hostname_val is not None:
+ data["check_hostname"] = check_hostname_val
+
+ if not data:
+ return None
+
+ return cls(**data)
+
+
class MCPConfig(BaseModel):
"""An MCP configuration for external MCP plugin objects.
@@ -258,6 +341,7 @@ class MCPConfig(BaseModel):
proto: TransportType
url: Optional[str] = None
script: Optional[str] = None
+ tls: Optional[MCPTransportTLSConfig] = None
@field_validator(URL, mode=AFTER)
@classmethod
@@ -302,6 +386,14 @@ def validate_script(cls, script: str | None) -> str | None:
raise ValueError(f"MCP server script {script} must have a .py or .sh suffix.")
return script
+ @model_validator(mode=AFTER)
+ def validate_tls_usage(self) -> Self: # pylint: disable=bad-classmethod-argument
+ """Ensure TLS configuration is only used with HTTP-based transports."""
+
+ if self.tls and self.proto not in (TransportType.SSE, TransportType.STREAMABLEHTTP):
+ raise ValueError("TLS configuration is only valid for HTTP/SSE transports")
+ return self
+
class PluginConfig(BaseModel):
"""A plugin configuration.
diff --git a/plugins/config.yaml b/plugins/config.yaml
index e1a0ecb36..975825bd2 100644
--- a/plugins/config.yaml
+++ b/plugins/config.yaml
@@ -469,6 +469,10 @@ plugins:
# mcp:
# proto: STREAMABLEHTTP
# url: http://127.0.0.1:8000/mcp
+ # # tls:
+ # # ca_bundle: /app/certs/plugins/ca.crt
+ # # client_cert: /app/certs/plugins/gateway-client.pem
+ # # verify: true
# Circuit Breaker - trip on high error rates or consecutive failures
- name: "CircuitBreaker"
diff --git a/plugins/external/config.yaml b/plugins/external/config.yaml
index d9632f3a9..f4fa1a4fb 100644
--- a/plugins/external/config.yaml
+++ b/plugins/external/config.yaml
@@ -6,6 +6,9 @@ plugins:
mcp:
proto: STREAMABLEHTTP
url: http://127.0.0.1:3000/mcp
+ # tls:
+ # ca_bundle: /app/certs/plugins/ca.crt
+ # client_cert: /app/certs/plugins/gateway-client.pem
- name: "OPAPluginFilter"
kind: "external"
@@ -13,6 +16,8 @@ plugins:
mcp:
proto: STREAMABLEHTTP
url: http://127.0.0.1:8000/mcp
+ # tls:
+ # verify: true
# Plugin directories to scan
plugin_dirs:
diff --git a/tests/unit/mcpgateway/plugins/framework/test_models_tls.py b/tests/unit/mcpgateway/plugins/framework/test_models_tls.py
new file mode 100644
index 000000000..d61f8693a
--- /dev/null
+++ b/tests/unit/mcpgateway/plugins/framework/test_models_tls.py
@@ -0,0 +1,114 @@
+"""Tests for TLS configuration on external MCP plugins."""
+
+# Standard
+from pathlib import Path
+
+# Third-Party
+import pytest
+
+# First-Party
+from mcpgateway.plugins.framework.models import MCPTransportTLSConfig, PluginConfig
+
+
+def _write_pem(path: Path) -> str:
+ path.write_text("-----BEGIN CERTIFICATE-----\nMIIBszCCAVmgAwIBAgIJALICEFAKE000MA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV\nBAMMCXRlc3QtY2EwHhcNMjUwMTAxMDAwMDAwWhcNMjYwMTAxMDAwMDAwWjAUMRIw\nEAYDVQQDDAl0ZXN0LWNsaTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\nALzM8FSo48ByKC16ecEsPpRghr7kDDLOZWisS+8mHb4RLzdrg5e8tRgFuBlbslUT\n8VE+j54v+J2mOv5u18CVeq4xjp1IqP/PpeL9Z8sY2XohGKVCUj8lMiMM6trXwPh3\n4nDXwG8hxhTZWOeAZv93FqMgBANpUAOC0yM5Ar+uSoC2Tbf3juDEnHiVNWdP6hJg\n38zrla9Yh+SPYj9m6z6wG6jZc37SaJnKI/v4ycq31wkK7S226gRA7i72H+eEt1Kp\nI5rkJ+6kkfgeJc8FvbB6c88T9EycneEW7Pm2Xp6gJdxeN1g2jeDJPnWc5Cj9VPYU\nCJPwy6DnKSmGA4MZij19+cUCAwEAAaNQME4wHQYDVR0OBBYEFL0CyJXw5CtP6Ls9\nVgn8BxwysA2fMB8GA1UdIwQYMBaAFL0CyJXw5CtP6Ls9Vgn8BxwysA2fMAwGA1Ud\nEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAIgUjACmJS4cGL7yp0T1vpuZi856\nG7k18Om8Ze9fJbVI1MBBxDWS5F9bNOn5z1ytgCMs9VXg7QibQPXlqprcM2aYJWaV\ndHZ92ohqzJ0EB1G2r8x5Fkw3O0mEWcJvl10FgUVHVGzi552MZGFMZ7DAMA4EAq/u\nsOUgWup8uLSyvvl7dao3rJ8k+YkBWkDu6eCKwQn3nNKFB5Bg9P6IKkmDdLhYodl/\nW1q/qmHZapCp8XDsrmS8skWsmcFJFU6f4VDOwdJaNiMgRGQpWlwO4dRw9xvyhsHc\nsOf0HWNvw60sX6Zav8HC0FzDGhGJkpyyU10BzpQLVEf5AEE7MkK5eeqi2+0=\n-----END CERTIFICATE-----\n", encoding="utf-8")
+ return str(path)
+
+
+@pytest.mark.parametrize(
+ "verify",
+ [True, False],
+)
+def test_plugin_config_supports_tls_block(tmp_path, verify):
+ ca_path = Path(tmp_path) / "ca.crt"
+ client_bundle = Path(tmp_path) / "client.pem"
+ _write_pem(ca_path)
+ _write_pem(client_bundle)
+
+ config = PluginConfig(
+ name="ExternalTLSPlugin",
+ kind="external",
+ hooks=["prompt_pre_fetch"],
+ mcp={
+ "proto": "STREAMABLEHTTP",
+ "url": "https://plugins.internal.example.com/mcp",
+ "tls": {
+ "ca_bundle": str(ca_path),
+ "client_cert": str(client_bundle),
+ "verify": verify,
+ },
+ },
+ )
+
+ assert config.mcp is not None
+ assert config.mcp.tls is not None
+ assert config.mcp.tls.client_cert == str(client_bundle)
+ assert config.mcp.tls.verify == verify
+
+
+def test_plugin_config_tls_missing_cert_raises(tmp_path):
+ ca_path = Path(tmp_path) / "ca.crt"
+ _write_pem(ca_path)
+
+ with pytest.raises(ValueError):
+ PluginConfig(
+ name="ExternalTLSPlugin",
+ kind="external",
+ hooks=["prompt_pre_fetch"],
+ mcp={
+ "proto": "STREAMABLEHTTP",
+ "url": "https://plugins.internal.example.com/mcp",
+ "tls": {
+ "client_key": str(ca_path),
+ },
+ },
+ )
+
+
+def test_plugin_config_tls_missing_file(tmp_path):
+ missing_path = Path(tmp_path) / "missing.crt"
+
+ with pytest.raises(ValueError):
+ PluginConfig(
+ name="ExternalTLSPlugin",
+ kind="external",
+ hooks=["prompt_pre_fetch"],
+ mcp={
+ "proto": "STREAMABLEHTTP",
+ "url": "https://plugins.internal.example.com/mcp",
+ "tls": {
+ "ca_bundle": str(missing_path),
+ },
+ },
+ )
+
+
+def test_tls_config_from_env_defaults(monkeypatch, tmp_path):
+ ca_path = Path(tmp_path) / "ca.crt"
+ client_cert = Path(tmp_path) / "client.pem"
+ _write_pem(ca_path)
+ _write_pem(client_cert)
+
+ monkeypatch.setenv("PLUGINS_MTLS_CA_BUNDLE", str(ca_path))
+ monkeypatch.setenv("PLUGINS_MTLS_CLIENT_CERT", str(client_cert))
+ monkeypatch.setenv("PLUGINS_MTLS_VERIFY", "true")
+ monkeypatch.setenv("PLUGINS_MTLS_CHECK_HOSTNAME", "true")
+
+ tls_config = MCPTransportTLSConfig.from_env()
+
+ assert tls_config is not None
+ assert tls_config.ca_bundle == str(ca_path)
+ assert tls_config.client_cert == str(client_cert)
+ assert tls_config.verify is True
+ assert tls_config.check_hostname is True
+
+
+def test_tls_config_from_env_returns_none(monkeypatch):
+ monkeypatch.delenv("PLUGINS_MTLS_CA_BUNDLE", raising=False)
+ monkeypatch.delenv("PLUGINS_MTLS_CLIENT_CERT", raising=False)
+ monkeypatch.delenv("PLUGINS_MTLS_CLIENT_KEY", raising=False)
+ monkeypatch.delenv("PLUGINS_MTLS_CLIENT_KEY_PASSWORD", raising=False)
+ monkeypatch.delenv("PLUGINS_MTLS_VERIFY", raising=False)
+ monkeypatch.delenv("PLUGINS_MTLS_CHECK_HOSTNAME", raising=False)
+
+ assert MCPTransportTLSConfig.from_env() is None