diff --git a/internal/cmd/run.go b/internal/cmd/run.go index 0a70655..96b4004 100644 --- a/internal/cmd/run.go +++ b/internal/cmd/run.go @@ -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 } diff --git a/internal/cmd/util.go b/internal/cmd/util.go index d28618c..fc53414 100644 --- a/internal/cmd/util.go +++ b/internal/cmd/util.go @@ -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 { diff --git a/internal/server/config.go b/internal/server/config.go index 0314825..346427a 100644 --- a/internal/server/config.go +++ b/internal/server/config.go @@ -18,6 +18,7 @@ type Config struct { HttpsPort int MetricsPort int HTTP3Enabled bool + MinTLS string AlternateConfigDir string } diff --git a/internal/server/server.go b/internal/server/server.go index 4b336de..7fde7ea 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -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 + default: + return fmt.Errorf("unsupported minimum TLS version: %q (use tls1_0, tls1_1, tls1_2, or tls1_3)", s.config.MinTLS) + } + s.httpsServer = &http.Server{ Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if s.config.HTTP3Enabled { @@ -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) diff --git a/internal/server/server_test.go b/internal/server/server_test.go index 4792d3d..e95b9ef 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -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) @@ -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 @@ -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) @@ -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)) + }) + }) + + 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) diff --git a/internal/server/testing.go b/internal/server/testing.go index 99d12f5..40bc7b5 100644 --- a/internal/server/testing.go +++ b/internal/server/testing.go @@ -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 { @@ -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{ @@ -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)