Expand Client Auth Type configuration

This commit is contained in:
Jean-Baptiste Doumenjou 2019-07-12 17:50:04 +02:00 committed by Traefiker Bot
parent 7a4b4c941c
commit 2c7cfd1c68
31 changed files with 304 additions and 151 deletions

View file

@ -139,35 +139,39 @@ tls:
minVersion: VersionTLS13 minVersion: VersionTLS13
``` ```
### Mutual Authentication ### Client Authentication (mTLS)
Traefik supports both optional and strict (which is the default) mutual authentication, though the `ClientCA.files` section. Traefik supports mutual authentication, through the `ClientAuth` section.
If present, connections from clients without a certificate will be rejected.
For clients with a certificate, the `optional` option governs the behaviour as follows: For authentication policies that require verification of the client certificate, the certificate authority for the certificate should be set in `ClientAuth.caFiles`.
- When `optional = false`, Traefik accepts connections only from clients presenting a certificate signed by a CA listed in `ClientCA.files`. The `ClientAuth.clientAuthType` option governs the behaviour as follows:
- When `optional = true`, Traefik authorizes connections from clients presenting a certificate signed by an unknown CA.
- `NoClientCert`: disregards any client certificate.
- `RequestClientCert`: asks for a certificate but proceeds anyway if none is provided.
- `RequireAnyClientCert`: requires a certificate but does not verify if it is signed by a CA listed in `ClientAuth.caFiles`.
- `VerifyClientCertIfGiven`: if a certificate is provided, verifies if it is signed by a CA listed in `ClientAuth.caFiles`. Otherwise proceeds without any certificate.
- `RequireAndVerifyClientCert`: requires a certificate, which must be signed by a CA listed in `ClientAuth.caFiles`.
```toml tab="TOML" ```toml tab="TOML"
[tls.options] [tls.options]
[tls.options.default] [tls.options.default]
[tls.options.default.clientCA] [tls.options.default.clientAuth]
# in PEM format. each file can contain multiple CAs. # in PEM format. each file can contain multiple CAs.
files = ["tests/clientca1.crt", "tests/clientca2.crt"] caFiles = ["tests/clientca1.crt", "tests/clientca2.crt"]
optional = false clientAuthType = "RequireAndVerifyClientCert"
``` ```
```yaml tab="YAML" ```yaml tab="YAML"
tls: tls:
options: options:
default: default:
clientCA: clientAuth:
# in PEM format. each file can contain multiple CAs. # in PEM format. each file can contain multiple CAs.
files: caFiles:
- tests/clientca1.crt - tests/clientca1.crt
- tests/clientca2.crt - tests/clientca2.crt
optional: false clientAuthType: RequireAndVerifyClientCert
``` ```
### Cipher Suites ### Cipher Suites

View file

@ -296,7 +296,7 @@ metadata:
namespace: default namespace: default
spec: spec:
minversion: VersionTLS12 minVersion: VersionTLS12
--- ---
apiVersion: traefik.containo.us/v1alpha1 apiVersion: traefik.containo.us/v1alpha1

View file

@ -275,16 +275,16 @@
minVersion = "foobar" minVersion = "foobar"
cipherSuites = ["foobar", "foobar"] cipherSuites = ["foobar", "foobar"]
sniStrict = true sniStrict = true
[tls.options.Options0.clientCA] [tls.options.Options0.clientAuth]
files = ["foobar", "foobar"] caFiles = ["foobar", "foobar"]
optional = true clientAuthType = "VerifyClientCertIfGiven"
[tls.options.Options1] [tls.options.Options1]
minVersion = "foobar" minVersion = "foobar"
cipherSuites = ["foobar", "foobar"] cipherSuites = ["foobar", "foobar"]
sniStrict = true sniStrict = true
[tls.options.Options1.clientCA] [tls.options.Options1.clientAuth]
files = ["foobar", "foobar"] caFiles = ["foobar", "foobar"]
optional = true clientAuthType = "VerifyClientCertIfGiven"
[tls.stores] [tls.stores]
[tls.stores.Store0] [tls.stores.Store0]
[tls.stores.Store0.defaultCertificate] [tls.stores.Store0.defaultCertificate]

View file

