From 2644c1f5980c77a21d60d40f708af1fdefbd179b Mon Sep 17 00:00:00 2001 From: Romain Date: Fri, 20 Aug 2021 18:20:06 +0200 Subject: [PATCH] Makes ALPN protocols configurable --- docs/content/https/tls.md | 41 ++++++++++++++ .../reference/dynamic-configuration/file.toml | 2 + .../reference/dynamic-configuration/file.yaml | 6 ++ .../kubernetes-crd-resource.yml | 3 + .../reference/dynamic-configuration/kv-ref.md | 4 ++ .../traefik.containo.us_tlsoptions.yaml | 4 ++ .../routing/providers/kubernetes-crd.md | 3 + integration/fixtures/k8s/01-traefik-crd.yml | 10 +++- pkg/provider/kubernetes/crd/kubernetes.go | 7 +++ .../kubernetes/crd/kubernetes_test.go | 55 +++++++++++++++++++ .../crd/traefik/v1alpha1/tlsoption.go | 1 + .../traefik/v1alpha1/zz_generated.deepcopy.go | 5 ++ pkg/provider/kv/kv_test.go | 10 ++++ pkg/server/aggregator_test.go | 32 +++++++++-- pkg/server/configurationwatcher_test.go | 24 +++++++- pkg/tls/tls.go | 7 +++ pkg/tls/tlsmanager.go | 12 ++-- pkg/tls/zz_generated.deepcopy.go | 5 ++ 18 files changed, 216 insertions(+), 15 deletions(-) diff --git a/docs/content/https/tls.md b/docs/content/https/tls.md index ae3584b8b..cff442cab 100644 --- a/docs/content/https/tls.md +++ b/docs/content/https/tls.md @@ -399,6 +399,47 @@ spec: preferServerCipherSuites: true ``` +### ALPN Protocols + +_Optional, Default="h2, http/1.1, acme-tls/1"_ + +This option allows to specify the list of supported application level protocols for the TLS handshake, +in order of preference. +If the client supports ALPN, the selected protocol will be one from this list, +and the connection will fail if there is no mutually supported protocol. + +```yaml tab="File (YAML)" +# Dynamic configuration + +tls: + options: + default: + alpnProtocols: + - http/1.1 + - h2 +``` + +```toml tab="File (TOML)" +# Dynamic configuration + +[tls.options] + [tls.options.default] + alpnProtocols = ["http/1.1", "h2"] +``` + +```yaml tab="Kubernetes" +apiVersion: traefik.containo.us/v1alpha1 +kind: TLSOption +metadata: + name: default + namespace: default + +spec: + alpnProtocols: + - http/1.1 + - h2 +``` + ### Client Authentication (mTLS) Traefik supports mutual authentication, through the `clientAuth` section. diff --git a/docs/content/reference/dynamic-configuration/file.toml b/docs/content/reference/dynamic-configuration/file.toml index d531f81a1..479b4a9a1 100644 --- a/docs/content/reference/dynamic-configuration/file.toml +++ b/docs/content/reference/dynamic-configuration/file.toml @@ -421,6 +421,7 @@ curvePreferences = ["foobar", "foobar"] sniStrict = true preferServerCipherSuites = true + alpnProtocols = ["foobar", "foobar"] [tls.options.Options0.clientAuth] caFiles = ["foobar", "foobar"] clientAuthType = "foobar" @@ -431,6 +432,7 @@ curvePreferences = ["foobar", "foobar"] sniStrict = true preferServerCipherSuites = true + alpnProtocols = ["foobar", "foobar"] [tls.options.Options1.clientAuth] caFiles = ["foobar", "foobar"] clientAuthType = "foobar" diff --git a/docs/content/reference/dynamic-configuration/file.yaml b/docs/content/reference/dynamic-configuration/file.yaml index 515dba07c..cd4e540dc 100644 --- a/docs/content/reference/dynamic-configuration/file.yaml +++ b/docs/content/reference/dynamic-configuration/file.yaml @@ -470,6 +470,9 @@ tls: clientAuthType: foobar sniStrict: true preferServerCipherSuites: true + alpnProtocols: + - foobar + - foobar Options1: minVersion: foobar maxVersion: foobar @@ -486,6 +489,9 @@ tls: clientAuthType: foobar sniStrict: true preferServerCipherSuites: true + alpnProtocols: + - foobar + - foobar stores: Store0: defaultCertificate: diff --git a/docs/content/reference/dynamic-configuration/kubernetes-crd-resource.yml b/docs/content/reference/dynamic-configuration/kubernetes-crd-resource.yml index 4b9bb5562..d86a2dd67 100644 --- a/docs/content/reference/dynamic-configuration/kubernetes-crd-resource.yml +++ b/docs/content/reference/dynamic-configuration/kubernetes-crd-resource.yml @@ -194,6 +194,9 @@ spec: clientAuthType: RequireAndVerifyClientCert sniStrict: true preferServerCipherSuites: true + alpnProtocols: + - foobar + - foobar --- apiVersion: traefik.containo.us/v1alpha1 diff --git a/docs/content/reference/dynamic-configuration/kv-ref.md b/docs/content/reference/dynamic-configuration/kv-ref.md index b4c81d76b..81d849d38 100644 --- a/docs/content/reference/dynamic-configuration/kv-ref.md +++ b/docs/content/reference/dynamic-configuration/kv-ref.md @@ -274,6 +274,8 @@ | `traefik/tls/certificates/1/keyFile` | `foobar` | | `traefik/tls/certificates/1/stores/0` | `foobar` | | `traefik/tls/certificates/1/stores/1` | `foobar` | +| `traefik/tls/options/Options0/alpnProtocols/0` | `foobar` | +| `traefik/tls/options/Options0/alpnProtocols/1` | `foobar` | | `traefik/tls/options/Options0/cipherSuites/0` | `foobar` | | `traefik/tls/options/Options0/cipherSuites/1` | `foobar` | | `traefik/tls/options/Options0/clientAuth/caFiles/0` | `foobar` | @@ -285,6 +287,8 @@ | `traefik/tls/options/Options0/minVersion` | `foobar` | | `traefik/tls/options/Options0/preferServerCipherSuites` | `true` | | `traefik/tls/options/Options0/sniStrict` | `true` | +| `traefik/tls/options/Options1/alpnProtocols/0` | `foobar` | +| `traefik/tls/options/Options1/alpnProtocols/1` | `foobar` | | `traefik/tls/options/Options1/cipherSuites/0` | `foobar` | | `traefik/tls/options/Options1/cipherSuites/1` | `foobar` | | `traefik/tls/options/Options1/clientAuth/caFiles/0` | `foobar` | diff --git a/docs/content/reference/dynamic-configuration/traefik.containo.us_tlsoptions.yaml b/docs/content/reference/dynamic-configuration/traefik.containo.us_tlsoptions.yaml index e18585a9b..86e9beb7c 100644 --- a/docs/content/reference/dynamic-configuration/traefik.containo.us_tlsoptions.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.containo.us_tlsoptions.yaml @@ -36,6 +36,10 @@ spec: spec: description: TLSOptionSpec configures TLS for an entry point. properties: + alpnProtocols: + items: + type: string + type: array cipherSuites: items: type: string diff --git a/docs/content/routing/providers/kubernetes-crd.md b/docs/content/routing/providers/kubernetes-crd.md index 0e5de9721..4db55f7f2 100644 --- a/docs/content/routing/providers/kubernetes-crd.md +++ b/docs/content/routing/providers/kubernetes-crd.md @@ -1506,6 +1506,8 @@ or referencing TLS options in the [`IngressRoute`](#kind-ingressroute) / [`Ingre - secret-ca2 clientAuthType: VerifyClientCertIfGiven # [7] sniStrict: true # [8] + alpnProtocols: # [9] + - foobar ``` | Ref | Attribute | Purpose | @@ -1518,6 +1520,7 @@ or referencing TLS options in the [`IngressRoute`](#kind-ingressroute) / [`Ingre | [6] | `clientAuth.secretNames` | list of names of the referenced Kubernetes [Secrets](https://kubernetes.io/docs/concepts/configuration/secret/) (in TLSOption namespace). The secret must contain a certificate under either a `tls.ca` or a `ca.crt` key. | | [7] | `clientAuth.clientAuthType` | defines the client authentication type to apply. The available values are: `NoClientCert`, `RequestClientCert`, `VerifyClientCertIfGiven` and `RequireAndVerifyClientCert` | | [8] | `sniStrict` | if `true`, Traefik won't allow connections from clients connections that do not specify a server_name extension | +| [9] | `alpnProtocols` | List of supported [application level protocols](../../https/tls.md#alpn-protocols) for the TLS handshake, in order of preference. | !!! info "CA Secret" diff --git a/integration/fixtures/k8s/01-traefik-crd.yml b/integration/fixtures/k8s/01-traefik-crd.yml index 84ca0e756..f5fc97d13 100644 --- a/integration/fixtures/k8s/01-traefik-crd.yml +++ b/integration/fixtures/k8s/01-traefik-crd.yml @@ -4,7 +4,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.4.1 + controller-gen.kubebuilder.io/version: v0.5.0 creationTimestamp: null name: ingressroutes.traefik.containo.us spec: @@ -202,7 +202,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.4.1 + controller-gen.kubebuilder.io/version: v0.5.0 creationTimestamp: null name: ingressroutetcps.traefik.containo.us spec: @@ -362,7 +362,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.4.1 + controller-gen.kubebuilder.io/version: v0.5.0 creationTimestamp: null name: ingressrouteudps.traefik.containo.us spec: @@ -1208,6 +1208,10 @@ spec: spec: description: TLSOptionSpec configures TLS for an entry point. properties: + alpnProtocols: + items: + type: string + type: array cipherSuites: items: type: string diff --git a/pkg/provider/kubernetes/crd/kubernetes.go b/pkg/provider/kubernetes/crd/kubernetes.go index dcb37ae8a..4dd1d208d 100644 --- a/pkg/provider/kubernetes/crd/kubernetes.go +++ b/pkg/provider/kubernetes/crd/kubernetes.go @@ -703,6 +703,12 @@ func buildTLSOptions(ctx context.Context, client Client) map[string]tls.Options id = tlsOption.Name nsDefault = append(nsDefault, tlsOption.Namespace) } + + alpnProtocols := tls.DefaultTLSOptions.ALPNProtocols + if len(tlsOption.Spec.ALPNProtocols) > 0 { + alpnProtocols = tlsOption.Spec.ALPNProtocols + } + tlsOptions[id] = tls.Options{ MinVersion: tlsOption.Spec.MinVersion, MaxVersion: tlsOption.Spec.MaxVersion, @@ -714,6 +720,7 @@ func buildTLSOptions(ctx context.Context, client Client) map[string]tls.Options }, SniStrict: tlsOption.Spec.SniStrict, PreferServerCipherSuites: tlsOption.Spec.PreferServerCipherSuites, + ALPNProtocols: alpnProtocols, } } diff --git a/pkg/provider/kubernetes/crd/kubernetes_test.go b/pkg/provider/kubernetes/crd/kubernetes_test.go index 8d62e6cf1..c02f9889d 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_test.go +++ b/pkg/provider/kubernetes/crd/kubernetes_test.go @@ -616,6 +616,11 @@ func TestLoadIngressRouteTCPs(t *testing.T) { }, SniStrict: true, PreferServerCipherSuites: true, + ALPNProtocols: []string{ + "h2", + "http/1.1", + "acme-tls/1", + }, }, }, }, @@ -678,6 +683,11 @@ func TestLoadIngressRouteTCPs(t *testing.T) { ClientAuthType: "VerifyClientCertIfGiven", }, SniStrict: true, + ALPNProtocols: []string{ + "h2", + "http/1.1", + "acme-tls/1", + }, }, }, }, @@ -739,6 +749,11 @@ func TestLoadIngressRouteTCPs(t *testing.T) { ClientAuthType: "VerifyClientCertIfGiven", }, SniStrict: true, + ALPNProtocols: []string{ + "h2", + "http/1.1", + "acme-tls/1", + }, }, }, }, @@ -789,6 +804,11 @@ func TestLoadIngressRouteTCPs(t *testing.T) { Options: map[string]tls.Options{ "default-foo": { MinVersion: "VersionTLS12", + ALPNProtocols: []string{ + "h2", + "http/1.1", + "acme-tls/1", + }, }, }, }, @@ -839,6 +859,11 @@ func TestLoadIngressRouteTCPs(t *testing.T) { Options: map[string]tls.Options{ "default-foo": { MinVersion: "VersionTLS12", + ALPNProtocols: []string{ + "h2", + "http/1.1", + "acme-tls/1", + }, }, }, }, @@ -2539,6 +2564,11 @@ func TestLoadIngressRoutes(t *testing.T) { }, SniStrict: true, PreferServerCipherSuites: true, + ALPNProtocols: []string{ + "h2", + "http/1.1", + "acme-tls/1", + }, }, }, }, @@ -2648,6 +2678,11 @@ func TestLoadIngressRoutes(t *testing.T) { }, SniStrict: true, PreferServerCipherSuites: true, + ALPNProtocols: []string{ + "h2", + "http/1.1", + "acme-tls/1", + }, }, }, }, @@ -2716,6 +2751,11 @@ func TestLoadIngressRoutes(t *testing.T) { ClientAuthType: "VerifyClientCertIfGiven", }, SniStrict: true, + ALPNProtocols: []string{ + "h2", + "http/1.1", + "acme-tls/1", + }, }, }, }, @@ -2779,6 +2819,11 @@ func TestLoadIngressRoutes(t *testing.T) { ClientAuthType: "VerifyClientCertIfGiven", }, SniStrict: true, + ALPNProtocols: []string{ + "h2", + "http/1.1", + "acme-tls/1", + }, }, }, }, @@ -2831,6 +2876,11 @@ func TestLoadIngressRoutes(t *testing.T) { Options: map[string]tls.Options{ "default-foo": { MinVersion: "VersionTLS12", + ALPNProtocols: []string{ + "h2", + "http/1.1", + "acme-tls/1", + }, }, }, }, @@ -2883,6 +2933,11 @@ func TestLoadIngressRoutes(t *testing.T) { Options: map[string]tls.Options{ "default-foo": { MinVersion: "VersionTLS12", + ALPNProtocols: []string{ + "h2", + "http/1.1", + "acme-tls/1", + }, }, }, }, diff --git a/pkg/provider/kubernetes/crd/traefik/v1alpha1/tlsoption.go b/pkg/provider/kubernetes/crd/traefik/v1alpha1/tlsoption.go index 52eb84a48..9faf2d61c 100644 --- a/pkg/provider/kubernetes/crd/traefik/v1alpha1/tlsoption.go +++ b/pkg/provider/kubernetes/crd/traefik/v1alpha1/tlsoption.go @@ -27,6 +27,7 @@ type TLSOptionSpec struct { ClientAuth ClientAuth `json:"clientAuth,omitempty"` SniStrict bool `json:"sniStrict,omitempty"` PreferServerCipherSuites bool `json:"preferServerCipherSuites,omitempty"` + ALPNProtocols []string `json:"alpnProtocols,omitempty"` } // +k8s:deepcopy-gen=true diff --git a/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go b/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go index 05b41d74c..b0a17ce4c 100644 --- a/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go @@ -1327,6 +1327,11 @@ func (in *TLSOptionSpec) DeepCopyInto(out *TLSOptionSpec) { copy(*out, *in) } in.ClientAuth.DeepCopyInto(&out.ClientAuth) + if in.ALPNProtocols != nil { + in, out := &in.ALPNProtocols, &out.ALPNProtocols + *out = make([]string, len(*in)) + copy(*out, *in) + } return } diff --git a/pkg/provider/kv/kv_test.go b/pkg/provider/kv/kv_test.go index 38896001a..ef6fde11c 100644 --- a/pkg/provider/kv/kv_test.go +++ b/pkg/provider/kv/kv_test.go @@ -846,6 +846,11 @@ func Test_buildConfiguration(t *testing.T) { ClientAuthType: "foobar", }, SniStrict: true, + ALPNProtocols: []string{ + "h2", + "http/1.1", + "acme-tls/1", + }, }, "Options1": { MinVersion: "foobar", @@ -866,6 +871,11 @@ func Test_buildConfiguration(t *testing.T) { ClientAuthType: "foobar", }, SniStrict: true, + ALPNProtocols: []string{ + "h2", + "http/1.1", + "acme-tls/1", + }, }, }, Stores: map[string]tls.Store{ diff --git a/pkg/server/aggregator_test.go b/pkg/server/aggregator_test.go index 70cce8820..8d9b0f18c 100644 --- a/pkg/server/aggregator_test.go +++ b/pkg/server/aggregator_test.go @@ -182,7 +182,13 @@ func Test_mergeConfiguration_tlsOptions(t *testing.T) { desc: "Nil returns an empty configuration", given: nil, expected: map[string]tls.Options{ - "default": {}, + "default": { + ALPNProtocols: []string{ + "h2", + "http/1.1", + "acme-tls/1", + }, + }, }, }, { @@ -199,7 +205,13 @@ func Test_mergeConfiguration_tlsOptions(t *testing.T) { }, }, expected: map[string]tls.Options{ - "default": {}, + "default": { + ALPNProtocols: []string{ + "h2", + "http/1.1", + "acme-tls/1", + }, + }, "foo@provider-1": { MinVersion: "VersionTLS12", }, @@ -228,7 +240,13 @@ func Test_mergeConfiguration_tlsOptions(t *testing.T) { }, }, expected: map[string]tls.Options{ - "default": {}, + "default": { + ALPNProtocols: []string{ + "h2", + "http/1.1", + "acme-tls/1", + }, + }, "foo@provider-1": { MinVersion: "VersionTLS13", }, @@ -334,7 +352,13 @@ func Test_mergeConfiguration_tlsOptions(t *testing.T) { }, }, expected: map[string]tls.Options{ - "default": {}, + "default": { + ALPNProtocols: []string{ + "h2", + "http/1.1", + "acme-tls/1", + }, + }, "foo@provider-1": { MinVersion: "VersionTLS12", }, diff --git a/pkg/server/configurationwatcher_test.go b/pkg/server/configurationwatcher_test.go index 276b6bc83..7b885cd22 100644 --- a/pkg/server/configurationwatcher_test.go +++ b/pkg/server/configurationwatcher_test.go @@ -76,7 +76,13 @@ func TestNewConfigurationWatcher(t *testing.T) { }, TLS: &dynamic.TLSConfiguration{ Options: map[string]tls.Options{ - "default": {}, + "default": { + ALPNProtocols: []string{ + "h2", + "http/1.1", + "acme-tls/1", + }, + }, }, Stores: map[string]tls.Store{}, }, @@ -236,7 +242,13 @@ func TestListenProvidersDoesNotSkipFlappingConfiguration(t *testing.T) { }, TLS: &dynamic.TLSConfiguration{ Options: map[string]tls.Options{ - "default": {}, + "default": { + ALPNProtocols: []string{ + "h2", + "http/1.1", + "acme-tls/1", + }, + }, }, Stores: map[string]tls.Store{}, }, @@ -292,7 +304,13 @@ func TestListenProvidersPublishesConfigForEachProvider(t *testing.T) { }, TLS: &dynamic.TLSConfiguration{ Options: map[string]tls.Options{ - "default": {}, + "default": { + ALPNProtocols: []string{ + "h2", + "http/1.1", + "acme-tls/1", + }, + }, }, Stores: map[string]tls.Store{}, }, diff --git a/pkg/tls/tls.go b/pkg/tls/tls.go index 8e2e3f884..d23370000 100644 --- a/pkg/tls/tls.go +++ b/pkg/tls/tls.go @@ -23,6 +23,13 @@ type Options struct { ClientAuth ClientAuth `json:"clientAuth,omitempty" toml:"clientAuth,omitempty" yaml:"clientAuth,omitempty"` SniStrict bool `json:"sniStrict,omitempty" toml:"sniStrict,omitempty" yaml:"sniStrict,omitempty" export:"true"` PreferServerCipherSuites bool `json:"preferServerCipherSuites,omitempty" toml:"preferServerCipherSuites,omitempty" yaml:"preferServerCipherSuites,omitempty" export:"true"` + ALPNProtocols []string `json:"alpnProtocols,omitempty" toml:"alpnProtocols,omitempty" yaml:"alpnProtocols,omitempty" export:"true"` +} + +// SetDefaults sets the default values for an Options struct. +func (o *Options) SetDefaults() { + // ensure http2 enabled + o.ALPNProtocols = DefaultTLSOptions.ALPNProtocols } // +k8s:deepcopy-gen=true diff --git a/pkg/tls/tlsmanager.go b/pkg/tls/tlsmanager.go index 2ac87a161..35ba50fac 100644 --- a/pkg/tls/tlsmanager.go +++ b/pkg/tls/tlsmanager.go @@ -24,7 +24,10 @@ const ( ) // DefaultTLSOptions the default TLS options. -var DefaultTLSOptions = Options{} +var DefaultTLSOptions = Options{ + // ensure http2 enabled + ALPNProtocols: []string{"h2", "http/1.1", tlsalpn01.ACMETLS1Protocol}, +} // Manager is the TLS option/store/configuration factory. type Manager struct { @@ -230,10 +233,9 @@ func buildCertificateStore(ctx context.Context, tlsStore Store, storename string // creates a TLS config that allows terminating HTTPS for multiple domains using SNI. func buildTLSConfig(tlsOption Options) (*tls.Config, error) { - conf := &tls.Config{} - - // ensure http2 enabled - conf.NextProtos = []string{"h2", "http/1.1", tlsalpn01.ACMETLS1Protocol} + conf := &tls.Config{ + NextProtos: tlsOption.ALPNProtocols, + } if len(tlsOption.ClientAuth.CAFiles) > 0 { pool := x509.NewCertPool() diff --git a/pkg/tls/zz_generated.deepcopy.go b/pkg/tls/zz_generated.deepcopy.go index 0025fb9f8..c4dbf9b3c 100644 --- a/pkg/tls/zz_generated.deepcopy.go +++ b/pkg/tls/zz_generated.deepcopy.go @@ -85,6 +85,11 @@ func (in *Options) DeepCopyInto(out *Options) { copy(*out, *in) } in.ClientAuth.DeepCopyInto(&out.ClientAuth) + if in.ALPNProtocols != nil { + in, out := &in.ALPNProtocols, &out.ALPNProtocols + *out = make([]string, len(*in)) + copy(*out, *in) + } return }