Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ Releases follow [Semantic Versioning](https://semver.org/).
- **secrets: `${file:/absolute/path}` references (#1706, spec 111 FR-030).** A secret can be read from a file, as Docker/Compose secrets, Kubernetes secret volumes and systemd `LoadCredential=` provide it, wherever `${env:}` and `${keyring:}` are accepted: an upstream server's `env`, `args` and `headers`, `data_dir`, and the proxy's own `api_key`. The path must be absolute, a symlink must lead to a regular file, at most 64 KiB is read, one trailing LF or CRLF is removed, and a missing, unreadable or empty file is an error that names the reference, never the content. A server shows a missing-secret error and the proxy stays up; an `api_key` reference that does not resolve stops startup (no key is generated over it). The stored config keeps the reference: the value is resolved on a copy, and the `api_key` secret is a process-only override that no save, including `PATCH /api/v1/config`, writes back. (refs #1706)

- **config: `MCPPROXY_API_KEY_FILE` reads the API key from a file (#1706).** The `*_FILE` form of Docker Compose/Swarm secrets, Kubernetes secret volumes and systemd `LoadCredential=`: the key is not in the process environment (`docker inspect`, `/proc/<pid>/environ`) and is never written to `mcp_config.json`. The path must be absolute; one trailing LF or CRLF is removed; at most 64 KiB is read. Setting both `MCPPROXY_API_KEY` and `MCPPROXY_API_KEY_FILE` (both non-empty) is an error: mcpproxy refuses to start with a message naming both variables (never a key or file contents), and the CLI commands and the tray stop the same way. An empty value is treated as unset. A file that is named but cannot be used stops startup with an error naming the variable and path: mcpproxy does not fall back to the config file or a generated key. `mcpproxy status`, the other CLI commands and the tray read it too, and a tray-launched core reads the file itself instead of receiving the key in its environment. `Config.EnsureAPIKey` now also returns an error. (refs #1706, spec 111 FR-014/FR-030/US7)
- **tls: serve a certificate for names other than `localhost` (#1719).** `tls.extra_sans` lists DNS names and IP addresses to add to the generated server certificate; when `localhost.pem` does not carry exactly those names plus `localhost`, `127.0.0.1` and `::1`, it is regenerated and signed by the same local CA, so `ca.pem` and `mcpproxy trust-cert` keep working. `tls.cert_file` and `tls.key_file` (absolute paths, both or neither) serve a certificate you provide instead, for a real or internal CA, `certbot` or a Docker/Kubernetes secret mount; nothing is generated, and a missing, unreadable or mismatched file stops startup with an error naming the setting. Environment variables `MCPPROXY_TLS_EXTRA_SANS`, `MCPPROXY_TLS_CERT_FILE` and `MCPPROXY_TLS_KEY_FILE`. Changing any of them needs a restart; with none set, startup is unchanged. Removes `MCPPROXY_TLS_CERT` and `MCPPROXY_TLS_KEY` from the environment-variable reference, which no code ever read.

- **catalog:** a catalog source whose live search times out or fails is now answered from the listing
the daemon last saw from it (at most 24 hours old, kept in memory). Those results are marked
Expand Down
43 changes: 41 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -1144,17 +1144,23 @@ The tokenizer automatically maps model names to encodings:
"enabled": false,
"require_client_cert": false,
"certs_dir": "",
"hsts": true
"hsts": true,
"extra_sans": [],
"cert_file": "",
"key_file": ""
}
}
```

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `enabled` | boolean | `false` | Enable HTTPS/TLS |
| `require_client_cert` | boolean | `false` | Enable mutual TLS (mTLS) for client authentication |
| `require_client_cert` | boolean | `false` | Enable mutual TLS (mTLS) for client authentication. Client certificates are verified against the local CA (`ca.pem` in `certs_dir`), also when `cert_file` is set |
| `certs_dir` | string | `""` | Custom certificate directory (defaults to `${data_dir}/certs`) |
| `hsts` | boolean | `true` | Enable HTTP Strict Transport Security headers |
| `extra_sans` | string array | `[]` | DNS names and IP addresses added to the generated server certificate, next to `localhost`, `127.0.0.1` and `::1`. See [Other host names](#other-host-names-extra_sans-cert_file-key_file) |
| `cert_file` | string | `""` | Absolute path of a server certificate (PEM) to serve instead of the generated one. Needs `key_file` |
| `key_file` | string | `""` | Absolute path of the private key (PEM) for `cert_file`. Needs `cert_file` |

**Quick Setup:**
1. Trust certificate: `mcpproxy trust-cert`
Expand All @@ -1163,6 +1169,36 @@ The tokenizer automatically maps model names to encodings:

See [Setup Guide - HTTPS](setup.md#optional-https-setup) for complete details.

### Other host names (`extra_sans`, `cert_file`, `key_file`)

The generated server certificate is valid for `localhost`, `127.0.0.1` and `::1` only. A client that reaches mcpproxy by another name (a Docker or Kubernetes service name, a VM host name, a tailnet name) cannot verify it. Either of these fixes that; without either, startup is unchanged.

**`extra_sans`**: names for the generated certificate.

```json
{ "tls": { "enabled": true, "extra_sans": ["mcp.internal", "mcpproxy.tail1234.ts.net", "10.0.0.5"] } }
```

- Each entry is a host name or an IP address. Wildcards (`*.example.com`), URLs, ports and empty entries are refused at startup with an error that names the entry. Host names are lower-cased; non-ASCII names must be given in punycode.
- When `localhost.pem` does not carry exactly `localhost`, `127.0.0.1`, `::1` and the listed names, it is replaced with a new certificate and key signed by the **same** CA. `ca.pem` and `ca.key` are never regenerated, so clients that trust `ca.pem` (`mcpproxy trust-cert`, `NODE_EXTRA_CA_CERTS`) keep working with no new step. A restart with an unchanged list rewrites nothing, and emptying the list replaces it with a plain `localhost` certificate (one the local CA did not sign is left as it is).
- With an empty list an existing `localhost.pem` is used as it is. If `localhost.pem` was not issued by the local CA and does not carry the listed names (for example one you placed there by hand), startup stops instead of overwriting it; use `cert_file` and `key_file` for such a certificate.
- The new certificate is valid for one year from the day it is written. There is no automatic renewal.
- Environment variable: `MCPPROXY_TLS_EXTRA_SANS` (comma-separated).

**`cert_file` and `key_file`**: serve a certificate you provide (a public or internal CA, `certbot`, a Docker or Kubernetes secret mount).

```json
{ "tls": { "enabled": true, "cert_file": "/run/secrets/mcpproxy.crt", "key_file": "/run/secrets/mcpproxy.key" } }
```

- Set both or neither, as absolute paths to PEM files. A certificate chain goes in `cert_file`, leaf first. The files are read with the same rules as `MCPPROXY_API_KEY_FILE`: a symlink is followed (Kubernetes secret volumes), the target must be a regular file of at most 64 KiB.
- If only one is set, a file cannot be read, or the key does not belong to the certificate, startup stops with an error that names the setting and never shows file content.
- Nothing is generated and `extra_sans` is refused alongside it, since the certificate you provide decides the names. The local CA is created only when `require_client_cert` is on, because client certificates are verified against it.
- The files are read once at startup. After the certificate is renewed, restart mcpproxy.
- Environment variables: `MCPPROXY_TLS_CERT_FILE` and `MCPPROXY_TLS_KEY_FILE`.

Changing any of the three settings needs a restart.

---

## Logging Configuration
Expand Down Expand Up @@ -1905,6 +1941,9 @@ Many configuration options can be overridden via environment variables:
| `MCPPROXY_TLS_ENABLED` | `tls.enabled` | Enable HTTPS/TLS |
| `MCPPROXY_TLS_REQUIRE_CLIENT_CERT` | `tls.require_client_cert` | Enable mTLS |
| `MCPPROXY_CERTS_DIR` | `tls.certs_dir` | Custom certificates directory |
| `MCPPROXY_TLS_EXTRA_SANS` | `tls.extra_sans` | Comma-separated host names and IP addresses added to the generated certificate |
| `MCPPROXY_TLS_CERT_FILE` | `tls.cert_file` | Absolute path of a server certificate to serve instead of the generated one |
| `MCPPROXY_TLS_KEY_FILE` | `tls.key_file` | Absolute path of the private key for `MCPPROXY_TLS_CERT_FILE` |
| `MCPPROXY_DATA_DIR` | `data_dir` | Override data directory; also where `serve` looks for `mcp_config.json` (same as `--data-dir`) |
| `MCPPROXY_DATA` | `data_dir` | Override data directory only (legacy name; `MCPPROXY_DATA_DIR` wins) |
| `MCPPROXY_TOOL_RESPONSE_MODE` | `tool_response_mode` | `retrieve_tools` serialization: `full` (default) or `compact` |
Expand Down
7 changes: 4 additions & 3 deletions docs/configuration/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,13 @@ secrets:
| `MCPPROXY_TRUSTED_HOSTS` | Comma-separated `Host` header allowlist for loopback listeners behind a reverse proxy (see [Reverse Proxy Deployment](/operations/reverse-proxy)) | - |
| `MCPPROXY_TRUSTED_PROXIES` | Comma-separated CIDRs or IP addresses whose `X-Forwarded-For` / `X-Real-IP` / `X-Forwarded-Proto` / `X-Forwarded-Host` headers are honoured (overrides `trusted_proxies`). Any other peer's forwarded headers are ignored. An entry that is neither a CIDR nor an IP is refused at boot with `trusted_proxies[N] "value" is not a valid CIDR or IP address`, exactly like a file value. See [trusted_proxies](/operations/reverse-proxy#trusted_proxies-forwarded-headers) | - (trust nobody) |
| `MCPPROXY_TLS_ENABLED` | Enable TLS/HTTPS | `false` |
| `MCPPROXY_TLS_CERT` | Path to TLS certificate | - |
| `MCPPROXY_TLS_KEY` | Path to TLS private key | - |
| `MCPPROXY_TLS_CERT_FILE` | Absolute path of a PEM server certificate to serve instead of the generated one (needs `MCPPROXY_TLS_KEY_FILE`) | - |
| `MCPPROXY_TLS_KEY_FILE` | Absolute path of the PEM private key for `MCPPROXY_TLS_CERT_FILE` | - |
| `MCPPROXY_TLS_EXTRA_SANS` | Comma-separated host names and IP addresses added to the generated certificate (not with `MCPPROXY_TLS_CERT_FILE`) | - |
| `MCPPROXY_TLS_REQUIRE_CLIENT_CERT` | Require client certificates for mTLS | `false` |
| `MCPPROXY_CERTS_DIR` | Custom directory for TLS certificates | - |

**Note:** TLS certificates are managed in `~/.mcpproxy/certs/` or via the `tls.certs_dir` config option. Use `mcpproxy trust-cert` to set up certificates.
**Note:** TLS certificates are managed in `~/.mcpproxy/certs/` or via the `tls.certs_dir` config option. Use `mcpproxy trust-cert` to set up certificates. To serve a name other than `localhost`, see [Other host names](https://github.com/smart-mcp-proxy/mcpproxy-go/blob/main/docs/configuration.md#other-host-names-extra_sans-cert_file-key_file).

### Audit Log

Expand Down
29 changes: 29 additions & 0 deletions docs/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,35 @@ After enabling HTTPS, update your client configurations to use `https://` URLs:
- `require_client_cert`: Enable mutual TLS (mTLS) for client authentication
- `certs_dir`: Custom directory for certificates (default: `{data_dir}/certs`)
- `hsts`: Enable HTTP Strict Transport Security headers
- `extra_sans`: Host names and IP addresses to add to the generated certificate (see below)
- `cert_file`, `key_file`: Serve your own certificate instead of the generated one (see below)

**Reaching mcpproxy by a name other than `localhost`:**

The generated certificate is valid for `localhost`, `127.0.0.1` and `::1`. For a Docker service name, a VM host name or a tailnet name, add it:

```json
{
"tls": {
"enabled": true,
"extra_sans": ["mcp.internal", "10.0.0.5"]
}
}
```

On the next start `localhost.pem` is replaced with a certificate that carries these names, signed by the same CA, so a client that already trusts `ca.pem` needs no change. Or serve a certificate you provide, for instance from a Docker or Kubernetes secret:

```json
{
"tls": {
"enabled": true,
"cert_file": "/run/secrets/mcpproxy.crt",
"key_file": "/run/secrets/mcpproxy.key"
}
}
```

Both settings are described in [Configuration Documentation - Other host names](configuration.md#other-host-names-extra_sans-cert_file-key_file).

**📚 For complete TLS configuration reference:** See [Configuration Documentation - TLS/HTTPS](configuration.md#tlshttps-configuration).

Expand Down
10 changes: 10 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,15 @@ type TLSConfig struct {
RequireClientCert bool `json:"require_client_cert" mapstructure:"require_client_cert"` // Enable mTLS
CertsDir string `json:"certs_dir,omitempty" mapstructure:"certs_dir"` // Directory for certificates
HSTS bool `json:"hsts" mapstructure:"hsts"` // Enable HTTP Strict Transport Security

// ExtraSANs are DNS names and IP addresses added to the generated server
// certificate, which is signed by the same local CA. Not used with CertFile.
ExtraSANs []string `json:"extra_sans,omitempty" mapstructure:"extra_sans"`
// CertFile is the absolute path of an operator-provided server certificate
// (PEM, leaf first), served instead of a generated one. Needs KeyFile.
CertFile string `json:"cert_file,omitempty" mapstructure:"cert_file"`
// KeyFile is the absolute path of the private key for CertFile (PEM).
KeyFile string `json:"key_file,omitempty" mapstructure:"key_file"`
}

// TokenizerConfig represents tokenizer configuration for token counting
Expand Down Expand Up @@ -3052,6 +3061,7 @@ func (c *Config) validateDetailedCore() []ValidationError {
})
}
}
errors = append(errors, validateTLSCertSettings(c.TLS)...)
}