@ -303,22 +303,22 @@ tls:
cipherSuites: cipherSuites:
- foobar - foobar
- foobar - foobar
clientCA: clientAuth:
files: caFiles:
- foobar - foobar
- foobar - foobar
optional: true clientAuthType: VerifyClientCertIfGiven
sniStrict: true sniStrict: true
Options1: Options1:
minVersion: foobar minVersion: foobar
cipherSuites: cipherSuites:
- foobar - foobar
- foobar - foobar
clientCA: clientAuth:
files: caFiles:
- foobar - foobar
- foobar - foobar
optional: true clientAuthType: VerifyClientCertIfGiven
sniStrict: true sniStrict: true
stores: stores:
Store0: Store0:

View file

@ -47,6 +47,6 @@
keyFile = "fixtures/https/snitest.org.key" keyFile = "fixtures/https/snitest.org.key"
[tls.options] [tls.options]
[tls.options.default.ClientCA] [tls.options.default.clientAuth]
files = ["fixtures/https/clientca/ca1.crt"] caFiles = ["fixtures/https/clientca/ca1.crt"]
optional = true clientAuthType = "VerifyClientCertIfGiven"

View file

@ -47,5 +47,5 @@
keyFile = "fixtures/https/snitest.org.key" keyFile = "fixtures/https/snitest.org.key"
[tls.options] [tls.options]
[tls.options.default.clientCA] [tls.options.default.clientAuth]
files = ["fixtures/https/clientca/ca1and2.crt"] caFiles = ["fixtures/https/clientca/ca1and2.crt"]

View file

@ -46,6 +46,6 @@
keyFile = "fixtures/https/snitest.org.key" keyFile = "fixtures/https/snitest.org.key"
[tls.options] [tls.options]
[tls.options.default.clientCA] [tls.options.default.clientAuth]
files = ["fixtures/https/clientca/ca1.crt", "fixtures/https/clientca/ca2.crt"] caFiles = ["fixtures/https/clientca/ca1.crt", "fixtures/https/clientca/ca2.crt"]
optional = false clientAuthType = "RequireAndVerifyClientCert"

View file

@ -69,13 +69,13 @@
[tls.options] [tls.options]
[tls.options.foo] [tls.options.foo]
minversion = "VersionTLS11" minVersion = "VersionTLS11"
[tls.options.baz] [tls.options.baz]
minversion = "VersionTLS11" minVersion = "VersionTLS11"
[tls.options.bar] [tls.options.bar]
minversion = "VersionTLS12" minVersion = "VersionTLS12"
[tls.options.default] [tls.options.default]
minversion = "VersionTLS12" minVersion = "VersionTLS12"

View file

@ -5,8 +5,8 @@ metadata:
namespace: default namespace: default
spec: spec:
minversion: VersionTLS12 minVersion: VersionTLS12
snistrict: true sniStrict: true
ciphersuites: cipherSuites:
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
- TLS_RSA_WITH_AES_256_GCM_SHA384 - TLS_RSA_WITH_AES_256_GCM_SHA384

View file

@ -40,7 +40,7 @@
[tls.options] [tls.options]
[tls.options.foo] [tls.options.foo]
minversion = "VersionTLS11" minVersion = "VersionTLS11"
[tls.options.bar] [tls.options.bar]
minversion = "VersionTLS12" minVersion = "VersionTLS12"

View file

@ -23,9 +23,9 @@
## dynamic configuration ## ## dynamic configuration ##
[tls.options] [tls.options]
[tls.options.default.clientCA] [tls.options.default.clientAuth]
files = [ """{{ .RootCertContent }}""" ] caFiles = [ """{{ .RootCertContent }}""" ]
optional = false clientAuthType = "RequireAndVerifyClientCert"
[tls.stores] [tls.stores]
[tls.stores.default.defaultCertificate] [tls.stores.default.defaultCertificate]

View file

@ -460,16 +460,16 @@
minVersion = "foobar" minVersion = "foobar"
cipherSuites = ["foobar", "foobar"] cipherSuites = ["foobar", "foobar"]
sniStrict = true sniStrict = true
[tls.options.TLS0.clientCA] [tls.options.TLS0.clientAuth]
files = ["foobar", "foobar"] caFiles = ["foobar", "foobar"]
optional = true clientAuthType = "VerifyClientCertIfGiven"
[tls.options.TLS1] [tls.options.TLS1]
minVersion = "foobar" minVersion = "foobar"
cipherSuites = ["foobar", "foobar"] cipherSuites = ["foobar", "foobar"]
sniStrict = true sniStrict = true
[tls.options.TLS1.clientCA] [tls.options.TLS1.clientAuth]
files = ["foobar", "foobar"] caFiles = ["foobar", "foobar"]
optional = true clientAuthType = "VerifyClientCertIfGiven"
[tls.stores] [tls.stores]
[tls.stores.Store0] [tls.stores.Store0]
[tls.stores.Store0.defaultCertificate] [tls.stores.Store0.defaultCertificate]

View file

@ -222,7 +222,7 @@ func (p *passTLSClientCert) modifyRequestHeaders(logger logrus.FieldLogger, r *h
if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 { if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 {
r.Header.Set(xForwardedTLSClientCert, getXForwardedTLSClientCert(logger, r.TLS.PeerCertificates)) r.Header.Set(xForwardedTLSClientCert, getXForwardedTLSClientCert(logger, r.TLS.PeerCertificates))
} else { } else {
logger.Warn("Try to extract certificate on a request without TLS") logger.Warn("Tried to extract a certificate on a request without mutual TLS")
} }
} }
@ -231,7 +231,7 @@ func (p *passTLSClientCert) modifyRequestHeaders(logger logrus.FieldLogger, r *h
headerContent := p.getXForwardedTLSClientCertInfo(r.TLS.PeerCertificates) headerContent := p.getXForwardedTLSClientCertInfo(r.TLS.PeerCertificates)
r.Header.Set(xForwardedTLSClientCertInfo, url.QueryEscape(headerContent)) r.Header.Set(xForwardedTLSClientCertInfo, url.QueryEscape(headerContent))
} else { } else {
logger.Warn("Try to extract certificate on a request without TLS") logger.Warn("Tried to extract a certificate on a request without mutual TLS")
} }
} }
} }

View file

@ -25,17 +25,17 @@ metadata:
namespace: default namespace: default
spec: spec:
minversion: VersionTLS12 minVersion: VersionTLS12
snistrict: true sniStrict: true
ciphersuites: cipherSuites:
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
- TLS_RSA_WITH_AES_256_GCM_SHA384 - TLS_RSA_WITH_AES_256_GCM_SHA384
clientca: clientAuth:
secretnames: secretNames:
- secretCA1 - secretCA1
- secretUnknown - secretUnknown
- emptySecret - emptySecret
optional: true clientAuthType: VerifyClientCertIfGiven
--- ---
apiVersion: v1 apiVersion: v1

View file

@ -25,16 +25,16 @@ metadata:
namespace: default namespace: default
spec: spec:
minversion: VersionTLS12 minVersion: VersionTLS12
snistrict: true sniStrict: true
ciphersuites: cipherSuites:
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
- TLS_RSA_WITH_AES_256_GCM_SHA384 - TLS_RSA_WITH_AES_256_GCM_SHA384
clientca: clientAuth:
secretnames: secretNames:
- secretCA1 - secretCA1
- secretCA2 - secretCA2
optional: true clientAuthType: VerifyClientCertIfGiven
--- ---
apiVersion: v1 apiVersion: v1

View file

@ -25,16 +25,16 @@ metadata:
namespace: myns namespace: myns
spec: spec:
minversion: VersionTLS12 minVersion: VersionTLS12
snistrict: true sniStrict: true
ciphersuites: cipherSuites:
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
- TLS_RSA_WITH_AES_256_GCM_SHA384 - TLS_RSA_WITH_AES_256_GCM_SHA384
clientca: clientAuth:
secretnames: secretNames:
- secretCA1 - secretCA1
- secretCA2 - secretCA2
optional: true clientAuthType: VerifyClientCertIfGiven
--- ---
apiVersion: v1 apiVersion: v1

View file

@ -6,7 +6,7 @@ metadata:
namespace: default namespace: default
spec: spec:
minversion: VersionTLS12 minVersion: VersionTLS12
--- ---
apiVersion: traefik.containo.us/v1alpha1 apiVersion: traefik.containo.us/v1alpha1

View file

@ -6,7 +6,7 @@ metadata:
namespace: default namespace: default
spec: spec:
minversion: VersionTLS12 minVersion: VersionTLS12
--- ---
apiVersion: traefik.containo.us/v1alpha1 apiVersion: traefik.containo.us/v1alpha1

View file

@ -25,17 +25,17 @@ metadata:
namespace: default namespace: default
spec: spec:
minversion: VersionTLS12 minVersion: VersionTLS12
snistrict: true sniStrict: true
ciphersuites: cipherSuites:
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
- TLS_RSA_WITH_AES_256_GCM_SHA384 - TLS_RSA_WITH_AES_256_GCM_SHA384
clientca: clientAuth:
secretnames: secretNames:
- secretCA1 - secretCA1
- secretUnknown - secretUnknown
- emptySecret - emptySecret
optional: true clientAuthType: VerifyClientCertIfGiven
--- ---
apiVersion: traefik.containo.us/v1alpha1 apiVersion: traefik.containo.us/v1alpha1

View file

@ -25,16 +25,16 @@ metadata:
namespace: default namespace: default
spec: spec:
minversion: VersionTLS12 minVersion: VersionTLS12
snistrict: true sniStrict: true
ciphersuites: cipherSuites:
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
- TLS_RSA_WITH_AES_256_GCM_SHA384 - TLS_RSA_WITH_AES_256_GCM_SHA384
clientca: clientAuth:
secretnames: secretNames:
- secretCA1 - secretCA1
- secretCA2 - secretCA2
optional: true clientAuthType: VerifyClientCertIfGiven
--- ---
apiVersion: traefik.containo.us/v1alpha1 apiVersion: traefik.containo.us/v1alpha1

View file

@ -25,16 +25,16 @@ metadata:
namespace: myns namespace: myns
spec: spec:
minversion: VersionTLS12 minVersion: VersionTLS12
snistrict: true sniStrict: true
ciphersuites: cipherSuites:
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
- TLS_RSA_WITH_AES_256_GCM_SHA384 - TLS_RSA_WITH_AES_256_GCM_SHA384
clientca: clientAuth:
secretnames: secretNames:
- secretCA1 - secretCA1
- secretCA2 - secretCA2
optional: true clientAuthType: VerifyClientCertIfGiven
--- ---
apiVersion: traefik.containo.us/v1alpha1 apiVersion: traefik.containo.us/v1alpha1

View file

@ -5,7 +5,7 @@ metadata:
namespace: default namespace: default
spec: spec:
minversion: VersionTLS12 minVersion: VersionTLS12
--- ---
apiVersion: traefik.containo.us/v1alpha1 apiVersion: traefik.containo.us/v1alpha1

View file

@ -5,7 +5,7 @@ metadata:
namespace: default namespace: default
spec: spec:
minversion: VersionTLS12 minVersion: VersionTLS12
--- ---
apiVersion: traefik.containo.us/v1alpha1 apiVersion: traefik.containo.us/v1alpha1

View file

@ -313,7 +313,7 @@ func buildTLSOptions(ctx context.Context, client Client) map[string]tls.Options
logger := log.FromContext(log.With(ctx, log.Str("tlsOption", tlsOption.Name), log.Str("namespace", tlsOption.Namespace))) logger := log.FromContext(log.With(ctx, log.Str("tlsOption", tlsOption.Name), log.Str("namespace", tlsOption.Namespace)))
var clientCAs []tls.FileOrContent var clientCAs []tls.FileOrContent
for _, secretName := range tlsOption.Spec.ClientCA.SecretNames { for _, secretName := range tlsOption.Spec.ClientAuth.SecretNames {
secret, exists, err := client.GetSecret(tlsOption.Namespace, secretName) secret, exists, err := client.GetSecret(tlsOption.Namespace, secretName)
if err != nil { if err != nil {
logger.Errorf("Failed to fetch secret %s/%s: %v", tlsOption.Namespace, secretName, err) logger.Errorf("Failed to fetch secret %s/%s: %v", tlsOption.Namespace, secretName, err)
@ -337,9 +337,9 @@ func buildTLSOptions(ctx context.Context, client Client) map[string]tls.Options
tlsOptions[makeID(tlsOption.Namespace, tlsOption.Name)] = tls.Options{ tlsOptions[makeID(tlsOption.Namespace, tlsOption.Name)] = tls.Options{
MinVersion: tlsOption.Spec.MinVersion, MinVersion: tlsOption.Spec.MinVersion,
CipherSuites: tlsOption.Spec.CipherSuites, CipherSuites: tlsOption.Spec.CipherSuites,
ClientCA: tls.ClientCA{ ClientAuth: tls.ClientAuth{
Files: clientCAs, CAFiles: clientCAs,
Optional: tlsOption.Spec.ClientCA.Optional, ClientAuthType: tlsOption.Spec.ClientAuth.ClientAuthType,
}, },
SniStrict: tlsOption.Spec.SniStrict, SniStrict: tlsOption.Spec.SniStrict,
} }

View file

@ -319,12 +319,12 @@ func TestLoadIngressRouteTCPs(t *testing.T) {
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_RSA_WITH_AES_256_GCM_SHA384", "TLS_RSA_WITH_AES_256_GCM_SHA384",
}, },
ClientCA: tls.ClientCA{ ClientAuth: tls.ClientAuth{
Files: []tls.FileOrContent{ CAFiles: []tls.FileOrContent{
tls.FileOrContent("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"), tls.FileOrContent("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"),
tls.FileOrContent("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"), tls.FileOrContent("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"),
}, },
Optional: true, ClientAuthType: "VerifyClientCertIfGiven",
}, },
SniStrict: true, SniStrict: true,
}, },
@ -377,12 +377,12 @@ func TestLoadIngressRouteTCPs(t *testing.T) {
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_RSA_WITH_AES_256_GCM_SHA384", "TLS_RSA_WITH_AES_256_GCM_SHA384",
}, },
ClientCA: tls.ClientCA{ ClientAuth: tls.ClientAuth{
Files: []tls.FileOrContent{ CAFiles: []tls.FileOrContent{
tls.FileOrContent("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"), tls.FileOrContent("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"),
tls.FileOrContent("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"), tls.FileOrContent("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"),
}, },
Optional: true, ClientAuthType: "VerifyClientCertIfGiven",
}, },
SniStrict: true, SniStrict: true,
}, },
@ -435,11 +435,11 @@ func TestLoadIngressRouteTCPs(t *testing.T) {
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_RSA_WITH_AES_256_GCM_SHA384", "TLS_RSA_WITH_AES_256_GCM_SHA384",
}, },
ClientCA: tls.ClientCA{ ClientAuth: tls.ClientAuth{
Files: []tls.FileOrContent{ CAFiles: []tls.FileOrContent{
tls.FileOrContent("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"), tls.FileOrContent("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"),
}, },
Optional: true, ClientAuthType: "VerifyClientCertIfGiven",
}, },
SniStrict: true, SniStrict: true,
}, },
@ -1009,12 +1009,12 @@ func TestLoadIngressRoutes(t *testing.T) {
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_RSA_WITH_AES_256_GCM_SHA384", "TLS_RSA_WITH_AES_256_GCM_SHA384",
}, },
ClientCA: tls.ClientCA{ ClientAuth: tls.ClientAuth{
Files: []tls.FileOrContent{ CAFiles: []tls.FileOrContent{
tls.FileOrContent("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"), tls.FileOrContent("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"),
tls.FileOrContent("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"), tls.FileOrContent("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"),
}, },
Optional: true, ClientAuthType: "VerifyClientCertIfGiven",
}, },
SniStrict: true, SniStrict: true,
}, },
@ -1067,12 +1067,12 @@ func TestLoadIngressRoutes(t *testing.T) {
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_RSA_WITH_AES_256_GCM_SHA384", "TLS_RSA_WITH_AES_256_GCM_SHA384",
}, },
ClientCA: tls.ClientCA{ ClientAuth: tls.ClientAuth{
Files: []tls.FileOrContent{ CAFiles: []tls.FileOrContent{
tls.FileOrContent("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"), tls.FileOrContent("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"),
tls.FileOrContent("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"), tls.FileOrContent("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"),
}, },
Optional: true, ClientAuthType: "VerifyClientCertIfGiven",
}, },
SniStrict: true, SniStrict: true,
}, },
@ -1125,11 +1125,11 @@ func TestLoadIngressRoutes(t *testing.T) {
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_RSA_WITH_AES_256_GCM_SHA384", "TLS_RSA_WITH_AES_256_GCM_SHA384",
}, },
ClientCA: tls.ClientCA{ ClientAuth: tls.ClientAuth{
Files: []tls.FileOrContent{ CAFiles: []tls.FileOrContent{
tls.FileOrContent("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"), tls.FileOrContent("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"),
}, },
Optional: true, ClientAuthType: "VerifyClientCertIfGiven",
}, },
SniStrict: true, SniStrict: true,
}, },

View file

@ -19,22 +19,22 @@ type TLSOption struct {
// TLSOptionSpec configures TLS for an entry point // TLSOptionSpec configures TLS for an entry point
type TLSOptionSpec struct { type TLSOptionSpec struct {
MinVersion string `json:"minversion"` MinVersion string `json:"minVersion,omitempty"`
CipherSuites []string `json:"ciphersuites"` CipherSuites []string `json:"cipherSuites,omitempty"`
ClientCA ClientCA `json:"clientca"` ClientAuth ClientAuth `json:"clientAuth,omitempty"`
SniStrict bool `json:"snistrict"` SniStrict bool `json:"sniStrict,omitempty"`
} }
// +k8s:deepcopy-gen=true // +k8s:deepcopy-gen=true
// ClientCA defines traefik CA files for an entryPoint // ClientAuth defines the parameters of the client authentication part of the TLS connection, if any.
// and it indicates if they are mandatory or have just to be analyzed if provided type ClientAuth struct {
type ClientCA struct {
// SecretName is the name of the referenced Kubernetes Secret to specify the // SecretName is the name of the referenced Kubernetes Secret to specify the
// certificate details. // certificate details.
SecretNames []string `json:"secretnames"` SecretNames []string `json:"secretNames"`
// Optional indicates if ClientCA are mandatory or have just to be analyzed if provided // ClientAuthType defines the client authentication type to apply.
Optional bool `json:"optional"` // The available values are: "NoClientCert", "RequestClientCert", "VerifyClientCertIfGiven" and "RequireAndVerifyClientCert".
ClientAuthType string `json:"clientAuthType"`
} }
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

