Skip to content

Commit 8bc385d

Browse files
authored
feat: add support for HTTPS/TLS listener (#3126)
This PR introduces the ability to run the Toolbox server over HTTPS. While the server still defaults to HTTP for local development, users can now enable TLS encryption via command-line flags. This is essential for secure communication when the Toolbox is exposed over a network or used in production-like environments. **New Flags:** * `--tls`: Boolean flag to enable HTTPS. * `--tls-cert`: String flag specifying the path to the PEM-encoded certificate file. * `--tls-key`: String flag specifying the path to the PEM-encoded private key file. **Use Case: How the Server Obtains .pem Files** In a typical deployment, the server does not generate these files itself; it expects them to be provided by the environment. 1. Local Development: Users can use tools like mkcert to generate a locally-trusted cert.pem and key.pem. 2. Production (Manual): Users obtain certificates from a Certificate Authority (CA) like Let's Encrypt via Certbot. Certbot handles the domain validation and saves the .pem files to a specific directory (e.g., /etc/letsencrypt/live/). 3. Execution: The user starts the Toolbox and points it to those specific paths: ``` ./toolbox --tls --tls-cert=cert.pem --tls-key=key.pem ``` 4. Loading: The server uses tls.LoadX509KeyPair to read these files from the disk and injects them into the listener before the HTTP server starts processing requests. 🛠️ Related #3113
1 parent da27b37 commit 8bc385d

10 files changed

Lines changed: 285 additions & 80 deletions

File tree

cmd/internal/flags.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ func ConfigFileFlags(flags *pflag.FlagSet, opts *ToolboxOptions) {
6161
func ServeFlags(flags *pflag.FlagSet, opts *ToolboxOptions) {
6262
flags.StringVarP(&opts.Cfg.Address, "address", "a", "127.0.0.1", "Address of the interface the server will listen on.")
6363
flags.IntVarP(&opts.Cfg.Port, "port", "p", 5000, "Port the server will listen on.")
64+
flags.StringVar(&opts.Cfg.CertFile, "tls-cert", "", "Path to TLS certificate file")
65+
flags.StringVar(&opts.Cfg.KeyFile, "tls-key", "", "Path to TLS key file")
6466
flags.BoolVar(&opts.Cfg.Stdio, "stdio", false, "Listens via MCP STDIO instead of acting as a remote HTTP server.")
6567
flags.BoolVar(&opts.Cfg.UI, "ui", false, "Launches the Toolbox UI web server.")
6668
flags.BoolVar(&opts.Cfg.EnableAPI, "enable-api", false, "Enable the /api endpoint.")

cmd/internal/serve/command.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@ func runServe(cmd *cobra.Command, opts *internal.ToolboxOptions) error {
7979
return errMsg
8080
}
8181

82+
useTLS := opts.Cfg.CertFile != "" || opts.Cfg.KeyFile != ""
83+
protocol := "http"
84+
if useTLS {
85+
protocol = "https"
86+
}
87+
8288
// run server in background
8389
srvErr := make(chan error, 1)
8490
if opts.Cfg.Stdio {
@@ -90,15 +96,15 @@ func runServe(cmd *cobra.Command, opts *internal.ToolboxOptions) error {
9096
}
9197
}()
9298
} else {
93-
err = s.Listen(ctx)
99+
err = s.Listen(ctx, opts.Cfg.CertFile, opts.Cfg.KeyFile)
94100
if err != nil {
95101
errMsg := fmt.Errorf("toolbox failed to start listener: %w", err)
96102
opts.Logger.ErrorContext(ctx, errMsg.Error())
97103
return errMsg
98104
}
99105
opts.Logger.InfoContext(ctx, "Server ready to serve!")
100106
if opts.Cfg.UI {
101-
opts.Logger.InfoContext(ctx, fmt.Sprintf("Toolbox UI is up and running at: http://%s:%d/ui", opts.Cfg.Address, opts.Cfg.Port))
107+
opts.Logger.InfoContext(ctx, fmt.Sprintf("Toolbox UI is up and running at: %s://%s:%d/ui", protocol, opts.Cfg.Address, opts.Cfg.Port))
102108
}
103109

104110
go func() {

cmd/root.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,12 @@ func run(cmd *cobra.Command, opts *internal.ToolboxOptions) error {
476476
return errMsg
477477
}
478478

479+
useTLS := opts.Cfg.CertFile != "" || opts.Cfg.KeyFile != ""
480+
protocol := "http"
481+
if useTLS {
482+
protocol = "https"
483+
}
484+
479485
// run server in background
480486
srvErr := make(chan error)
481487
if opts.Cfg.Stdio {
@@ -487,15 +493,15 @@ func run(cmd *cobra.Command, opts *internal.ToolboxOptions) error {
487493
}
488494
}()
489495
} else {
490-
err = s.Listen(ctx)
496+
err = s.Listen(ctx, opts.Cfg.CertFile, opts.Cfg.KeyFile)
491497
if err != nil {
492498
errMsg := fmt.Errorf("toolbox failed to start listener: %w", err)
493499
opts.Logger.ErrorContext(ctx, errMsg.Error())
494500
return errMsg
495501
}
496502
opts.Logger.InfoContext(ctx, "Server ready to serve!")
497503
if opts.Cfg.UI {
498-
opts.Logger.InfoContext(ctx, fmt.Sprintf("Toolbox UI is up and running at: http://%s:%d/ui", opts.Cfg.Address, opts.Cfg.Port))
504+
opts.Logger.InfoContext(ctx, fmt.Sprintf("Toolbox UI is up and running at: %s://%s:%d/ui", protocol, opts.Cfg.Address, opts.Cfg.Port))
499505
}
500506

501507
go func() {

cmd/root_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,20 @@ func TestServerConfigFlags(t *testing.T) {
232232
UserAgentMetadata: []string{"foo", "bar"},
233233
}),
234234
},
235+
{
236+
desc: "cert file",
237+
args: []string{"--tls-cert", "cert.pem"},
238+
want: withDefaults(server.ServerConfig{
239+
CertFile: "cert.pem",
240+
}),
241+
},
242+
{
243+
desc: "key file",
244+
args: []string{"--tls-key", "key.pem"},
245+
want: withDefaults(server.ServerConfig{
246+
KeyFile: "key.pem",
247+
}),
248+
},
235249
}
236250
for _, tc := range tcs {
237251
t.Run(tc.desc, func(t *testing.T) {

docs/en/documentation/deploy-to/_index.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,11 @@ Choose your preferred deployment platform below to get started:
1515
* **[Kubernetes](./kubernetes/)**: Deploy the Toolbox as a microservice using GKE.
1616

1717
{{< notice tip >}}
18-
**Production Security:** When moving to production, never hardcode passwords or API keys directly into your `tools.yaml`. Always use environment variable substitution and inject those values securely through your deployment platform's secret manager.
18+
**Production Security:** When moving to production, never hardcode passwords or
19+
API keys directly into your `tools.yaml`. Always use environment variable
20+
substitution and inject those values securely through your deployment platform's
21+
secret manager.
22+
23+
To enable HTTPS, you must provide a valid pair of `--tls-cert` and `--tls-key`
24+
files; specifying only one will cause the server to fail at startup.
1925
{{< /notice >}}

docs/en/reference/cli.md

Lines changed: 73 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,29 +8,31 @@ description: >
88

99
## Reference
1010

11-
| Flag (Short) | Flag (Long) | Description | Default |
12-
|--------------|----------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------|
13-
| `-a` | `--address` | Address of the interface the server will listen on. | `127.0.0.1` |
11+
| Flag (Short) | Flag (Long) | Description | Default |
12+
|--------------|----------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------|
13+
| `-a` | `--address` | Address of the interface the server will listen on. | `127.0.0.1` |
1414
| | `--disable-reload` | Disables dynamic reloading config. | |
15-
| `-h` | `--help` | help for toolbox | |
16-
| | `--log-level` | Specify the minimum level logged. Allowed: 'DEBUG', 'INFO', 'WARN', 'ERROR'. | `info` |
17-
| | `--logging-format` | Specify logging format to use. Allowed: 'standard' or 'JSON'. | `standard` |
18-
| | `--mcp-prm-file` | Path to a manual Protected Resource Metadata (PRM) JSON file. If provided, overrides auto-generation for MCP Server-Wide Authentication. | |
19-
| `-p` | `--port` | Port the server will listen on. | `5000` |
20-
| | `--prebuilt` | Use one or more prebuilt tool configuration by source type. See [Prebuilt Tools Reference](../documentation/configuration/prebuilt-configs/_index.md) for allowed values. | |
21-
| | `--stdio` | Listens via MCP STDIO instead of acting as a remote HTTP server. | |
22-
| | `--telemetry-gcp` | Enable exporting directly to Google Cloud Monitoring. | |
23-
| | `--telemetry-otlp` | Enable exporting using OpenTelemetry Protocol (OTLP) to the specified endpoint (e.g. 'http://127.0.0.1:4318') | |
24-
| | `--telemetry-service-name` | Sets the value of the service.name resource attribute for telemetry data. | `toolbox` |
25-
| | `--config` | File path specifying the tool configuration. Cannot be used with --configs or --config-folder. | |
26-
| | `--configs` | Multiple file paths specifying tool configurations. Files will be merged. Cannot be used with --config or --config-folder. | |
27-
| | `--config-folder` | Directory path containing YAML tool configuration files. All .yaml and .yml files in the directory will be loaded and merged. Cannot be used with --config or --configs. | |
28-
| | `--ui` | Launches the Toolbox UI web server. | |
29-
| | `--allowed-origins` | Specifies a list of origins permitted to access this server for CORs access. | `*` |
30-
| | `--allowed-hosts` | Specifies a list of hosts permitted to access this server to prevent DNS rebinding attacks. | `*` |
31-
| | `--user-agent-metadata` | Appends additional metadata to the User-Agent. | |
32-
| | `--poll-interval` | Specifies the polling frequency (seconds) for configuration file updates. | `0` |
33-
| `-v` | `--version` | version for toolbox | |
15+
| `-h` | `--help` | help for toolbox | |
16+
| | `--log-level` | Specify the minimum level logged. Allowed: 'DEBUG', 'INFO', 'WARN', 'ERROR'. | `info` |
17+
| | `--logging-format` | Specify logging format to use. Allowed: 'standard' or 'JSON'. | `standard` |
18+
| | `--mcp-prm-file` | Path to a manual Protected Resource Metadata (PRM) JSON file. If provided, overrides auto-generation for MCP Server-Wide Authentication. | |
19+
| `-p` | `--port` | Port the server will listen on. | `5000` |
20+
| | `--tls-cert` | Path to the PEM-encoded TLS certificate file. | |
21+
| | `--tls-key` | Path to the PEM-encoded TLS private key file. | |
22+
| | `--prebuilt` | Use one or more prebuilt tool configuration by source type. See [Prebuilt Tools Reference](../documentation/configuration/prebuilt-configs/_index.md) for allowed values. | |
23+
| | `--stdio` | Listens via MCP STDIO instead of acting as a remote HTTP server. | |
24+
| | `--telemetry-gcp` | Enable exporting directly to Google Cloud Monitoring. | |
25+
| | `--telemetry-otlp` | Enable exporting using OpenTelemetry Protocol (OTLP) to the specified endpoint (e.g. 'http://127.0.0.1:4318') | |
26+
| | `--telemetry-service-name` | Sets the value of the service.name resource attribute for telemetry data. | `toolbox` |
27+
| | `--config` | File path specifying the tool configuration. Cannot be used with --configs or --config-folder. | |
28+
| | `--configs` | Multiple file paths specifying tool configurations. Files will be merged. Cannot be used with --config or --config-folder. | |
29+
| | `--config-folder` | Directory path containing YAML tool configuration files. All .yaml and .yml files in the directory will be loaded and merged. Cannot be used with --config or --configs. | |
30+
| | `--ui` | Launches the Toolbox UI web server. | |
31+
| | `--allowed-origins` | Specifies a list of origins permitted to access this server for CORs access. | `*` |
32+
| | `--allowed-hosts` | Specifies a list of hosts permitted to access this server to prevent DNS rebinding attacks. | `*` |
33+
| | `--user-agent-metadata` | Appends additional metadata to the User-Agent. | |
34+
| | `--poll-interval` | Specifies the polling frequency (seconds) for configuration file updates. | `0` |
35+
| `-v` | `--version` | version for toolbox | |
3436

3537
## Sub Commands
3638

@@ -82,6 +84,55 @@ For more detailed instructions, see [Generate Agent Skills](../documentation/con
8284

8385
## Examples
8486

87+
### Hardening Toolbox
88+
89+
Toolbox is designed for flexibility, but security should not be ignored—even in
90+
local development. When exposing the server to a network or running it alongside
91+
a web browser, use these configurations to protect your data and system.
92+
93+
#### Host Validation & DNS Rebinding Protection
94+
The `--allowed-hosts` flag controls which Host headers the server accepts.
95+
Restricting this is the primary defense against DNS Rebinding attacks.
96+
97+
* Flag: `--allowed-hosts`
98+
* Local Development: Set to localhost or 127.0.0.1.
99+
* Production: Set to your specific FQDN (e.g., toolbox.example.com).
100+
* Example:
101+
```
102+
./toolbox --allowed-hosts="localhost,127.0.0.1"
103+
```
104+
105+
106+
{{< notice tip >}}
107+
**The "Local" Fallacy:** Using `--allowed-hosts="*"` is unsafe even on localhost. A
108+
malicious website can trick your browser into making requests to `127.0.0.1`,
109+
effectively bypassing the browser's security to control your local Toolbox.
110+
{{< /notice >}}
111+
112+
#### Cross-Origin Resource Sharing (CORS)
113+
The `--allowed-origins` flag dictates which web applications (frontends) are
114+
permitted to communicate with your Toolbox API.
115+
116+
* Flag: `--allowed-origins`
117+
* Recommendation: Avoid `*` in any environment containing sensitive data. Explicitly list your trusted frontend URLs.
118+
* Example:
119+
```
120+
./toolbox --allowed-origins="https://my-mcp-ui.internal.com"
121+
```
122+
123+
#### Transport Layer Security (TLS/HTTPS)
124+
By default, traffic is unencrypted (HTTP). In production or shared networks, you must enable TLS to prevent Man-in-the-Middle (MitM) attacks and packet sniffing.
125+
126+
* Flag: `--tls-cert` and `--tls-key` (Both cert and key files are required for
127+
TLS activation)
128+
* Protocol: Toolbox enforces TLS 1.2 as a minimum version to ensure modern encryption standards.
129+
* Use Case: Use Certbot for public domains or mkcert for locally-trusted development certificates.
130+
* Example:
131+
```
132+
./toolbox --tls-cert=cert.pem --tls-key=key.pem
133+
```
134+
135+
85136
### Transport Configuration
86137

87138
**Server Settings:**

docs/en/reference/faq.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ For detailed instructions, check out these resources:
1515

1616
- [Quickstart: How to Run Locally](../documentation/getting-started/local_quickstart.md)
1717
- [Deploy to Cloud Run](../documentation/deploy-to/cloud-run/_index.md)
18+
- To run Toolbox more securely or harden security, check out our [hardening guidelines](./cli.md#hardening-toolbox)
1819

1920
[release-notes]: https://github.com/googleapis/mcp-toolbox/releases/
2021

internal/server/config.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ type ServerConfig struct {
4040
Address string
4141
// Port is the port the server will listen on.
4242
Port int
43+
// CertFile is the path to tls certificate file
44+
CertFile string
45+
// KeyFile is the path to TLS key file
46+
KeyFile string
4347
// SourceConfigs defines what sources of data are available for tools.
4448
SourceConfigs SourceConfigs
4549
// AuthServiceConfigs defines what sources of authentication are available for tools.

internal/server/server.go

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package server
1616

1717
import (
1818
"context"
19+
"crypto/tls"
1920
"encoding/json"
2021
"errors"
2122
"fmt"
@@ -535,19 +536,34 @@ func mcpAuthMiddleware(s *Server) func(http.Handler) http.Handler {
535536
}
536537

537538
// Listen starts a listener for the given Server instance.
538-
func (s *Server) Listen(ctx context.Context) error {
539+
func (s *Server) Listen(ctx context.Context, certFile, keyFile string) error {
539540
ctx, cancel := context.WithCancel(ctx)
540541
defer cancel()
541542

542543
if s.listener != nil {
543544
return fmt.Errorf("server is already listening: %s", s.listener.Addr().String())
544545
}
545546
lc := net.ListenConfig{KeepAlive: 30 * time.Second}
546-
var err error
547-
if s.listener, err = lc.Listen(ctx, "tcp", s.srv.Addr); err != nil {
547+
ln, err := lc.Listen(ctx, "tcp", s.srv.Addr)
548+
if err != nil {
548549
return fmt.Errorf("failed to open listener for %q: %w", s.srv.Addr, err)
549550
}
550-
s.logger.DebugContext(ctx, fmt.Sprintf("server listening on %s", s.srv.Addr))
551+
552+
if certFile != "" || keyFile != "" {
553+
// Load the certificates
554+
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
555+
if err != nil {
556+
ln.Close()
557+
return fmt.Errorf("failed to load TLS key pair (cert: %q, key: %q): %w", certFile, keyFile, err)
558+
}
559+
// Wrap the listener with TLS
560+
config := &tls.Config{Certificates: []tls.Certificate{cert}, MinVersion: tls.VersionTLS12}
561+
s.listener = tls.NewListener(ln, config)
562+
s.logger.DebugContext(ctx, fmt.Sprintf("secure server listening on %s", s.srv.Addr))
563+
} else {
564+
s.listener = ln
565+
s.logger.DebugContext(ctx, fmt.Sprintf("server listening on %s", s.srv.Addr))
566+
}
551567
return nil
552568
}
553569

0 commit comments

Comments
 (0)