Skip to content
Open
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 internal/cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ func newRunCommand() *runCommand {
runCommand.cmd.Flags().IntVar(&globalConfig.HttpsPort, "https-port", getEnvInt("HTTPS_PORT", server.DefaultHttpsPort), "Port to serve HTTPS traffic on")
runCommand.cmd.Flags().IntVar(&globalConfig.MetricsPort, "metrics-port", getEnvInt("METRICS_PORT", 0), "Publish metrics on the specified port (default zero to disable)")
runCommand.cmd.Flags().BoolVar(&globalConfig.HTTP3Enabled, "http3", false, "Enable HTTP/3")
runCommand.cmd.Flags().StringVar(&globalConfig.MinTLS, "min-tls", getEnvString("MIN_TLS", ""), "Set minimum TLS version (tls1_0, tls1_1, tls1_2, tls1_3)")

return runCommand
}
Expand Down
9 changes: 9 additions & 0 deletions internal/cmd/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,15 @@ func getEnvInt(key string, defaultValue int) int {
return intValue
}

func getEnvString(key string, defaultValue string) string {
value, ok := findEnv(key)
if !ok {
return defaultValue
}

return value
}

func getEnvBool(key string, defaultValue bool) bool {
value, ok := findEnv(key)
if !ok {
Expand Down
1 change: 1 addition & 0 deletions internal/server/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type Config struct {
HttpsPort int
MetricsPort int
HTTP3Enabled bool
MinTLS string

AlternateConfigDir string
}
Expand Down
24 changes: 20 additions & 4 deletions internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,25 @@ func (s *Server) startHTTPServers() error {
return err
}
s.httpsListener = httpsListener
tlsConfig := &tls.Config{
NextProtos: []string{"h2", "http/1.1", acme.ALPNProto},
GetCertificate: s.router.GetCertificate,
}
switch s.config.MinTLS {
case "tls1_0":
tlsConfig.MinVersion = tls.VersionTLS10
case "tls1_1":
tlsConfig.MinVersion = tls.VersionTLS11
case "tls1_2":
tlsConfig.MinVersion = tls.VersionTLS12
case "tls1_3":
tlsConfig.MinVersion = tls.VersionTLS13
case "":
// No minimum TLS version set

Copilot AI Mar 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When MinTLS is empty, the HTTPS server TLS config leaves MinVersion unset. That means the effective minimum is Go's runtime default, which can be changed by environment/runtime settings and may not meet the PR's stated goal of hardening the server. If the intent is to be secure by default, consider setting an explicit default minimum (e.g., TLS 1.2) when MinTLS is not provided, and reserve an explicit opt-out value if you need to preserve legacy behavior.

Suggested change
// No minimum TLS version set
// Default to a secure minimum TLS version when not explicitly configured.
tlsConfig.MinVersion = tls.VersionTLS12

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

default and empty case should use tls 1.2 because Go does it implicitly at the moment.

default:
return fmt.Errorf("unsupported minimum TLS version: %q (use tls1_0, tls1_1, tls1_2, or tls1_3)", s.config.MinTLS)
}
Comment thread
pebanb marked this conversation as resolved.
Comment on lines +168 to +170

Copilot AI Mar 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new error branch for unsupported MinTLS values is user-facing behavior but isn't covered by tests. Adding a small test that sets Config.MinTLS to an invalid value and asserts Server.Start() fails with this error would help prevent regressions (especially since the flag/env input is free-form).

Copilot uses AI. Check for mistakes.

s.httpsServer = &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if s.config.HTTP3Enabled {
Expand All @@ -158,10 +177,7 @@ func (s *Server) startHTTPServers() error {

handler.ServeHTTP(w, r)
}),
TLSConfig: &tls.Config{
NextProtos: []string{"h2", "http/1.1", acme.ALPNProto},
GetCertificate: s.router.GetCertificate,
},
TLSConfig: tlsConfig,
}

go s.httpServer.Serve(s.httpListener)
Expand Down
58 changes: 53 additions & 5 deletions internal/server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (

func TestServer_Deploying(t *testing.T) {
target := testTarget(t, func(w http.ResponseWriter, r *http.Request) {})
server := testServer(t, true)
server := testServer(t, false, "")

testDeployTarget(t, target, server, defaultServiceOptions)

Expand All @@ -24,9 +24,9 @@ func TestServer_Deploying(t *testing.T) {
}

func TestServer_DeployingHTTPS(t *testing.T) {
startDeployment := func(http3Enabled bool) *Server {
startDeployment := func(http3Enabled bool, minTLS string) *Server {
target := testTarget(t, func(w http.ResponseWriter, r *http.Request) {})
server := testServer(t, http3Enabled)
server := testServer(t, http3Enabled, minTLS)

certPath, keyPath := prepareTestCertificateFiles(t)
serviceOptions := defaultServiceOptions
Expand All @@ -40,7 +40,7 @@ func TestServer_DeployingHTTPS(t *testing.T) {
}

t.Run("with HTTP/3 enabled", func(t *testing.T) {
server := startDeployment(true)
server := startDeployment(true, "")

t.Run("http/1.1", func(t *testing.T) {
resp, err := testRequestUsingHTTP11(t, server)
Expand Down Expand Up @@ -72,8 +72,56 @@ func TestServer_DeployingHTTPS(t *testing.T) {
})
})

t.Run("with min TLS 1.0", func(t *testing.T) {
server := startDeployment(false, "tls1_0")

t.Run("http/1.1", func(t *testing.T) {
resp, err := testRequestUsingHTTP11(t, server)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, "HTTP/1.1", resp.Proto)
assert.GreaterOrEqual(t, resp.TLS.Version, uint16(tls.VersionTLS10))
})
Comment on lines +80 to +84

Copilot AI Mar 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new min-TLS subtests don't actually verify that the server enforces the configured minimum: asserting resp.TLS.Version >= TLS1.0/1.1/1.2 will pass even if the server ignores MinTLS (because the negotiated version is commonly higher anyway). Consider asserting the server's configured MinVersion directly (tests are in the same package, so you can read server.httpsServer.TLSConfig.MinVersion), and/or add a negative handshake test by forcing the client MaxVersion below the configured minimum and asserting the request fails.

Copilot uses AI. Check for mistakes.
})

t.Run("with min TLS 1.1", func(t *testing.T) {
server := startDeployment(false, "tls1_1")

t.Run("http/1.1", func(t *testing.T) {
resp, err := testRequestUsingHTTP11(t, server)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, "HTTP/1.1", resp.Proto)
assert.GreaterOrEqual(t, resp.TLS.Version, uint16(tls.VersionTLS11))
})
})

t.Run("with min TLS 1.2", func(t *testing.T) {
server := startDeployment(false, "tls1_2")

t.Run("http/1.1", func(t *testing.T) {
resp, err := testRequestUsingHTTP11(t, server)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, "HTTP/1.1", resp.Proto)
assert.GreaterOrEqual(t, resp.TLS.Version, uint16(tls.VersionTLS12))
})
})

t.Run("with min TLS 1.3", func(t *testing.T) {
server := startDeployment(false, "tls1_3")

t.Run("http/1.1", func(t *testing.T) {
resp, err := testRequestUsingHTTP11(t, server)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, "HTTP/1.1", resp.Proto)
assert.Equal(t, uint16(tls.VersionTLS13), resp.TLS.Version)
})
})

t.Run("with HTTP/3 disabled", func(t *testing.T) {
server := startDeployment(false)
server := startDeployment(false, "")

t.Run("http/1.1", func(t *testing.T) {
resp, err := testRequestUsingHTTP11(t, server)
Expand Down
13 changes: 7 additions & 6 deletions internal/server/testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ import (
)

var (
defaultHealthCheckConfig = HealthCheckConfig{Path: DefaultHealthCheckPath, Port: DefaultHealthCheckPort, Interval: DefaultHealthCheckInterval, Timeout: time.Second * 5}
defaultEmptyReaders = []string{}
defaultServiceOptions = ServiceOptions{TLSRedirect: true}
defaultTargetOptions = TargetOptions{HealthCheckConfig: defaultHealthCheckConfig, ResponseTimeout: DefaultTargetTimeout}
defaultDeploymentOptions = DeploymentOptions{DeployTimeout: DefaultDeployTimeout, DrainTimeout: DefaultDrainTimeout, Force: false}
defaultHealthCheckConfig = HealthCheckConfig{Path: DefaultHealthCheckPath, Port: DefaultHealthCheckPort, Interval: DefaultHealthCheckInterval, Timeout: time.Second * 5}
defaultEmptyReaders = []string{}
defaultServiceOptions = ServiceOptions{TLSRedirect: true}
defaultTargetOptions = TargetOptions{HealthCheckConfig: defaultHealthCheckConfig, ResponseTimeout: DefaultTargetTimeout}
defaultDeploymentOptions = DeploymentOptions{DeployTimeout: DefaultDeployTimeout, DrainTimeout: DefaultDrainTimeout, Force: false}
)

func testTarget(t testing.TB, handler http.HandlerFunc) *Target {
Expand Down Expand Up @@ -69,7 +69,7 @@ func testBackendWithHandler(t testing.TB, handler http.HandlerFunc) (*httptest.S
return server, serverURL.Host
}

func testServer(t testing.TB, http3Enabled bool) *Server {
func testServer(t testing.TB, http3Enabled bool, minTLS string) *Server {
t.Helper()

config := &Config{
Expand All @@ -78,6 +78,7 @@ func testServer(t testing.TB, http3Enabled bool) *Server {
HttpsPort: 0,
AlternateConfigDir: t.TempDir(),
HTTP3Enabled: http3Enabled,
MinTLS: minTLS,
}
router := NewRouter(config.StatePath())
server := NewServer(config, router)
Expand Down
Loading