View file

@ -33,7 +33,7 @@ import (
) )
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ClientCA) DeepCopyInto(out *ClientCA) { func (in *ClientAuth) DeepCopyInto(out *ClientAuth) {
*out = *in *out = *in
if in.SecretNames != nil { if in.SecretNames != nil {
in, out := &in.SecretNames, &out.SecretNames in, out := &in.SecretNames, &out.SecretNames
@ -43,12 +43,12 @@ func (in *ClientCA) DeepCopyInto(out *ClientCA) {
return return
} }
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClientCA. // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClientAuth.
func (in *ClientCA) DeepCopy() *ClientCA { func (in *ClientAuth) DeepCopy() *ClientAuth {
if in == nil { if in == nil {
return nil return nil
} }
out := new(ClientCA) out := new(ClientAuth)
in.DeepCopyInto(out) in.DeepCopyInto(out)
return out return out
} }
@ -529,7 +529,7 @@ func (in *TLSOptionSpec) DeepCopyInto(out *TLSOptionSpec) {
*out = make([]string, len(*in)) *out = make([]string, len(*in))
copy(*out, *in) copy(*out, *in)
} }
in.ClientCA.DeepCopyInto(&out.ClientCA) in.ClientAuth.DeepCopyInto(&out.ClientAuth)
return return
} }

View file

@ -4,21 +4,22 @@ const certificateHeader = "-----BEGIN CERTIFICATE-----\n"
// +k8s:deepcopy-gen=true // +k8s:deepcopy-gen=true
// ClientCA defines traefik CA files for a entryPoint // ClientAuth defines the parameters of the client authentication part of the TLS connection, if any.
// and it indicates if they are mandatory or have just to be analyzed if provided. type ClientAuth struct {
type ClientCA struct { CAFiles []FileOrContent `json:"caFiles,omitempty" toml:"caFiles,omitempty" yaml:"caFiles,omitempty"`
Files []FileOrContent `json:"files,omitempty" toml:"files,omitempty" yaml:"files,omitempty"` // ClientAuthType defines the client authentication type to apply.
Optional bool `json:"optional,omitempty" toml:"optional,omitempty" yaml:"optional,omitempty"` // The available values are: "NoClientCert", "RequestClientCert", "VerifyClientCertIfGiven" and "RequireAndVerifyClientCert".
ClientAuthType string `json:"clientAuthType,omitempty" toml:"clientAuthType,omitempty" yaml:"clientAuthType,omitempty"`
} }
// +k8s:deepcopy-gen=true // +k8s:deepcopy-gen=true
// Options configures TLS for an entry point // Options configures TLS for an entry point
type Options struct { type Options struct {
MinVersion string `json:"minVersion,omitempty" toml:"minVersion,omitempty" yaml:"minVersion,omitempty" export:"true"` MinVersion string `json:"minVersion,omitempty" toml:"minVersion,omitempty" yaml:"minVersion,omitempty" export:"true"`
CipherSuites []string `json:"cipherSuites,omitempty" toml:"cipherSuites,omitempty" yaml:"cipherSuites,omitempty"` CipherSuites []string `json:"cipherSuites,omitempty" toml:"cipherSuites,omitempty" yaml:"cipherSuites,omitempty"`
ClientCA ClientCA `json:"clientCA,omitempty" toml:"clientCA,omitempty" yaml:"clientCA,omitempty"` ClientAuth ClientAuth `json:"clientAuth,omitempty" toml:"clientAuth,omitempty" yaml:"clientAuth,omitempty"`
SniStrict bool `json:"sniStrict,omitempty" toml:"sniStrict,omitempty" yaml:"sniStrict,omitempty" export:"true"` SniStrict bool `json:"sniStrict,omitempty" toml:"sniStrict,omitempty" yaml:"sniStrict,omitempty" export:"true"`
} }
// +k8s:deepcopy-gen=true // +k8s:deepcopy-gen=true

