diff --git a/README.md b/README.md index 3bb228d..a9089d9 100644 --- a/README.md +++ b/README.md @@ -483,7 +483,7 @@ The following environment variables are used to configure the ClickHouse and chD * `CLICKHOUSE_VERIFY`: Enable/disable SSL certificate verification * Default: `"true"` * Set to `"false"` to disable certificate verification (not recommended for production) - * TLS certificates: The package uses your operating system trust store for TLS certificate verification via `truststore`. We call `truststore.inject_into_ssl()` at startup to ensure proper certificate handling. Python’s default SSL behavior is used as a fallback only if an unexpected error occurs. + * TLS certificates: The package uses your operating system trust store for TLS certificate verification via `truststore`. We call `truststore.inject_into_ssl()` at startup to ensure proper certificate handling. Python's default SSL behavior is used as a fallback only if an unexpected error occurs. * `CLICKHOUSE_CONNECT_TIMEOUT`: Connection timeout in seconds * Default: `"30"` * Increase this value if you experience connection timeouts @@ -536,6 +536,30 @@ The following environment variables are used to configure the ClickHouse and chD * The module must provide a `setup_middleware(mcp)` function * See [Custom Middleware](#custom-middleware) for details and examples +##### mTLS (Mutual TLS) Variables + +These variables enable client certificate authentication for ClickHouse servers that require mutual TLS: + +* `CLICKHOUSE_CA_CERT`: Path to CA certificate file + * Default: None + * Set this to specify a custom CA certificate for SSL verification + * Example: `/path/to/ca.crt` +* `CLICKHOUSE_CLIENT_CERT`: Path to client certificate file + * Default: None + * Required for mTLS authentication + * Can be a `.pem` file containing both the certificate and private key + * Example: `/path/to/client.crt` or `/path/to/client.pem` +* `CLICKHOUSE_CLIENT_CERT_KEY`: Path to client private key file + * Default: None + * Optional if `CLICKHOUSE_CLIENT_CERT` is a `.pem` file containing both the certificate and private key + * Example: `/path/to/client.key` +* `CLICKHOUSE_TLS_MODE`: TLS mode for client certificate authentication + * Default: None (auto-detected based on `CLICKHOUSE_CLIENT_CERT`) + * Valid options: + * `"mutual"` - Use client certificate for authentication (default when `CLICKHOUSE_CLIENT_CERT` is set) + * `"proxy"` - TLS termination at proxy, use Basic Auth with client certs for TLS only + * `"strict"` - Strict TLS mode with Basic Auth + #### chDB Variables * `CHDB_ENABLED`: Enable/disable chDB functionality @@ -583,6 +607,55 @@ CLICKHOUSE_PASSWORD= # Uses secure defaults (HTTPS on port 8443) ``` +For ClickHouse with mTLS (Mutual TLS): + +```env +# Required variables +CLICKHOUSE_HOST=your-secure-clickhouse.example.com +CLICKHOUSE_PORT=8443 +CLICKHOUSE_USER=your-user +CLICKHOUSE_PASSWORD=your-password + +# mTLS configuration +CLICKHOUSE_SECURE=true +CLICKHOUSE_CA_CERT=/path/to/ca.crt +CLICKHOUSE_CLIENT_CERT=/path/to/client.crt +CLICKHOUSE_CLIENT_CERT_KEY=/path/to/client.key + +# Or if using a combined .pem file: +# CLICKHOUSE_CLIENT_CERT=/path/to/client.pem +``` + +Example Claude Desktop configuration with mTLS: + +```json +{ + "mcpServers": { + "mcp-clickhouse": { + "command": "uv", + "args": [ + "run", + "--with", + "mcp-clickhouse", + "--python", + "3.10", + "mcp-clickhouse" + ], + "env": { + "CLICKHOUSE_HOST": "your-secure-clickhouse.example.com", + "CLICKHOUSE_PORT": "8443", + "CLICKHOUSE_USER": "your-user", + "CLICKHOUSE_PASSWORD": "your-password", + "CLICKHOUSE_SECURE": "true", + "CLICKHOUSE_CA_CERT": "/path/to/ca.crt", + "CLICKHOUSE_CLIENT_CERT": "/path/to/client.crt", + "CLICKHOUSE_CLIENT_CERT_KEY": "/path/to/client.key" + } + } + } +} +``` + For chDB only (in-memory): ```env diff --git a/mcp_clickhouse/mcp_env.py b/mcp_clickhouse/mcp_env.py index 49d5e9b..a175023 100644 --- a/mcp_clickhouse/mcp_env.py +++ b/mcp_clickhouse/mcp_env.py @@ -45,6 +45,10 @@ class ClickHouseConfig: CLICKHOUSE_DATABASE: Default database to use (default: None) CLICKHOUSE_PROXY_PATH: Path to be added to the host URL. For instance, for servers behind an HTTP proxy (default: None) CLICKHOUSE_ENABLED: Enable ClickHouse server (default: true) + CLICKHOUSE_CA_CERT: Path to CA certificate file for SSL verification (default: None) + CLICKHOUSE_CLIENT_CERT: Path to client certificate file for mTLS authentication (default: None) + CLICKHOUSE_CLIENT_CERT_KEY: Path to client private key file for mTLS authentication (default: None) + CLICKHOUSE_TLS_MODE: TLS mode for client certificate usage - "mutual", "proxy", or "strict" (default: None) CLICKHOUSE_ALLOW_WRITE_ACCESS: Allow write operations (DDL and DML) (default: false) CLICKHOUSE_ALLOW_DROP: Allow destructive operations (DROP, TRUNCATE) when writes are also enabled (default: false) """ @@ -135,6 +139,44 @@ def proxy_path(self) -> str: return os.getenv("CLICKHOUSE_PROXY_PATH") @property + def ca_cert(self) -> Optional[str]: + """Get the path to CA certificate file for SSL verification. + + Default: None + """ + return os.getenv("CLICKHOUSE_CA_CERT") + + @property + def client_cert(self) -> Optional[str]: + """Get the path to client certificate file for mTLS authentication. + + Default: None + """ + return os.getenv("CLICKHOUSE_CLIENT_CERT") + + @property + def client_cert_key(self) -> Optional[str]: + """Get the path to client private key file for mTLS authentication. + + This is optional if the client_cert file contains both the certificate + and the private key (e.g., a combined .pem file). + + Default: None + """ + return os.getenv("CLICKHOUSE_CLIENT_CERT_KEY") + + @property + def tls_mode(self) -> Optional[str]: + """Get the TLS mode for client certificate usage. + + Valid values: + - 'mutual': Use client certificate for authentication (default when client_cert is set) + - 'proxy': TLS termination at proxy, use Basic Auth with client certs for TLS only + - 'strict': Strict TLS mode, use Basic Auth with client certs for TLS only + + Default: None (auto-detected by clickhouse-connect) + """ + return os.getenv("CLICKHOUSE_TLS_MODE") def allow_write_access(self) -> bool: """Get whether write operations (DDL and DML) are allowed. @@ -183,6 +225,19 @@ def get_client_config(self) -> dict: if self.proxy_path: config["proxy_path"] = self.proxy_path + # Add mTLS configuration if set + if self.ca_cert: + config["ca_cert"] = self.ca_cert + + if self.client_cert: + config["client_cert"] = self.client_cert + + if self.client_cert_key: + config["client_cert_key"] = self.client_cert_key + + if self.tls_mode: + config["tls_mode"] = self.tls_mode + return config def _validate_required_vars(self) -> None: