From 916bdeae94f200d85603a9dcec2fa1bb22b755a8 Mon Sep 17 00:00:00 2001 From: vitshev Date: Tue, 10 Dec 2024 17:12:24 +0100 Subject: [PATCH 1/4] feat(configuration): support mtls auth mod Signed-off-by: vitshev --- configuration/configuration.go | 29 +++++++++++++++++++++++++++++ configuration/configuration_test.go | 29 ++++++++++++++++++----------- docs/content/about/configuration.md | 17 ++++++++++------- registry/registry.go | 21 ++++++++++++++++++++- 4 files changed, 77 insertions(+), 19 deletions(-) diff --git a/configuration/configuration.go b/configuration/configuration.go index f686c6bff..25e09ba7c 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -109,6 +109,10 @@ type Configuration struct { // A file may contain multiple CA certificates encoded as PEM ClientCAs []string `yaml:"clientcas,omitempty"` + // Client certificate authentication mode + // One of: request-client-cert, require-any-client-cert, verify-client-cert-if-given, require-and-verify-client-cert + ClientAuth ClientAuth `yaml:"clientauth,omitempty"` + // Specifies the lowest TLS version allowed MinimumTLS string `yaml:"minimumtls,omitempty"` @@ -899,3 +903,28 @@ func setFieldValue(field reflect.Value, value interface{}) error { } return nil } + +type ClientAuth string + +// UnmarshalYAML implements the yaml.Umarshaler interface +// Unmarshals a string into a ClientAuth, validating that it represents a valid ClientAuth mod +func (clientAuth *ClientAuth) UnmarshalYAML(unmarshal func(interface{}) error) error { + var clientAuthString string + err := unmarshal(&clientAuthString) + if err != nil { + return err + } + + switch clientAuthString { + case "request-client-cert": + case "require-any-client-cert": + case "verify-client-cert-if-given": + case "require-and-verify-client-cert": + default: + return fmt.Errorf("invalid ClientAuth %s Must be one of: request-client-cert, require-any-client-cert, verify-client-cert-if-given, require-and-verify-client-cert", clientAuthString) + } + + *clientAuth = ClientAuth(clientAuthString) + + return nil +} diff --git a/configuration/configuration_test.go b/configuration/configuration_test.go index d55c05fca..5e83866d5 100644 --- a/configuration/configuration_test.go +++ b/configuration/configuration_test.go @@ -78,11 +78,12 @@ var configStruct = Configuration{ RelativeURLs bool `yaml:"relativeurls,omitempty"` DrainTimeout time.Duration `yaml:"draintimeout,omitempty"` TLS struct { - Certificate string `yaml:"certificate,omitempty"` - Key string `yaml:"key,omitempty"` - ClientCAs []string `yaml:"clientcas,omitempty"` - MinimumTLS string `yaml:"minimumtls,omitempty"` - CipherSuites []string `yaml:"ciphersuites,omitempty"` + Certificate string `yaml:"certificate,omitempty"` + Key string `yaml:"key,omitempty"` + ClientCAs []string `yaml:"clientcas,omitempty"` + ClientAuth ClientAuth `yaml:"clientauth,omitempty"` + MinimumTLS string `yaml:"minimumtls,omitempty"` + CipherSuites []string `yaml:"ciphersuites,omitempty"` LetsEncrypt struct { CacheFile string `yaml:"cachefile,omitempty"` Email string `yaml:"email,omitempty"` @@ -106,11 +107,12 @@ var configStruct = Configuration{ } `yaml:"h2c,omitempty"` }{ TLS: struct { - Certificate string `yaml:"certificate,omitempty"` - Key string `yaml:"key,omitempty"` - ClientCAs []string `yaml:"clientcas,omitempty"` - MinimumTLS string `yaml:"minimumtls,omitempty"` - CipherSuites []string `yaml:"ciphersuites,omitempty"` + Certificate string `yaml:"certificate,omitempty"` + Key string `yaml:"key,omitempty"` + ClientCAs []string `yaml:"clientcas,omitempty"` + ClientAuth ClientAuth `yaml:"clientauth,omitempty"` + MinimumTLS string `yaml:"minimumtls,omitempty"` + CipherSuites []string `yaml:"ciphersuites,omitempty"` LetsEncrypt struct { CacheFile string `yaml:"cachefile,omitempty"` Email string `yaml:"email,omitempty"` @@ -118,7 +120,8 @@ var configStruct = Configuration{ DirectoryURL string `yaml:"directoryurl,omitempty"` } `yaml:"letsencrypt,omitempty"` }{ - ClientCAs: []string{"/path/to/ca.pem"}, + ClientCAs: []string{"/path/to/ca.pem"}, + ClientAuth: "verify-client-cert-if-given", }, Headers: http.Header{ "X-Content-Type-Options": []string{"nosniff"}, @@ -202,6 +205,7 @@ http: tls: clientcas: - /path/to/ca.pem + clientauth: verify-client-cert-if-given headers: X-Content-Type-Options: [nosniff] redis: @@ -297,6 +301,7 @@ func (suite *ConfigSuite) TestParseInmemory() { suite.expectedConfig.Storage = Storage{"inmemory": Parameters{}} suite.expectedConfig.Log.Fields = nil suite.expectedConfig.HTTP.TLS.ClientCAs = nil + suite.expectedConfig.HTTP.TLS.ClientAuth = "" suite.expectedConfig.Redis = Redis{} config, err := Parse(bytes.NewReader([]byte(inmemoryConfigYamlV0_1))) @@ -318,6 +323,7 @@ func (suite *ConfigSuite) TestParseIncomplete() { suite.expectedConfig.Notifications = Notifications{} suite.expectedConfig.HTTP.Headers = nil suite.expectedConfig.HTTP.TLS.ClientCAs = nil + suite.expectedConfig.HTTP.TLS.ClientAuth = "" suite.expectedConfig.Redis = Redis{} suite.expectedConfig.Validation.Manifests.Indexes.Platforms = "" @@ -590,6 +596,7 @@ func copyConfig(config Configuration) *Configuration { } configCopy.HTTP.TLS.ClientCAs = make([]string, 0, len(config.HTTP.TLS.ClientCAs)) configCopy.HTTP.TLS.ClientCAs = append(configCopy.HTTP.TLS.ClientCAs, config.HTTP.TLS.ClientCAs...) + configCopy.HTTP.TLS.ClientAuth = config.HTTP.TLS.ClientAuth configCopy.Redis = config.Redis configCopy.Redis.TLS.Certificate = config.Redis.TLS.Certificate diff --git a/docs/content/about/configuration.md b/docs/content/about/configuration.md index 1040de428..1f83575d4 100644 --- a/docs/content/about/configuration.md +++ b/docs/content/about/configuration.md @@ -229,6 +229,7 @@ http: clientcas: - /path/to/ca.pem - /path/to/another/ca.pem + clientauth: require-and-verify-client-cert letsencrypt: cachefile: /path/to/cache-file email: emailused@letsencrypt.com @@ -808,6 +809,7 @@ http: clientcas: - /path/to/ca.pem - /path/to/another/ca.pem + clientauth: require-and-verify-client-cert minimumtls: tls1.2 ciphersuites: - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 @@ -848,13 +850,14 @@ for the server. If you already have a web server running on the same host as the registry, you may prefer to configure TLS on that web server and proxy connections to the registry server. -| Parameter | Required | Description | -|-----------|----------|-------------------------------------------------------| -| `certificate` | yes | Absolute path to the x509 certificate file. | -| `key` | yes | Absolute path to the x509 private key file. | -| `clientcas` | no | An array of absolute paths to x509 CA files. | -| `minimumtls` | no | Minimum TLS version allowed (tls1.0, tls1.1, tls1.2, tls1.3). Defaults to tls1.2 | -| `ciphersuites` | no | Cipher suites allowed. Please see below for allowed values and default. | +| Parameter | Required | Description | +|----------------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `certificate` | yes | Absolute path to the x509 certificate file. | +| `key` | yes | Absolute path to the x509 private key file. | +| `clientcas` | no | An array of absolute paths to x509 CA files. | +| `clientauth` | no | Client certificate authentication mode. This setting determines how the server handles client certificates during the TLS handshake. If clientcas is not provided, TLS Client Authentication is disabled, and the mode is ignored. Allowed (request-client-cert, require-any-client-cert, verify-client-cert-if-given, require-and-verify-client-cert). Defaults to require-and-verify-client-cert | +| `minimumtls` | no | Minimum TLS version allowed (tls1.0, tls1.1, tls1.2, tls1.3). Defaults to tls1.2 | +| `ciphersuites` | no | Cipher suites allowed. Please see below for allowed values and default. | Available cipher suites: - TLS_RSA_WITH_RC4_128_SHA diff --git a/registry/registry.go b/registry/registry.go index 3d3bf1eb1..84c6c3424 100644 --- a/registry/registry.go +++ b/registry/registry.go @@ -79,6 +79,14 @@ var tlsVersions = map[string]uint16{ "tls1.3": tls.VersionTLS13, } +// tlsClientAuth maps user-specified values to TLS Client Authentication constants. +var tlsClientAuth = map[string]tls.ClientAuthType{ + "request-client-cert": tls.RequestClientCert, + "require-any-client-cert": tls.RequireAnyClientCert, + "verify-client-cert-if-given": tls.VerifyClientCertIfGiven, + "require-and-verify-client-cert": tls.RequireAndVerifyClientCert, +} + // defaultLogFormatter is the default formatter to use for logs. const defaultLogFormatter = "text" @@ -298,7 +306,18 @@ func (registry *Registry) ListenAndServe() error { dcontext.GetLogger(registry.app).Debugf("CA Subject: %s", string(subj)) } - tlsConf.ClientAuth = tls.RequireAndVerifyClientCert + if config.HTTP.TLS.ClientAuth != "" { + tlsClientAuthMod, ok := tlsClientAuth[string(config.HTTP.TLS.ClientAuth)] + + if !ok { + return fmt.Errorf("unknown client auth mod '%s' specified for http.tls.clientauth", config.HTTP.TLS.ClientAuth) + } + + tlsConf.ClientAuth = tlsClientAuthMod + } else { + tlsConf.ClientAuth = tls.RequireAndVerifyClientCert + } + tlsConf.ClientCAs = pool } From 328f802b8e87ae1f6bd8319be2f427e588043397 Mon Sep 17 00:00:00 2001 From: vitshev Date: Mon, 16 Dec 2024 18:02:07 +0100 Subject: [PATCH 2/4] fix(configuration): replace string literals with constants Signed-off-by: vitshev --- configuration/configuration.go | 15 +++++++++++---- registry/registry.go | 8 ++++---- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/configuration/configuration.go b/configuration/configuration.go index 25e09ba7c..0da95892b 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -904,6 +904,13 @@ func setFieldValue(field reflect.Value, value interface{}) error { return nil } +const ( + ClientAuthRequestClientCert = "request-client-cert" + ClientAuthRequireAnyClientCert = "require-any-client-cert" + ClientAuthVerifyClientCertIfGiven = "verify-client-cert-if-given" + ClientAuthRequireAndVerifyClientCert = "require-and-verify-client-cert" +) + type ClientAuth string // UnmarshalYAML implements the yaml.Umarshaler interface @@ -916,10 +923,10 @@ func (clientAuth *ClientAuth) UnmarshalYAML(unmarshal func(interface{}) error) e } switch clientAuthString { - case "request-client-cert": - case "require-any-client-cert": - case "verify-client-cert-if-given": - case "require-and-verify-client-cert": + case ClientAuthRequestClientCert: + case ClientAuthRequireAnyClientCert: + case ClientAuthVerifyClientCertIfGiven: + case ClientAuthRequireAndVerifyClientCert: default: return fmt.Errorf("invalid ClientAuth %s Must be one of: request-client-cert, require-any-client-cert, verify-client-cert-if-given, require-and-verify-client-cert", clientAuthString) } diff --git a/registry/registry.go b/registry/registry.go index 84c6c3424..29fc1c401 100644 --- a/registry/registry.go +++ b/registry/registry.go @@ -81,10 +81,10 @@ var tlsVersions = map[string]uint16{ // tlsClientAuth maps user-specified values to TLS Client Authentication constants. var tlsClientAuth = map[string]tls.ClientAuthType{ - "request-client-cert": tls.RequestClientCert, - "require-any-client-cert": tls.RequireAnyClientCert, - "verify-client-cert-if-given": tls.VerifyClientCertIfGiven, - "require-and-verify-client-cert": tls.RequireAndVerifyClientCert, + configuration.ClientAuthRequestClientCert: tls.RequestClientCert, + configuration.ClientAuthRequireAnyClientCert: tls.RequireAnyClientCert, + configuration.ClientAuthVerifyClientCertIfGiven: tls.VerifyClientCertIfGiven, + configuration.ClientAuthRequireAndVerifyClientCert: tls.RequireAndVerifyClientCert, } // defaultLogFormatter is the default formatter to use for logs. From 96c9a85b62ee6ffbe7df85b6fb95f054e6a6399b Mon Sep 17 00:00:00 2001 From: Vitshev Date: Mon, 16 Dec 2024 22:30:37 +0100 Subject: [PATCH 3/4] fix(configuration): replace string literals with constants in error Co-authored-by: Milos Gajdos Signed-off-by: Vitshev --- configuration/configuration.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configuration/configuration.go b/configuration/configuration.go index 0da95892b..f6b88d84b 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -928,7 +928,7 @@ func (clientAuth *ClientAuth) UnmarshalYAML(unmarshal func(interface{}) error) e case ClientAuthVerifyClientCertIfGiven: case ClientAuthRequireAndVerifyClientCert: default: - return fmt.Errorf("invalid ClientAuth %s Must be one of: request-client-cert, require-any-client-cert, verify-client-cert-if-given, require-and-verify-client-cert", clientAuthString) + return fmt.Errorf("invalid ClientAuth %s Must be one of: %s, %s, %s, %s", clientAuthString, ClientAuthRequestClientCert, ClientAuthRequireAnyClientCert, ClientAuthVerifyClientCertIfGiven, ClientAuthRequireAndVerifyClientCert) } *clientAuth = ClientAuth(clientAuthString) From 41a906f0c670b5bdc7f5fc9d00f02219a5d4532b Mon Sep 17 00:00:00 2001 From: vitshev Date: Mon, 16 Dec 2024 22:34:12 +0100 Subject: [PATCH 4/4] fix(configuration): replace string literals with constants in tests Signed-off-by: vitshev --- configuration/configuration_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configuration/configuration_test.go b/configuration/configuration_test.go index 5e83866d5..8718c6596 100644 --- a/configuration/configuration_test.go +++ b/configuration/configuration_test.go @@ -121,7 +121,7 @@ var configStruct = Configuration{ } `yaml:"letsencrypt,omitempty"` }{ ClientCAs: []string{"/path/to/ca.pem"}, - ClientAuth: "verify-client-cert-if-given", + ClientAuth: ClientAuthVerifyClientCertIfGiven, }, Headers: http.Header{ "X-Content-Type-Options": []string{"nosniff"},