View file

@ -3,6 +3,7 @@ package tls
import ( import (
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"errors"
"fmt" "fmt"
"sync" "sync"
@ -159,23 +160,45 @@ func buildTLSConfig(tlsOption Options) (*tls.Config, error) {
// ensure http2 enabled // ensure http2 enabled
conf.NextProtos = []string{"h2", "http/1.1", tlsalpn01.ACMETLS1Protocol} conf.NextProtos = []string{"h2", "http/1.1", tlsalpn01.ACMETLS1Protocol}
if len(tlsOption.ClientCA.Files) > 0 { if len(tlsOption.ClientAuth.CAFiles) > 0 {
pool := x509.NewCertPool() pool := x509.NewCertPool()
for _, caFile := range tlsOption.ClientCA.Files { for _, caFile := range tlsOption.ClientAuth.CAFiles {
data, err := caFile.Read() data, err := caFile.Read()
if err != nil { if err != nil {
return nil, err return nil, err
} }
ok := pool.AppendCertsFromPEM(data) ok := pool.AppendCertsFromPEM(data)
if !ok { if !ok {
return nil, fmt.Errorf("invalid certificate(s) in %s", caFile) if caFile.IsPath() {
return nil, fmt.Errorf("invalid certificate(s) in %s", caFile)
}
return nil, errors.New("invalid certificate(s) content")
} }
} }
conf.ClientCAs = pool conf.ClientCAs = pool
if tlsOption.ClientCA.Optional { conf.ClientAuth = tls.RequireAndVerifyClientCert
}
clientAuthType := tlsOption.ClientAuth.ClientAuthType
if len(clientAuthType) > 0 {
if conf.ClientCAs == nil && (clientAuthType == "VerifyClientCertIfGiven" ||
clientAuthType == "RequireAndVerifyClientCert") {
return nil, fmt.Errorf("invalid clientAuthType: %s, CAFiles is required", clientAuthType)
}
switch clientAuthType {
case "NoClientCert":
conf.ClientAuth = tls.NoClientCert
case "RequestClientCert":
conf.ClientAuth = tls.RequestClientCert
case "RequireAnyClientCert":
conf.ClientAuth = tls.RequireAnyClientCert
case "VerifyClientCertIfGiven":
conf.ClientAuth = tls.VerifyClientCertIfGiven conf.ClientAuth = tls.VerifyClientCertIfGiven
} else { case "RequireAndVerifyClientCert":
conf.ClientAuth = tls.RequireAndVerifyClientCert conf.ClientAuth = tls.RequireAndVerifyClientCert
default:
return nil, fmt.Errorf("unknown client auth type %q", clientAuthType)
} }
} }

View file

@ -2,9 +2,12 @@ package tls
import ( import (
"crypto/tls" "crypto/tls"
"crypto/x509"
"encoding/pem"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
// LocalhostCert is a PEM-encoded TLS cert with SAN IPs // LocalhostCert is a PEM-encoded TLS cert with SAN IPs
@ -146,3 +149,125 @@ func TestManager_Get(t *testing.T) {
}) })
} }
} }
func TestClientAuth(t *testing.T) {
tlsConfigs := map[string]Options{
"eca": {ClientAuth: ClientAuth{}},
"ecat": {ClientAuth: ClientAuth{ClientAuthType: ""}},
"ncc": {ClientAuth: ClientAuth{ClientAuthType: "NoClientCert"}},
"rcc": {ClientAuth: ClientAuth{ClientAuthType: "RequestClientCert"}},
"racc": {ClientAuth: ClientAuth{ClientAuthType: "RequireAnyClientCert"}},
"vccig": {
ClientAuth: ClientAuth{
CAFiles: []FileOrContent{localhostCert},
ClientAuthType: "VerifyClientCertIfGiven",
},
},
"vccigwca": {
ClientAuth: ClientAuth{ClientAuthType: "VerifyClientCertIfGiven"},
},
"ravcc": {ClientAuth: ClientAuth{ClientAuthType: "RequireAndVerifyClientCert"}},
"ravccwca": {
ClientAuth: ClientAuth{
CAFiles: []FileOrContent{localhostCert},
ClientAuthType: "RequireAndVerifyClientCert",
},
},
"ravccwbca": {
ClientAuth: ClientAuth{
CAFiles: []FileOrContent{"Bad content"},
ClientAuthType: "RequireAndVerifyClientCert",
},
},
"ucat": {ClientAuth: ClientAuth{ClientAuthType: "Unknown"}},
}
block, _ := pem.Decode([]byte(localhostCert))
cert, err := x509.ParseCertificate(block.Bytes)
require.NoError(t, err)
testCases := []struct {
desc string
tlsOptionsName string
expectedClientAuth tls.ClientAuthType
expectedRawSubject []byte
}{
{
desc: "Empty ClientAuth option should get a tls.NoClientCert (default value)",
tlsOptionsName: "eca",
expectedClientAuth: tls.NoClientCert,
},
{
desc: "Empty ClientAuthType option should get a tls.NoClientCert (default value)",
tlsOptionsName: "ecat",
expectedClientAuth: tls.NoClientCert,
},
{
desc: "NoClientCert option should get a tls.NoClientCert as ClientAuthType",
tlsOptionsName: "ncc",
expectedClientAuth: tls.NoClientCert,
},
{
desc: "RequestClientCert option should get a tls.RequestClientCert as ClientAuthType",
tlsOptionsName: "rcc",
expectedClientAuth: tls.RequestClientCert,
},
{
desc: "RequireAnyClientCert option should get a tls.RequireAnyClientCert as ClientAuthType",
tlsOptionsName: "racc",
expectedClientAuth: tls.RequireAnyClientCert,
},
{
desc: "VerifyClientCertIfGiven option should get a tls.VerifyClientCertIfGiven as ClientAuthType",
tlsOptionsName: "vccig",
expectedClientAuth: tls.VerifyClientCertIfGiven,
},
{
desc: "VerifyClientCertIfGiven option without CAFiles yields a default ClientAuthType (NoClientCert)",
tlsOptionsName: "vccigwca",
expectedClientAuth: tls.NoClientCert,
},
{
desc: "RequireAndVerifyClientCert option without CAFiles yields a default ClientAuthType (NoClientCert)",
tlsOptionsName: "ravcc",
expectedClientAuth: tls.NoClientCert,
},
{
desc: "RequireAndVerifyClientCert option should get a tls.RequireAndVerifyClientCert as ClientAuthType with CA files",
tlsOptionsName: "ravccwca",
expectedClientAuth: tls.RequireAndVerifyClientCert,
expectedRawSubject: cert.RawSubject,
},
{
desc: "Unknown option yields a default ClientAuthType (NoClientCert)",
tlsOptionsName: "ucat",
expectedClientAuth: tls.NoClientCert,
},
{
desc: "Bad CA certificate content yields a default ClientAuthType (NoClientCert)",
tlsOptionsName: "ravccwbca",
expectedClientAuth: tls.NoClientCert,
},
}
tlsManager := NewManager()
tlsManager.UpdateConfigs(nil, tlsConfigs, nil)
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
config, err := tlsManager.Get("default", test.tlsOptionsName)
assert.NoError(t, err)
if test.expectedRawSubject != nil {
subjects := config.ClientCAs.Subjects()
assert.Len(t, subjects, 1)
assert.Equal(t, subjects[0], test.expectedRawSubject)
}
assert.Equal(t, config.ClientAuth, test.expectedClientAuth)
})
}
}

