You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 0a3971c
Browse filesBrowse the repository at this point in the historyBrowse files
feat(tls): serve a certificate for names other than localhost (extra_sans, cert_file, key_file) (#1745)
## Description
Native TLS can only serve a certificate that is valid for `localhost`, `127.0.0.1` and `::1`, so a client that reaches mcpproxy by a Docker service name, a VM host name or a tailnet name cannot verify it. This adds the two settings proposed in #1719 (items 1 and 2; the optional `tls.client_ca_file` is not included). With none of them set, startup is unchanged.
- **`tls.extra_sans`**: DNS names and IP addresses added to the generated server certificate. When `localhost.pem` does not carry exactly `localhost`, `127.0.0.1`, `::1` and the listed names, it is regenerated from the existing local CA. `ca.pem` and `ca.key` are never regenerated, so `mcpproxy trust-cert` and `NODE_EXTRA_CA_CERTS` keep working. A restart with an unchanged list rewrites nothing. Emptying the list gives a plain `localhost` certificate again; a `localhost.pem` the local CA did not sign is never touched without `extra_sans`, as before.
- **`tls.cert_file` and `tls.key_file`**: serve a certificate the operator provides (real CA, internal CA, `certbot`, a Docker or Kubernetes secret mount). Both or neither, absolute paths, read once at startup with `secret.ReadFile` (symlinks followed, regular file, 64 KiB cap). Only one set, a file that cannot be read, or a key that does not match the certificate stops startup with an error that names the setting and never shows file content. Nothing is generated; the local CA is created only when `require_client_cert` is on, because client certificates are verified against it.
- **Validation** (config load, REST config apply and startup all use the same rules): `extra_sans` entries must be a host name or an IP address; empty entries, wildcards, URLs, ports, brackets and non-ASCII names are refused with an error that names the index and value (`tls.extra_sans: extra_sans[1] "*.x": wildcard names are not supported; list each name`). `extra_sans` together with `cert_file` is refused, since the provided certificate decides the names. `cert_file`/`key_file` pairing and absolute paths are checked at load; readability and key match at startup.
- **Env vars**, following the existing `MCPPROXY_TLS_*` pattern and recorded as process-only overrides (never written back to `mcp_config.json`): `MCPPROXY_TLS_EXTRA_SANS` (comma-separated), `MCPPROXY_TLS_CERT_FILE`, `MCPPROXY_TLS_KEY_FILE`.
- All three settings are restart-required (`DetectConfigChanges`), like the rest of the `tls` block.
- Leaf and key writes now go through a temporary file and a rename (key first), so a crash during regeneration leaves a pair the next start repairs rather than a half-written file.
Not changed: `mcpproxy trust-cert`, the tray and CLI clients, the Settings page (the new keys are file/env only), the default certificate lifetime (one year, no renewal).
`docs/configuration/environment-variables.md` listed `MCPPROXY_TLS_CERT` and `MCPPROXY_TLS_KEY`, which no code reads; those two rows are replaced by the real names above.
Changes: `internal/tlslocal` (`localca.go`, new `sans.go`), `internal/config` (`config.go`, `loader.go`, `process_overrides.go`, new `tls_cert_validate.go`), `internal/runtime/config_hotreload.go`, `internal/server/server.go`, regenerated `oas/` (`make swagger`, clean on `main` without this change), docs (`configuration.md`, `setup.md`, `configuration/environment-variables.md`), CHANGELOG.
## Testing
- [x] I have tested these changes locally
- [x] I have added/updated tests that prove my fix is effective or my feature works
- [x] All existing tests pass
New tests:
- `internal/tlslocal`: SAN parsing and every refusal; the leaf gains the names while `ca.pem`/`ca.key` stay byte-identical and the leaf verifies against that CA; the same list in another order and spelling rewrites nothing; a changed list regenerates, and so does removing one name; emptying the list gives a plain localhost leaf, and a restart after that rewrites nothing; a leaf from another CA is not overwritten; no temporary files are left in `certs_dir`; `cert_file`/`key_file` served as is with no CA or directory created, CA created only with `require_client_cert`, CRLF files, and the error cases (one of the two, missing, relative, directory, key of another certificate, not PEM, with `extra_sans`) with a check that no error contains file content.
- `internal/tlslocal` handshakes: a client that trusts `ca.pem` fails `https://mcp.internal/` before `extra_sans` and succeeds after (with the same CA file), for a DNS name and an IP literal, still for `localhost` and `127.0.0.1`, and a name outside the list is still refused; the same with a provided certificate. The client uses a custom dialer, so no DNS is involved.
- `internal/config`: validation (valid, invalid entries, pairing, relative paths, `extra_sans` with `cert_file`, not checked while TLS is off), loading from a file with JSON built by `json.Marshal` (so Windows paths are escaped), env override wins and is not persisted, one-sided env value refused.
- `internal/runtime`: each setting needs a restart; nil and empty `extra_sans` are equal.
Commands run (Linux, Go 1.27.1): `go build ./...`, `go build -tags server ./...`, `GOOS=windows go vet ./internal/tlslocal ./internal/config`, `go vet` on the touched packages, `go test -race ./internal/tlslocal/... ./internal/config/...`, `go test -race -run 'DetectConfigChanges|HotReload' ./internal/runtime`, `go test ./internal/runtime` (whole package, 270s, passes), `go test -run 'TLS|Tls|Config|HotReload|Trust' ./internal/runtime ./cmd/mcpproxy ./internal/server`, and `golangci-lint run --config .github/.golangci.yml ./internal/tlslocal/... ./internal/config/... ./internal/runtime/...` (v2.9.0 built with Go 1.26): 0 issues.
By hand with a built binary: with `extra_sans: ["mcp.internal", "10.9.8.7"]`, `curl --cacert certs/ca.pem --resolve mcp.internal:PORT:127.0.0.1 https://mcp.internal:PORT/healthz` verifies (200) and a name outside the list fails; `openssl x509 -ext subjectAltName` shows the five names. With `cert_file` alone, a key that does not match, and a missing file, startup stops with the setting named (exit 1); with a matching pair the provided certificate is served (`curl --cacert other.pem` verifies `svc.example`).
Not run: macOS and Windows (the tests use only `filepath`/`t.TempDir()` and no file permissions; `go vet` for `GOOS=windows` is clean).
Closes#1719
-**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)
105
105
106
106
- **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)
107
+
-**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.
107
108
108
109
-**catalog:** a catalog source whose live search times out or fails is now answered from the listing
109
110
the daemon last saw from it (at most 24 hours old, kept in memory). Those results are marked
|`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|
1156
1159
|`certs_dir`| string |`""`| Custom certificate directory (defaults to `${data_dir}/certs`) |
1157
1160
|`hsts`| boolean |`true`| Enable HTTP Strict Transport Security headers |
1161
+
|`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)|
1162
+
|`cert_file`| string |`""`| Absolute path of a server certificate (PEM) to serve instead of the generated one. Needs `key_file`|
1163
+
|`key_file`| string |`""`| Absolute path of the private key (PEM) for `cert_file`. Needs `cert_file`|
1158
1164
1159
1165
**Quick Setup:**
1160
1166
1. Trust certificate: `mcpproxy trust-cert`
@@ -1163,6 +1169,36 @@ The tokenizer automatically maps model names to encodings:
1163
1169
1164
1170
See [Setup Guide - HTTPS](setup.md#optional-https-setup) for complete details.
1165
1171
1172
+
### Other host names (`extra_sans`, `cert_file`, `key_file`)
1173
+
1174
+
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.
1175
+
1176
+
**`extra_sans`**: names for the generated certificate.
- 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.
1183
+
- 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).
1184
+
- 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.
1185
+
- The new certificate is valid for one year from the day it is written. There is no automatic renewal.
- 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.
1195
+
- 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.
1196
+
- 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.
1197
+
- The files are read once at startup. After the certificate is renewed, restart mcpproxy.
1198
+
- Environment variables: `MCPPROXY_TLS_CERT_FILE` and `MCPPROXY_TLS_KEY_FILE`.
1199
+
1200
+
Changing any of the three settings needs a restart.
1201
+
1166
1202
---
1167
1203
1168
1204
## Logging Configuration
@@ -1905,6 +1941,9 @@ Many configuration options can be overridden via environment variables:
Copy file name to clipboardExpand all lines: docs/configuration/environment-variables.md
+4-3Lines changed: 4 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -79,12 +79,13 @@ secrets:
79
79
| `MCPPROXY_TRUSTED_HOSTS` | Comma-separated `Host` header allowlist for loopback listeners behind a reverse proxy (see [Reverse Proxy Deployment](/operations/reverse-proxy)) | - |
80
80
| `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) |
**Note:** TLS certificates are managed in `~/.mcpproxy/certs/` or via the `tls.certs_dir` config option. Use `mcpproxy trust-cert` to set up certificates.
88
+
**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).
Copy file name to clipboardExpand all lines: docs/setup.md
+29Lines changed: 29 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -535,6 +535,35 @@ After enabling HTTPS, update your client configurations to use `https://` URLs:
535
535
-`require_client_cert`: Enable mutual TLS (mTLS) for client authentication
536
536
-`certs_dir`: Custom directory for certificates (default: `{data_dir}/certs`)
537
537
-`hsts`: Enable HTTP Strict Transport Security headers
538
+
-`extra_sans`: Host names and IP addresses to add to the generated certificate (see below)
539
+
-`cert_file`, `key_file`: Serve your own certificate instead of the generated one (see below)
540
+
541
+
**Reaching mcpproxy by a name other than `localhost`:**
542
+
543
+
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:
544
+
545
+
```json
546
+
{
547
+
"tls": {
548
+
"enabled": true,
549
+
"extra_sans": ["mcp.internal", "10.0.0.5"]
550
+
}
551
+
}
552
+
```
553
+
554
+
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:
555
+
556
+
```json
557
+
{
558
+
"tls": {
559
+
"enabled": true,
560
+
"cert_file": "/run/secrets/mcpproxy.crt",
561
+
"key_file": "/run/secrets/mcpproxy.key"
562
+
}
563
+
}
564
+
```
565
+
566
+
Both settings are described in [Configuration Documentation - Other host names](configuration.md#other-host-names-extra_sans-cert_file-key_file).
538
567
539
568
**📚 For complete TLS configuration reference:** See [Configuration Documentation - TLS/HTTPS](configuration.md#tlshttps-configuration).
0 commit comments