// Validate logging configuration
Expand Down
18 changes: 18 additions & 0 deletions internal/config/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -859,6 +859,24 @@ func applyTLSEnvOverrides(cfg *Config) {
value = os.Getenv("MCPPROXY_CERTS_DIR")
envOverride(b, cfg, FieldTLSCertsDir, value != "", value)

// Extra names for the generated server certificate: comma-separated DNS
// names and IP addresses. An empty variable leaves the file value; entries
// are validated like file values (Validate runs after the overrides).
var sans []string
value = os.Getenv("MCPPROXY_TLS_EXTRA_SANS")
for _, e := range strings.Split(value, ",") {
if e = strings.TrimSpace(e); e != "" {
sans = append(sans, e)
}
}
envOverride(b, cfg, FieldTLSExtraSANs, len(sans) > 0, sans)

// Operator-provided server certificate and key (absolute paths).
value = os.Getenv("MCPPROXY_TLS_CERT_FILE")
envOverride(b, cfg, FieldTLSCertFile, value != "", value)
value = os.Getenv("MCPPROXY_TLS_KEY_FILE")
envOverride(b, cfg, FieldTLSKeyFile, value != "", value)

// Override data directory from environment. MCPPROXY_DATA_DIR is the
// documented name; MCPPROXY_DATA is kept for backward compatibility and
// loses when both are set.
Expand Down
15 changes: 15 additions & 0 deletions internal/config/process_overrides.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,21 @@ var (
Get: func(c *Config) string { return tlsOf(c).CertsDir },
Set: func(c *Config, v string) { t := cowTLS(c); t.CertsDir = v },
}
FieldTLSExtraSANs = Field[[]string]{
Name: "tls.extra_sans",
Get: func(c *Config) []string { return tlsOf(c).ExtraSANs },
Set: func(c *Config, v []string) { t := cowTLS(c); t.ExtraSANs = v },
}
FieldTLSCertFile = Field[string]{
Name: "tls.cert_file",
Get: func(c *Config) string { return tlsOf(c).CertFile },
Set: func(c *Config, v string) { t := cowTLS(c); t.CertFile = v },
}
FieldTLSKeyFile = Field[string]{
Name: "tls.key_file",
Get: func(c *Config) string { return tlsOf(c).KeyFile },
Set: func(c *Config, v string) { t := cowTLS(c); t.KeyFile = v },
}
FieldTPABundlePath = Field[string]{
Name: "security.tpa_bundle_path",
Get: func(c *Config) string { return securityOf(c).TPABundlePath },
Expand Down
Loading
Loading