View file

@ -51,22 +51,22 @@ func (in *CertAndStores) DeepCopy() *CertAndStores {
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ClientCA) DeepCopyInto(out *ClientCA) { func (in *ClientAuth) DeepCopyInto(out *ClientAuth) {
*out = *in *out = *in
if in.Files != nil { if in.CAFiles != nil {
in, out := &in.Files, &out.Files in, out := &in.CAFiles, &out.CAFiles
*out = make([]FileOrContent, len(*in)) *out = make([]FileOrContent, len(*in))
copy(*out, *in) copy(*out, *in)
} }
return return
} }
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClientCA. // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClientAuth.
func (in *ClientCA) DeepCopy() *ClientCA { func (in *ClientAuth) DeepCopy() *ClientAuth {
if in == nil { if in == nil {
return nil return nil
} }
out := new(ClientCA) out := new(ClientAuth)
in.DeepCopyInto(out) in.DeepCopyInto(out)
return out return out
} }
@ -79,7 +79,7 @@ func (in *Options) DeepCopyInto(out *Options) {
*out = make([]string, len(*in)) *out = make([]string, len(*in))
copy(*out, *in) copy(*out, *in)
} }
in.ClientCA.DeepCopyInto(&out.ClientCA) in.ClientAuth.DeepCopyInto(&out.ClientAuth)
return return
} }