From de67dbc220d480a6cc86ffe006b671226f6c8053 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 2 Nov 2023 13:00:24 -0500 Subject: [PATCH 01/12] feat: add configurable cipher suites for tls listening --- cli/server.go | 156 ++++++++++++++++++++++++++++++++-- cli/server_internal_test.go | 102 ++++++++++++++++++++++ codersdk/deployment.go | 22 ++--- enterprise/cli/proxyserver.go | 2 +- 4 files changed, 266 insertions(+), 16 deletions(-) create mode 100644 cli/server_internal_test.go diff --git a/cli/server.go b/cli/server.go index 61755840382e1..358f35790818e 100644 --- a/cli/server.go +++ b/cli/server.go @@ -354,7 +354,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. logger.Debug(ctx, "tracing closed", slog.Error(traceCloseErr)) }() - httpServers, err := ConfigureHTTPServers(inv, vals) + httpServers, err := ConfigureHTTPServers(logger, inv, vals) if err != nil { return xerrors.Errorf("configure http(s): %w", err) } @@ -1411,7 +1411,10 @@ func generateSelfSignedCertificate() (*tls.Certificate, error) { return &cert, nil } -func configureTLS(tlsMinVersion, tlsClientAuth string, tlsCertFiles, tlsKeyFiles []string, tlsClientCAFile string) (*tls.Config, error) { +// configureServerTLS returns the TLS config used for the Coderd server +// connections to clients. A logger is passed in to allow printing warning +// messages that do not block startup. +func configureServerTLS(ctx context.Context, logger slog.Logger, tlsMinVersion, tlsClientAuth string, tlsCertFiles, tlsKeyFiles []string, tlsClientCAFile string, ciphers []string, allowInsecureCiphers bool) (*tls.Config, error) { tlsConfig := &tls.Config{ MinVersion: tls.VersionTLS12, NextProtos: []string{"h2", "http/1.1"}, @@ -1429,6 +1432,15 @@ func configureTLS(tlsMinVersion, tlsClientAuth string, tlsCertFiles, tlsKeyFiles return nil, xerrors.Errorf("unrecognized tls version: %q", tlsMinVersion) } + // A custom set of supported ciphers. + if len(ciphers) > 0 { + cipherIDs, err := configureCipherSuites(ctx, logger, ciphers, allowInsecureCiphers, tlsConfig.MinVersion, tls.VersionTLS13) + if err != nil { + return nil, err + } + tlsConfig.CipherSuites = cipherIDs + } + switch tlsClientAuth { case "none": tlsConfig.ClientAuth = tls.NoClientCert @@ -1487,6 +1499,135 @@ func configureTLS(tlsMinVersion, tlsClientAuth string, tlsCertFiles, tlsKeyFiles return tlsConfig, nil } +func configureCipherSuites(ctx context.Context, logger slog.Logger, ciphers []string, allowInsecureCiphers bool, minTLS, maxTLS uint16) ([]uint16, error) { + if minTLS >= tls.VersionTLS13 { + // The cipher suites config option is ignored for tls 1.3 and higher. + // So this user flag is a no-op if the min version is 1.3. + return nil, xerrors.Errorf("tls ciphers cannot be specified when using minimum tls version 1.3 or higher") + } + // Configure the cipher suites which parses the strings and converts them + // to golang cipher suites. + supported, err := parseTLSCipherSuites(ciphers) + if err != nil { + return nil, xerrors.Errorf("tls ciphers: %w", err) + } + + // allVersions is all tls versions the server supports. + allVersions := make(map[uint16]bool) + for v := minTLS; v <= maxTLS; v++ { + allVersions[v] = false + } + + var insecure []string + cipherIDs := make([]uint16, 0, len(supported)) + for _, cipher := range supported { + if cipher.Insecure { + // Always show this warning, even if they have allowInsecureCiphers + // specified. + logger.Warn(ctx, "insecure tls cipher specified for server use", slog.F("cipher", cipher.Name)) + insecure = append(insecure, cipher.Name) + } + + // This is a warning message to tell the user if they are specifying + // a cipher that does not support the tls versions they have specified. + // This makes the cipher essentially a "noop" cipher. + if !hasSupportedVersion(minTLS, maxTLS, cipher.SupportedVersions) { + versions := make([]string, 0, len(cipher.SupportedVersions)) + for _, sv := range cipher.SupportedVersions { + versions = append(versions, tls.VersionName(sv)) + } + logger.Warn(ctx, "cipher not supported for tls versions allowed, cipher will not be used", + slog.F("cipher", cipher.Name), + slog.F("cipher_supported_versions", strings.Join(versions, ",")), + slog.F("server_min_version", tls.VersionName(minTLS)), + slog.F("server_max_version", tls.VersionName(maxTLS)), + ) + } + + for _, v := range cipher.SupportedVersions { + allVersions[v] = true + } + + cipherIDs = append(cipherIDs, cipher.ID) + } + + if len(insecure) > 0 && !allowInsecureCiphers { + return nil, xerrors.Errorf("insecure tls ciphers specified, must use '--tls-allow-insecure-ciphers' to allow these: %s", strings.Join(insecure, ", ")) + } + + // This is an additional sanity check. The user can specify ciphers that + // do not cover the full range of tls versions they have specified. + // They can unintentionally break TLS for some tls configured versions. + var missedVersions []string + for version, covered := range allVersions { + if version == tls.VersionTLS13 { + continue // v1.3 ignores configured cipher suites. + } + if !covered { + missedVersions = append(missedVersions, tls.VersionName(version)) + } + } + if len(missedVersions) > 0 { + return nil, xerrors.Errorf("no tls ciphers supported for tls versions %q."+ + "Add additional ciphers, specify the minimum version to 'tls13, or remove the ciphers configured and rely on the default", + strings.Join(missedVersions, ",")) + } + + return cipherIDs, nil +} + +// parseTLSCipherSuites will parse cipher suite names like 'TLS_RSA_WITH_AES_128_CBC_SHA' +// to their tls cipher suite structs. If a cipher suite that is unsupported is +// passed in, this function will return an error. +// This function can return insecure cipher suites. +func parseTLSCipherSuites(ciphers []string) ([]tls.CipherSuite, error) { + if len(ciphers) == 0 { + return nil, nil + } + + var unsupported []string + var supported []tls.CipherSuite + // A custom set of supported ciphers. + allCiphers := append(tls.CipherSuites(), tls.InsecureCipherSuites()...) + for _, cipher := range ciphers { + // For each cipher specified by the client, find the cipher in the + // list of golang supported ciphers. + var found *tls.CipherSuite + for _, supported := range allCiphers { + if strings.EqualFold(supported.Name, cipher) { + found = supported + break + } + } + + if found == nil { + unsupported = append(unsupported, cipher) + continue + } + + supported = append(supported, *found) + } + + if len(unsupported) > 0 { + return nil, xerrors.Errorf("unsupported tls ciphers specified: %s", strings.Join(unsupported, ", ")) + } + + return supported, nil +} + +// hasSupportedVersion is a helper function that returns true if the list +// of supported versions contains a version between min and max. +// If the versions list is outside the min/max, then it returns false. +func hasSupportedVersion(min, max uint16, versions []uint16) bool { + for _, v := range versions { + if v >= min && v <= max { + // If one version is in between min/max, return true. + return true + } + } + return false +} + func configureOIDCPKI(orig *oauth2.Config, keyFile string, certFile string) (*oauthpki.Config, error) { // Read the files keyData, err := os.ReadFile(keyFile) @@ -2078,7 +2219,8 @@ func ConfigureTraceProvider( return tracerProvider, sqlDriver, closeTracing } -func ConfigureHTTPServers(inv *clibase.Invocation, cfg *codersdk.DeploymentValues) (_ *HTTPServers, err error) { +func ConfigureHTTPServers(logger slog.Logger, inv *clibase.Invocation, cfg *codersdk.DeploymentValues) (_ *HTTPServers, err error) { + ctx := inv.Context() httpServers := &HTTPServers{} defer func() { if err != nil { @@ -2154,16 +2296,20 @@ func ConfigureHTTPServers(inv *clibase.Invocation, cfg *codersdk.DeploymentValue // DEPRECATED: This redirect used to default to true. // It made more sense to have the redirect be opt-in. if inv.Environ.Get("CODER_TLS_REDIRECT_HTTP") == "true" || inv.ParsedFlags().Changed("tls-redirect-http-to-https") { - cliui.Warn(inv.Stderr, "--tls-redirect-http-to-https is deprecated, please use --redirect-to-access-url instead") + logger.Warn(ctx, "--tls-redirect-http-to-https is deprecated, please use --redirect-to-access-url instead") cfg.RedirectToAccessURL = cfg.TLS.RedirectHTTP } - tlsConfig, err := configureTLS( + tlsConfig, err := configureServerTLS( + ctx, + logger, cfg.TLS.MinVersion.String(), cfg.TLS.ClientAuth.String(), cfg.TLS.CertFiles, cfg.TLS.KeyFiles, cfg.TLS.ClientCAFile.String(), + cfg.TLS.SupportedCiphers.Value(), + cfg.TLS.AllowInsecureCiphers.Value(), ) if err != nil { return nil, xerrors.Errorf("configure tls: %w", err) diff --git a/cli/server_internal_test.go b/cli/server_internal_test.go new file mode 100644 index 0000000000000..199445fb7656b --- /dev/null +++ b/cli/server_internal_test.go @@ -0,0 +1,102 @@ +package cli + +import ( + "bytes" + "context" + "crypto/tls" + "testing" + + "github.com/stretchr/testify/assert" + + "cdr.dev/slog/sloggers/sloghuman" + "github.com/stretchr/testify/require" + + "cdr.dev/slog" +) + +func Test_configureCipherSuites(t *testing.T) { + t.Parallel() + + cipherNames := func(ciphers []*tls.CipherSuite) []string { + var names []string + for _, c := range ciphers { + names = append(names, c.Name) + } + return names + } + + cipherIDs := func(ciphers []*tls.CipherSuite) []uint16 { + var ids []uint16 + for _, c := range ciphers { + ids = append(ids, c.ID) + } + return ids + } + + tests := []struct { + name string + wantErr string + wantWarnings []string + inputCiphers []string + minTLS uint16 + maxTLS uint16 + allowInsecure bool + expectCiphers []uint16 + }{ + { + name: "AllowInsecure", + minTLS: tls.VersionTLS10, + maxTLS: tls.VersionTLS13, + inputCiphers: append(cipherNames(tls.CipherSuites()), tls.InsecureCipherSuites()[0].Name), + allowInsecure: true, + wantWarnings: []string{ + "insecure tls cipher specified", + }, + expectCiphers: append(cipherIDs(tls.CipherSuites()), tls.InsecureCipherSuites()[0].ID), + }, + // Errors + { + name: "NoCiphers", + minTLS: tls.VersionTLS10, + maxTLS: tls.VersionTLS13, + wantErr: "no tls ciphers supported", + }, + { + name: "InsecureNotAllowed", + minTLS: tls.VersionTLS10, + maxTLS: tls.VersionTLS13, + inputCiphers: append(cipherNames(tls.CipherSuites()), tls.InsecureCipherSuites()[0].Name), + wantErr: "insecure tls ciphers specified", + }, + { + name: "TLS1.3", + minTLS: tls.VersionTLS13, + maxTLS: tls.VersionTLS13, + inputCiphers: cipherNames(tls.CipherSuites()), + wantErr: "tls ciphers cannot be specified when using minimum tls version 1.3", + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + ctx := context.Background() + var out bytes.Buffer + logger := slog.Make(sloghuman.Sink(&out)) + + found, err := configureCipherSuites(ctx, logger, tt.inputCiphers, tt.allowInsecure, tt.minTLS, tt.maxTLS) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + } else { + require.NoError(t, err, "no error") + require.ElementsMatch(t, tt.expectCiphers, found, "expected ciphers") + if len(tt.wantWarnings) > 0 { + logger.Sync() + for _, w := range tt.wantWarnings { + assert.Contains(t, out.String(), w, "expected warning") + } + } + } + }) + } +} diff --git a/codersdk/deployment.go b/codersdk/deployment.go index c53ba8d055194..1b0cbf62355d4 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -305,16 +305,18 @@ type TelemetryConfig struct { } type TLSConfig struct { - Enable clibase.Bool `json:"enable" typescript:",notnull"` - Address clibase.HostPort `json:"address" typescript:",notnull"` - RedirectHTTP clibase.Bool `json:"redirect_http" typescript:",notnull"` - CertFiles clibase.StringArray `json:"cert_file" typescript:",notnull"` - ClientAuth clibase.String `json:"client_auth" typescript:",notnull"` - ClientCAFile clibase.String `json:"client_ca_file" typescript:",notnull"` - KeyFiles clibase.StringArray `json:"key_file" typescript:",notnull"` - MinVersion clibase.String `json:"min_version" typescript:",notnull"` - ClientCertFile clibase.String `json:"client_cert_file" typescript:",notnull"` - ClientKeyFile clibase.String `json:"client_key_file" typescript:",notnull"` + Enable clibase.Bool `json:"enable" typescript:",notnull"` + Address clibase.HostPort `json:"address" typescript:",notnull"` + RedirectHTTP clibase.Bool `json:"redirect_http" typescript:",notnull"` + CertFiles clibase.StringArray `json:"cert_file" typescript:",notnull"` + ClientAuth clibase.String `json:"client_auth" typescript:",notnull"` + ClientCAFile clibase.String `json:"client_ca_file" typescript:",notnull"` + KeyFiles clibase.StringArray `json:"key_file" typescript:",notnull"` + MinVersion clibase.String `json:"min_version" typescript:",notnull"` + ClientCertFile clibase.String `json:"client_cert_file" typescript:",notnull"` + ClientKeyFile clibase.String `json:"client_key_file" typescript:",notnull"` + SupportedCiphers clibase.StringArray `json:"supported_ciphers" typescript:",notnull"` + AllowInsecureCiphers clibase.Bool `json:"allow_insecure_ciphers" typescript:",notnull"` } type TraceConfig struct { diff --git a/enterprise/cli/proxyserver.go b/enterprise/cli/proxyserver.go index 6829b155eb2fd..0d7e92531342f 100644 --- a/enterprise/cli/proxyserver.go +++ b/enterprise/cli/proxyserver.go @@ -158,7 +158,7 @@ func (*RootCmd) proxyServer() *clibase.Cmd { logger.Debug(ctx, "tracing closed", slog.Error(traceCloseErr)) }() - httpServers, err := cli.ConfigureHTTPServers(inv, cfg) + httpServers, err := cli.ConfigureHTTPServers(logger, inv, cfg) if err != nil { return xerrors.Errorf("configure http(s): %w", err) } From a8c9b769e0866d2edb59a99180e3feea7624abe6 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 2 Nov 2023 15:00:20 -0500 Subject: [PATCH 02/12] add unit tests --- cli/server.go | 7 ++-- cli/server_internal_test.go | 70 +++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/cli/server.go b/cli/server.go index 358f35790818e..f78870085c98a 100644 --- a/cli/server.go +++ b/cli/server.go @@ -1021,7 +1021,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. r.Verbosef(inv, "Shutting down provisioner daemon %d...", id) err := shutdownWithTimeout(provisionerDaemon.Shutdown, 5*time.Second) if err != nil { - cliui.Errorf(inv.Stderr, "Failed to shutdown provisioner daemon %d: %s\n", id, err) + cliui.Errorf(inv.Stderr, "Failed to shut down provisioner daemon %d: %s\n", id, err) return } err = provisionerDaemon.Close() @@ -1500,6 +1500,9 @@ func configureServerTLS(ctx context.Context, logger slog.Logger, tlsMinVersion, } func configureCipherSuites(ctx context.Context, logger slog.Logger, ciphers []string, allowInsecureCiphers bool, minTLS, maxTLS uint16) ([]uint16, error) { + if minTLS > maxTLS { + return nil, xerrors.Errorf("minimum tls version cannot be greater than maximum tls version") + } if minTLS >= tls.VersionTLS13 { // The cipher suites config option is ignored for tls 1.3 and higher. // So this user flag is a no-op if the min version is 1.3. @@ -1536,7 +1539,7 @@ func configureCipherSuites(ctx context.Context, logger slog.Logger, ciphers []st for _, sv := range cipher.SupportedVersions { versions = append(versions, tls.VersionName(sv)) } - logger.Warn(ctx, "cipher not supported for tls versions allowed, cipher will not be used", + logger.Warn(ctx, "cipher not supported for tls versions enabled, cipher will not be used", slog.F("cipher", cipher.Name), slog.F("cipher_supported_versions", strings.Join(versions, ",")), slog.F("server_min_version", tls.VersionName(minTLS)), diff --git a/cli/server_internal_test.go b/cli/server_internal_test.go index 199445fb7656b..e41cf8455ec3f 100644 --- a/cli/server_internal_test.go +++ b/cli/server_internal_test.go @@ -33,6 +33,16 @@ func Test_configureCipherSuites(t *testing.T) { return ids } + cipherByName := func(cipher string) *tls.CipherSuite { + for _, c := range append(tls.CipherSuites(), tls.InsecureCipherSuites()...) { + if cipher == c.Name { + c := c + return c + } + } + return nil + } + tests := []struct { name string wantErr string @@ -43,6 +53,14 @@ func Test_configureCipherSuites(t *testing.T) { allowInsecure bool expectCiphers []uint16 }{ + { + name: "AllSecure", + minTLS: tls.VersionTLS10, + maxTLS: tls.VersionTLS13, + inputCiphers: cipherNames(tls.CipherSuites()), + wantWarnings: []string{}, + expectCiphers: cipherIDs(tls.CipherSuites()), + }, { name: "AllowInsecure", minTLS: tls.VersionTLS10, @@ -54,7 +72,45 @@ func Test_configureCipherSuites(t *testing.T) { }, expectCiphers: append(cipherIDs(tls.CipherSuites()), tls.InsecureCipherSuites()[0].ID), }, + { + name: "AllInsecure", + minTLS: tls.VersionTLS10, + maxTLS: tls.VersionTLS13, + inputCiphers: append(cipherNames(tls.CipherSuites()), cipherNames(tls.InsecureCipherSuites())...), + allowInsecure: true, + wantWarnings: []string{ + "insecure tls cipher specified", + }, + expectCiphers: append(cipherIDs(tls.CipherSuites()), cipherIDs(tls.InsecureCipherSuites())...), + }, + { + // Providing ciphers that are not compatible with any tls version + // enabled should generate a warning. + name: "ExcessiveCiphers", + minTLS: tls.VersionTLS10, + maxTLS: tls.VersionTLS11, + inputCiphers: []string{ + "TLS_RSA_WITH_AES_128_CBC_SHA", + // Only for TLS 1.3 + "TLS_AES_128_GCM_SHA256", + }, + allowInsecure: true, + wantWarnings: []string{ + "cipher not supported for tls versions", + }, + expectCiphers: cipherIDs([]*tls.CipherSuite{ + cipherByName("TLS_RSA_WITH_AES_128_CBC_SHA"), + cipherByName("TLS_AES_128_GCM_SHA256"), + }), + }, // Errors + { + name: "NotRealCiphers", + minTLS: tls.VersionTLS10, + maxTLS: tls.VersionTLS13, + inputCiphers: []string{"RSA-Fake"}, + wantErr: "unsupported tls ciphers", + }, { name: "NoCiphers", minTLS: tls.VersionTLS10, @@ -75,6 +131,20 @@ func Test_configureCipherSuites(t *testing.T) { inputCiphers: cipherNames(tls.CipherSuites()), wantErr: "tls ciphers cannot be specified when using minimum tls version 1.3", }, + { + name: "TLSUnsupported", + minTLS: tls.VersionTLS10, + maxTLS: tls.VersionTLS13, + // TLS_RSA_WITH_AES_128_GCM_SHA256 only supports tls 1.2 + inputCiphers: []string{"TLS_RSA_WITH_AES_128_GCM_SHA256"}, + wantErr: "no tls ciphers supported for tls versions", + }, + { + name: "Min>Max", + minTLS: tls.VersionTLS13, + maxTLS: tls.VersionTLS12, + wantErr: "minimum tls version cannot be greater than maximum tls version", + }, } for _, tt := range tests { tt := tt From f9c139a9608a98eab66f24abeca5c9e38dd64c0f Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 2 Nov 2023 16:42:06 -0500 Subject: [PATCH 03/12] Add tls options for ciphers --- codersdk/deployment.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 1b0cbf62355d4..586de0774849f 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -742,6 +742,28 @@ when required by your organization's security policy.`, YAML: "clientKeyFile", Annotations: clibase.Annotations{}.Mark(annotationExternalProxies, "true"), }, + { + Name: "TLS Ciphers", + Description: "Specify specific TLS ciphers that allowed to be used. See https://github.com/golang/go/blob/master/src/crypto/tls/cipher_suites.go#L53-L75.", + Flag: "tls-ciphers", + Env: "CODER_TLS_CIPHERS", + Default: "", + Value: &c.TLS.SupportedCiphers, + Group: &deploymentGroupNetworkingTLS, + YAML: "tlsCiphers", + Annotations: clibase.Annotations{}.Mark(annotationExternalProxies, "true"), + }, + { + Name: "TLS Allow Insecure Ciphers", + Description: "By default, only ciphers marked as 'secure' are allowed to be used. See https://github.com/golang/go/blob/master/src/crypto/tls/cipher_suites.go#L82-L95.", + Flag: "tls-allow-insecure-ciphers", + Env: "CODER_TLS_ALLOW_INSECURE_CIPHERS", + Default: "false", + Value: &c.TLS.AllowInsecureCiphers, + Group: &deploymentGroupNetworkingTLS, + YAML: "tlsAllowInsecureCiphers", + Annotations: clibase.Annotations{}.Mark(annotationExternalProxies, "true"), + }, // Derp settings { Name: "DERP Server Enable", From 54b64f94ef701c8dbfbf6955c2acd81fd682d8db Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 2 Nov 2023 16:48:43 -0500 Subject: [PATCH 04/12] plumb through deployment options for ciphers --- cli/server_internal_test.go | 3 +-- coderd/apidoc/docs.go | 9 ++++++++ coderd/apidoc/swagger.json | 9 ++++++++ docs/api/general.md | 4 +++- docs/api/schemas.md | 38 ++++++++++++++++++++-------------- docs/cli/server.md | 21 +++++++++++++++++++ site/src/api/typesGenerated.ts | 2 ++ 7 files changed, 68 insertions(+), 18 deletions(-) diff --git a/cli/server_internal_test.go b/cli/server_internal_test.go index e41cf8455ec3f..5e8fdf25253b9 100644 --- a/cli/server_internal_test.go +++ b/cli/server_internal_test.go @@ -7,11 +7,10 @@ import ( "testing" "github.com/stretchr/testify/assert" - - "cdr.dev/slog/sloggers/sloghuman" "github.com/stretchr/testify/require" "cdr.dev/slog" + "cdr.dev/slog/sloggers/sloghuman" ) func Test_configureCipherSuites(t *testing.T) { diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 991f5095870f3..9b9f60c6da0ad 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -9870,6 +9870,9 @@ const docTemplate = `{ "address": { "$ref": "#/definitions/clibase.HostPort" }, + "allow_insecure_ciphers": { + "type": "boolean" + }, "cert_file": { "type": "array", "items": { @@ -9902,6 +9905,12 @@ const docTemplate = `{ }, "redirect_http": { "type": "boolean" + }, + "supported_ciphers": { + "type": "array", + "items": { + "type": "string" + } } } }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 34c599bb9a04b..9366b9581476e 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -8906,6 +8906,9 @@ "address": { "$ref": "#/definitions/clibase.HostPort" }, + "allow_insecure_ciphers": { + "type": "boolean" + }, "cert_file": { "type": "array", "items": { @@ -8938,6 +8941,12 @@ }, "redirect_http": { "type": "boolean" + }, + "supported_ciphers": { + "type": "array", + "items": { + "type": "string" + } } } }, diff --git a/docs/api/general.md b/docs/api/general.md index 577781136ef61..6d000836670ea 100644 --- a/docs/api/general.md +++ b/docs/api/general.md @@ -368,6 +368,7 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \ "host": "string", "port": "string" }, + "allow_insecure_ciphers": true, "cert_file": ["string"], "client_auth": "string", "client_ca_file": "string", @@ -376,7 +377,8 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \ "enable": true, "key_file": ["string"], "min_version": "string", - "redirect_http": true + "redirect_http": true, + "supported_ciphers": ["string"] }, "trace": { "capture_logs": true, diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 23985414d0727..4111224c08ee3 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -2289,6 +2289,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "host": "string", "port": "string" }, + "allow_insecure_ciphers": true, "cert_file": ["string"], "client_auth": "string", "client_ca_file": "string", @@ -2297,7 +2298,8 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "enable": true, "key_file": ["string"], "min_version": "string", - "redirect_http": true + "redirect_http": true, + "supported_ciphers": ["string"] }, "trace": { "capture_logs": true, @@ -2658,6 +2660,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "host": "string", "port": "string" }, + "allow_insecure_ciphers": true, "cert_file": ["string"], "client_auth": "string", "client_ca_file": "string", @@ -2666,7 +2669,8 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "enable": true, "key_file": ["string"], "min_version": "string", - "redirect_http": true + "redirect_http": true, + "supported_ciphers": ["string"] }, "trace": { "capture_logs": true, @@ -4278,6 +4282,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "host": "string", "port": "string" }, + "allow_insecure_ciphers": true, "cert_file": ["string"], "client_auth": "string", "client_ca_file": "string", @@ -4286,24 +4291,27 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "enable": true, "key_file": ["string"], "min_version": "string", - "redirect_http": true + "redirect_http": true, + "supported_ciphers": ["string"] } ``` ### Properties -| Name | Type | Required | Restrictions | Description | -| ------------------ | ------------------------------------ | -------- | ------------ | ----------- | -| `address` | [clibase.HostPort](#clibasehostport) | false | | | -| `cert_file` | array of string | false | | | -| `client_auth` | string | false | | | -| `client_ca_file` | string | false | | | -| `client_cert_file` | string | false | | | -| `client_key_file` | string | false | | | -| `enable` | boolean | false | | | -| `key_file` | array of string | false | | | -| `min_version` | string | false | | | -| `redirect_http` | boolean | false | | | +| Name | Type | Required | Restrictions | Description | +| ------------------------ | ------------------------------------ | -------- | ------------ | ----------- | +| `address` | [clibase.HostPort](#clibasehostport) | false | | | +| `allow_insecure_ciphers` | boolean | false | | | +| `cert_file` | array of string | false | | | +| `client_auth` | string | false | | | +| `client_ca_file` | string | false | | | +| `client_cert_file` | string | false | | | +| `client_key_file` | string | false | | | +| `enable` | boolean | false | | | +| `key_file` | array of string | false | | | +| `min_version` | string | false | | | +| `redirect_http` | boolean | false | | | +| `supported_ciphers` | array of string | false | | | ## codersdk.TelemetryConfig diff --git a/docs/cli/server.md b/docs/cli/server.md index 9258f0f92f7e6..93a07b72f98a8 100644 --- a/docs/cli/server.md +++ b/docs/cli/server.md @@ -874,6 +874,17 @@ Two optional fields can be set in the Strict-Transport-Security header; 'include HTTPS bind address of the server. +### --tls-allow-insecure-ciphers + +| | | +| ----------- | --------------------------------------------------- | +| Type | bool | +| Environment | $CODER_TLS_ALLOW_INSECURE_CIPHERS | +| YAML | networking.tls.tlsAllowInsecureCiphers | +| Default | false | + +By default, only ciphers marked as 'secure' are allowed to be used. See https://github.com/golang/go/blob/master/src/crypto/tls/cipher_suites.go#L82-L95. + ### --tls-cert-file | | | @@ -884,6 +895,16 @@ HTTPS bind address of the server. Path to each certificate for TLS. It requires a PEM-encoded file. To configure the listener to use a CA certificate, concatenate the primary certificate and the CA certificate together. The primary certificate should appear first in the combined file. +### --tls-ciphers + +| | | +| ----------- | -------------------------------------- | +| Type | string-array | +| Environment | $CODER_TLS_CIPHERS | +| YAML | networking.tls.tlsCiphers | + +Specify specific TLS ciphers that allowed to be used. See https://github.com/golang/go/blob/master/src/crypto/tls/cipher_suites.go#L53-L75. + ### --tls-client-auth | | | diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index ce7facdc55522..babbac1ef0a25 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -882,6 +882,8 @@ export interface TLSConfig { readonly min_version: string; readonly client_cert_file: string; readonly client_key_file: string; + readonly supported_ciphers: string[]; + readonly allow_insecure_ciphers: boolean; } // From codersdk/deployment.go From 636d31ca07bdf46c949e4da2c0f5abaa5f3cd621 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 3 Nov 2023 11:17:07 -0500 Subject: [PATCH 05/12] tls.VersionName is go 1.21, copy the function --- cli/server.go | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/cli/server.go b/cli/server.go index f78870085c98a..ba33091e6c01e 100644 --- a/cli/server.go +++ b/cli/server.go @@ -1537,13 +1537,13 @@ func configureCipherSuites(ctx context.Context, logger slog.Logger, ciphers []st if !hasSupportedVersion(minTLS, maxTLS, cipher.SupportedVersions) { versions := make([]string, 0, len(cipher.SupportedVersions)) for _, sv := range cipher.SupportedVersions { - versions = append(versions, tls.VersionName(sv)) + versions = append(versions, versionName(sv)) } logger.Warn(ctx, "cipher not supported for tls versions enabled, cipher will not be used", slog.F("cipher", cipher.Name), slog.F("cipher_supported_versions", strings.Join(versions, ",")), - slog.F("server_min_version", tls.VersionName(minTLS)), - slog.F("server_max_version", tls.VersionName(maxTLS)), + slog.F("server_min_version", versionName(minTLS)), + slog.F("server_max_version", versionName(maxTLS)), ) } @@ -1567,7 +1567,7 @@ func configureCipherSuites(ctx context.Context, logger slog.Logger, ciphers []st continue // v1.3 ignores configured cipher suites. } if !covered { - missedVersions = append(missedVersions, tls.VersionName(version)) + missedVersions = append(missedVersions, versionName(version)) } } if len(missedVersions) > 0 { @@ -1631,6 +1631,25 @@ func hasSupportedVersion(min, max uint16, versions []uint16) bool { return false } +// versionName is tls.VersionName in go 1.21. +// Until the switch, the function is copied locally. +func versionName(version uint16) string { + switch version { + case tls.VersionSSL30: + return "SSLv3" + case tls.VersionTLS10: + return "TLS 1.0" + case tls.VersionTLS11: + return "TLS 1.1" + case tls.VersionTLS12: + return "TLS 1.2" + case tls.VersionTLS13: + return "TLS 1.3" + default: + return fmt.Sprintf("0x%04X", version) + } +} + func configureOIDCPKI(orig *oauth2.Config, keyFile string, certFile string) (*oauthpki.Config, error) { // Read the files keyData, err := os.ReadFile(keyFile) From 284653fcfa6ee95bafde5d8f0cd7c5b7c76b51ff Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 3 Nov 2023 11:24:41 -0500 Subject: [PATCH 06/12] update golden files --- cli/testdata/coder_server_--help.golden | 9 +++++++++ cli/testdata/server-config.yaml.golden | 8 ++++++++ enterprise/cli/testdata/coder_server_--help.golden | 9 +++++++++ 3 files changed, 26 insertions(+) diff --git a/cli/testdata/coder_server_--help.golden b/cli/testdata/coder_server_--help.golden index 68953eb14e434..8981ea5f30d8f 100644 --- a/cli/testdata/coder_server_--help.golden +++ b/cli/testdata/coder_server_--help.golden @@ -249,12 +249,21 @@ can safely ignore these settings. --tls-address host:port, $CODER_TLS_ADDRESS (default: 127.0.0.1:3443) HTTPS bind address of the server. + --tls-allow-insecure-ciphers bool, $CODER_TLS_ALLOW_INSECURE_CIPHERS (default: false) + By default, only ciphers marked as 'secure' are allowed to be used. + See + https://github.com/golang/go/blob/master/src/crypto/tls/cipher_suites.go#L82-L95. + --tls-cert-file string-array, $CODER_TLS_CERT_FILE Path to each certificate for TLS. It requires a PEM-encoded file. To configure the listener to use a CA certificate, concatenate the primary certificate and the CA certificate together. The primary certificate should appear first in the combined file. + --tls-ciphers string-array, $CODER_TLS_CIPHERS + Specify specific TLS ciphers that allowed to be used. See + https://github.com/golang/go/blob/master/src/crypto/tls/cipher_suites.go#L53-L75. + --tls-client-auth string, $CODER_TLS_CLIENT_AUTH (default: none) Policy the server will follow for TLS Client Authentication. Accepted values are "none", "request", "require-any", "verify-if-given", or diff --git a/cli/testdata/server-config.yaml.golden b/cli/testdata/server-config.yaml.golden index 31304d68e1633..a52ec496de6c1 100644 --- a/cli/testdata/server-config.yaml.golden +++ b/cli/testdata/server-config.yaml.golden @@ -83,6 +83,14 @@ networking: # Path to key for client TLS authentication. It requires a PEM-encoded file. # (default: , type: string) clientKeyFile: "" + # Specify specific TLS ciphers that allowed to be used. See + # https://github.com/golang/go/blob/master/src/crypto/tls/cipher_suites.go#L53-L75. + # (default: , type: string-array) + tlsCiphers: [] + # By default, only ciphers marked as 'secure' are allowed to be used. See + # https://github.com/golang/go/blob/master/src/crypto/tls/cipher_suites.go#L82-L95. + # (default: false, type: bool) + tlsAllowInsecureCiphers: false # Controls if the 'Strict-Transport-Security' header is set on all static file # responses. This header should only be set if the server is accessed via HTTPS. # This value is the MaxAge in seconds of the header. diff --git a/enterprise/cli/testdata/coder_server_--help.golden b/enterprise/cli/testdata/coder_server_--help.golden index f4bb522f5ec62..6e3b4a502ed27 100644 --- a/enterprise/cli/testdata/coder_server_--help.golden +++ b/enterprise/cli/testdata/coder_server_--help.golden @@ -250,12 +250,21 @@ can safely ignore these settings. --tls-address host:port, $CODER_TLS_ADDRESS (default: 127.0.0.1:3443) HTTPS bind address of the server. + --tls-allow-insecure-ciphers bool, $CODER_TLS_ALLOW_INSECURE_CIPHERS (default: false) + By default, only ciphers marked as 'secure' are allowed to be used. + See + https://github.com/golang/go/blob/master/src/crypto/tls/cipher_suites.go#L82-L95. + --tls-cert-file string-array, $CODER_TLS_CERT_FILE Path to each certificate for TLS. It requires a PEM-encoded file. To configure the listener to use a CA certificate, concatenate the primary certificate and the CA certificate together. The primary certificate should appear first in the combined file. + --tls-ciphers string-array, $CODER_TLS_CIPHERS + Specify specific TLS ciphers that allowed to be used. See + https://github.com/golang/go/blob/master/src/crypto/tls/cipher_suites.go#L53-L75. + --tls-client-auth string, $CODER_TLS_CLIENT_AUTH (default: none) Policy the server will follow for TLS Client Authentication. Accepted values are "none", "request", "require-any", "verify-if-given", or From f4a679ed27a32fb2c56698eb88a49230d38faa92 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 3 Nov 2023 11:34:17 -0500 Subject: [PATCH 07/12] Nolint --- cli/server.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cli/server.go b/cli/server.go index ba33091e6c01e..91705845e699f 100644 --- a/cli/server.go +++ b/cli/server.go @@ -1414,6 +1414,8 @@ func generateSelfSignedCertificate() (*tls.Certificate, error) { // configureServerTLS returns the TLS config used for the Coderd server // connections to clients. A logger is passed in to allow printing warning // messages that do not block startup. +// +//nolint:revive func configureServerTLS(ctx context.Context, logger slog.Logger, tlsMinVersion, tlsClientAuth string, tlsCertFiles, tlsKeyFiles []string, tlsClientCAFile string, ciphers []string, allowInsecureCiphers bool) (*tls.Config, error) { tlsConfig := &tls.Config{ MinVersion: tls.VersionTLS12, From 9ffabc9ab8bdf2475d6c79421ef166b89290ff0b Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 3 Nov 2023 11:44:16 -0500 Subject: [PATCH 08/12] fixup! Nolint --- cli/server.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/server.go b/cli/server.go index 91705845e699f..6ae0160465c3d 100644 --- a/cli/server.go +++ b/cli/server.go @@ -1501,6 +1501,7 @@ func configureServerTLS(ctx context.Context, logger slog.Logger, tlsMinVersion, return tlsConfig, nil } +//nolint:revive func configureCipherSuites(ctx context.Context, logger slog.Logger, ciphers []string, allowInsecureCiphers bool, minTLS, maxTLS uint16) ([]uint16, error) { if minTLS > maxTLS { return nil, xerrors.Errorf("minimum tls version cannot be greater than maximum tls version") From fc034993ffe5eff5297f835b4e140e157d496a39 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 3 Nov 2023 11:51:17 -0500 Subject: [PATCH 09/12] Improve error message --- cli/server.go | 7 ++++--- cli/server_internal_test.go | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/cli/server.go b/cli/server.go index 6ae0160465c3d..2c1588fc1492e 100644 --- a/cli/server.go +++ b/cli/server.go @@ -1503,13 +1503,14 @@ func configureServerTLS(ctx context.Context, logger slog.Logger, tlsMinVersion, //nolint:revive func configureCipherSuites(ctx context.Context, logger slog.Logger, ciphers []string, allowInsecureCiphers bool, minTLS, maxTLS uint16) ([]uint16, error) { + fmt.Println(ciphers) if minTLS > maxTLS { - return nil, xerrors.Errorf("minimum tls version cannot be greater than maximum tls version") + return nil, xerrors.Errorf("minimum tls version (%s) cannot be greater than maximum tls version (%s)", versionName(minTLS), versionName(maxTLS)) } if minTLS >= tls.VersionTLS13 { // The cipher suites config option is ignored for tls 1.3 and higher. // So this user flag is a no-op if the min version is 1.3. - return nil, xerrors.Errorf("tls ciphers cannot be specified when using minimum tls version 1.3 or higher") + return nil, xerrors.Errorf("'--tls-ciphers' cannot be specified when using minimum tls version 1.3 or higher, %d ciphers found as input.", len(ciphers)) } // Configure the cipher suites which parses the strings and converts them // to golang cipher suites. @@ -1615,7 +1616,7 @@ func parseTLSCipherSuites(ciphers []string) ([]tls.CipherSuite, error) { } if len(unsupported) > 0 { - return nil, xerrors.Errorf("unsupported tls ciphers specified: %s", strings.Join(unsupported, ", ")) + return nil, xerrors.Errorf("unsupported tls ciphers specified, see https://github.com/golang/go/blob/master/src/crypto/tls/cipher_suites.go#L53-L75: %s", strings.Join(unsupported, ", ")) } return supported, nil diff --git a/cli/server_internal_test.go b/cli/server_internal_test.go index 5e8fdf25253b9..66c9a66aeb6d6 100644 --- a/cli/server_internal_test.go +++ b/cli/server_internal_test.go @@ -128,7 +128,7 @@ func Test_configureCipherSuites(t *testing.T) { minTLS: tls.VersionTLS13, maxTLS: tls.VersionTLS13, inputCiphers: cipherNames(tls.CipherSuites()), - wantErr: "tls ciphers cannot be specified when using minimum tls version 1.3", + wantErr: "'--tls-ciphers' cannot be specified when using minimum tls version 1.3", }, { name: "TLSUnsupported", @@ -142,7 +142,7 @@ func Test_configureCipherSuites(t *testing.T) { name: "Min>Max", minTLS: tls.VersionTLS13, maxTLS: tls.VersionTLS12, - wantErr: "minimum tls version cannot be greater than maximum tls version", + wantErr: "minimum tls version (TLS 1.3) cannot be greater than maximum tls version (TLS 1.2)", }, } for _, tt := range tests { From 17029bf9977687262bcad17365ceb061ecc99e5e Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 3 Nov 2023 11:52:24 -0500 Subject: [PATCH 10/12] fixup! Improve error message --- cli/server.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cli/server.go b/cli/server.go index 2c1588fc1492e..edd62e228fe5b 100644 --- a/cli/server.go +++ b/cli/server.go @@ -1503,7 +1503,6 @@ func configureServerTLS(ctx context.Context, logger slog.Logger, tlsMinVersion, //nolint:revive func configureCipherSuites(ctx context.Context, logger slog.Logger, ciphers []string, allowInsecureCiphers bool, minTLS, maxTLS uint16) ([]uint16, error) { - fmt.Println(ciphers) if minTLS > maxTLS { return nil, xerrors.Errorf("minimum tls version (%s) cannot be greater than maximum tls version (%s)", versionName(minTLS), versionName(maxTLS)) } From c1584bf282533f78b159171cd1af47adce39fb64 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 3 Nov 2023 11:55:30 -0500 Subject: [PATCH 11/12] update comment --- cli/server.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cli/server.go b/cli/server.go index edd62e228fe5b..dc1e656cef080 100644 --- a/cli/server.go +++ b/cli/server.go @@ -1519,6 +1519,8 @@ func configureCipherSuites(ctx context.Context, logger slog.Logger, ciphers []st } // allVersions is all tls versions the server supports. + // We enumerate these to ensure if ciphers are configured, at least + // 1 cipher for each version exists. allVersions := make(map[uint16]bool) for v := minTLS; v <= maxTLS; v++ { allVersions[v] = false From d0353b063e1e9fffd95998482e960021c430775f Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 7 Nov 2023 08:46:18 -0600 Subject: [PATCH 12/12] Fix typo --- cli/server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/server.go b/cli/server.go index dc1e656cef080..3baded2363bf6 100644 --- a/cli/server.go +++ b/cli/server.go @@ -1577,7 +1577,7 @@ func configureCipherSuites(ctx context.Context, logger slog.Logger, ciphers []st } if len(missedVersions) > 0 { return nil, xerrors.Errorf("no tls ciphers supported for tls versions %q."+ - "Add additional ciphers, specify the minimum version to 'tls13, or remove the ciphers configured and rely on the default", + "Add additional ciphers, set the minimum version to 'tls13, or remove the ciphers configured and rely on the default", strings.Join(missedVersions, ",")) }