From 450471d30a062ca21cc595ef6a72fcfa8751fc5a Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Doumenjou Date: Mon, 29 Oct 2018 16:02:06 +0100 Subject: [PATCH] Add the missing pass-client-tls annotation to the kubernetes provider --- autogen/gentemplates/gen.go | 22 +++++ docs/configuration/backends/kubernetes.md | 86 ++++++++++++------- provider/kubernetes/annotations.go | 3 +- .../kubernetes/builder_configuration_test.go | 22 +++++ provider/kubernetes/kubernetes.go | 70 +++++++++------ provider/kubernetes/kubernetes_test.go | 23 +++-- templates/kubernetes.tmpl | 22 +++++ 7 files changed, 183 insertions(+), 65 deletions(-) diff --git a/autogen/gentemplates/gen.go b/autogen/gentemplates/gen.go index bc64f14f7..057cb3e52 100644 --- a/autogen/gentemplates/gen.go +++ b/autogen/gentemplates/gen.go @@ -1379,6 +1379,28 @@ var _templatesKubernetesTmpl = []byte(`[backends] {{end}} {{end}} + {{if $frontend.PassTLSClientCert }} + [frontends."{{ $frontendName }}".passTLSClientCert] + pem = {{ $frontend.PassTLSClientCert.PEM }} + {{ $infos := $frontend.PassTLSClientCert.Infos }} + {{if $infos }} + [frontends."{{ $frontendName }}".passTLSClientCert.infos] + notAfter = {{ $infos.NotAfter }} + notBefore = {{ $infos.NotBefore }} + sans = {{ $infos.Sans }} + {{ $subject := $infos.Subject }} + {{if $subject }} + [frontends."{{ $frontendName }}".passTLSClientCert.infos.subject] + country = {{ $subject.Country }} + province = {{ $subject.Province }} + locality = {{ $subject.Locality }} + organization = {{ $subject.Organization }} + commonName = {{ $subject.CommonName }} + serialNumber = {{ $subject.SerialNumber }} + {{end}} + {{end}} + {{end}} + {{if $frontend.Headers }} [frontends."{{ $frontendName }}".headers] SSLRedirect = {{ $frontend.Headers.SSLRedirect }} diff --git a/docs/configuration/backends/kubernetes.md b/docs/configuration/backends/kubernetes.md index 962cc3225..eb526993e 100644 --- a/docs/configuration/backends/kubernetes.md +++ b/docs/configuration/backends/kubernetes.md @@ -146,29 +146,35 @@ If either of those configuration options exist, then the backend communication p The following general annotations are applicable on the Ingress object: -| Annotation | Description | -|---------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `traefik.ingress.kubernetes.io/error-pages: ` | (1) See [custom error pages](/configuration/commons/#custom-error-pages) section. | -| `traefik.ingress.kubernetes.io/frontend-entry-points: http,https` | Override the default frontend endpoints. | -| `traefik.ingress.kubernetes.io/pass-tls-cert: "true"` | Override the default frontend PassTLSCert value. Default: `false`. | -| `traefik.ingress.kubernetes.io/preserve-host: "true"` | Forward client `Host` header to the backend. | -| `traefik.ingress.kubernetes.io/priority: "3"` | Override the default frontend rule priority. | -| `traefik.ingress.kubernetes.io/rate-limit: ` | (2) See [rate limiting](/configuration/commons/#rate-limiting) section. | -| `traefik.ingress.kubernetes.io/redirect-entry-point: https` | Enables Redirect to another entryPoint for that frontend (e.g. HTTPS). | -| `traefik.ingress.kubernetes.io/redirect-permanent: "true"` | Return 301 instead of 302. | -| `traefik.ingress.kubernetes.io/redirect-regex: ^http://localhost/(.*)` | Redirect to another URL for that frontend. Must be set with `traefik.ingress.kubernetes.io/redirect-replacement`. | -| `traefik.ingress.kubernetes.io/redirect-replacement: http://mydomain/$1` | Redirect to another URL for that frontend. Must be set with `traefik.ingress.kubernetes.io/redirect-regex`. | -| `traefik.ingress.kubernetes.io/rewrite-target: /users` | Replaces each matched Ingress path with the specified one, and adds the old path to the `X-Replaced-Path` header. | -| `traefik.ingress.kubernetes.io/rule-type: PathPrefixStrip` | Overrides the default frontend rule type. Only path-related matchers can be specified [(`Path`, `PathPrefix`, `PathStrip`, `PathPrefixStrip`)](/basics/#path-matcher-usage-guidelines). | -| `traefik.ingress.kubernetes.io/request-modifier: AddPrefix: /users` | Adds a [request modifier](/basics/#modifiers) to the backend request. | -| `traefik.ingress.kubernetes.io/whitelist-source-range: "1.2.3.0/24, fe80::/16"` | A comma-separated list of IP ranges permitted for access (6). | -| `ingress.kubernetes.io/whitelist-x-forwarded-for: "true"` | Use `X-Forwarded-For` header as valid source of IP for the white list. | -| `traefik.ingress.kubernetes.io/app-root: "/index.html"` | Redirects all requests for `/` to the defined path. (4) | -| `traefik.ingress.kubernetes.io/service-weights: ` | Set ingress backend weights specified as percentage or decimal numbers in YAML. (5) -| `ingress.kubernetes.io/protocol: ` | Set the protocol Traefik will use to communicate with pods. +| Annotation | Description | +|---------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `traefik.ingress.kubernetes.io/app-root: "/index.html"` | Redirects all requests for `/` to the defined path. (1) | +| `traefik.ingress.kubernetes.io/error-pages: ` | See [custom error pages](/configuration/commons/#custom-error-pages) section. (2) | +| `traefik.ingress.kubernetes.io/frontend-entry-points: http,https` | Override the default frontend endpoints. | +| `traefik.ingress.kubernetes.io/pass-client-tls-cert: ` | Forward the client certificate following the configuration in YAML. (3) | +| `traefik.ingress.kubernetes.io/pass-tls-cert: "true"` | Override the default frontend PassTLSCert value. Default: `false`.(DEPRECATED) | +| `traefik.ingress.kubernetes.io/preserve-host: "true"` | Forward client `Host` header to the backend. | +| `traefik.ingress.kubernetes.io/priority: "3"` | Override the default frontend rule priority. | +| `traefik.ingress.kubernetes.io/rate-limit: ` | See [rate limiting](/configuration/commons/#rate-limiting) section. (4) | +| `traefik.ingress.kubernetes.io/redirect-entry-point: https` | Enables Redirect to another entryPoint for that frontend (e.g. HTTPS). | +| `traefik.ingress.kubernetes.io/redirect-permanent: "true"` | Return 301 instead of 302. | +| `traefik.ingress.kubernetes.io/redirect-regex: ^http://localhost/(.*)` | Redirect to another URL for that frontend. Must be set with `traefik.ingress.kubernetes.io/redirect-replacement`. | +| `traefik.ingress.kubernetes.io/redirect-replacement: http://mydomain/$1` | Redirect to another URL for that frontend. Must be set with `traefik.ingress.kubernetes.io/redirect-regex`. | +| `traefik.ingress.kubernetes.io/request-modifier: AddPrefix: /users` | Adds a [request modifier](/basics/#modifiers) to the backend request. | +| `traefik.ingress.kubernetes.io/rewrite-target: /users` | Replaces each matched Ingress path with the specified one, and adds the old path to the `X-Replaced-Path` header. | +| `traefik.ingress.kubernetes.io/rule-type: PathPrefixStrip` | Overrides the default frontend rule type. Only path-related matchers can be specified [(`Path`, `PathPrefix`, `PathStrip`, `PathPrefixStrip`)](/basics/#path-matcher-usage-guidelines).(5) | +| `traefik.ingress.kubernetes.io/service-weights: ` | Set ingress backend weights specified as percentage or decimal numbers in YAML. (6) | +| `traefik.ingress.kubernetes.io/whitelist-source-range: "1.2.3.0/24, fe80::/16"` | A comma-separated list of IP ranges permitted for access (7). | +| `ingress.kubernetes.io/whitelist-x-forwarded-for: "true"` | Use `X-Forwarded-For` header as valid source of IP for the white list. | +| `ingress.kubernetes.io/protocol: ` | Set the protocol Traefik will use to communicate with pods. | +<1> `traefik.ingress.kubernetes.io/app-root`: +Non-root paths will not be affected by this annotation and handled normally. +This annotation may not be combined with other redirect annotations. +Trying to do so will result in the other redirects being ignored. +This annotation can be used in combination with `traefik.ingress.kubernetes.io/redirect-permanent` to configure whether the `app-root` redirect is a 301 or a 302. -<1> `traefik.ingress.kubernetes.io/error-pages` example: +<2> `traefik.ingress.kubernetes.io/error-pages` example: ```yaml foo: @@ -184,7 +190,31 @@ fii: query: /bir ``` -<2> `traefik.ingress.kubernetes.io/rate-limit` example: +<3> `traefik.ingress.kubernetes.io/pass-client-tls-cert` example: + +```yaml +# add escaped pem in the `X-Forwarded-Tls-Client-Cert` header +pem: true +# add escaped certificate following infos in the `X-Forwarded-Tls-Client-Cert-Infos` header +infos: + notafter: true + notbefore: true + sans: true + subject: + country: true + province: true + locality: true + organization: true + commonname: true + serialnumber: true +``` + +If `pem` is set, it will add a `X-Forwarded-Tls-Client-Cert` header that contains the escaped pem as value. +If at least one flag of the `infos` part is set, it will add a `X-Forwarded-Tls-Client-Cert-Infos` header that contains an escaped string composed of the client certificate data selected by the infos flags. +This infos part is composed like the following example (not escaped): +```Subject="C=FR,ST=SomeState,L=Lyon,O=Cheese,CN=*.cheese.org",NB=1531900816,NA=1563436816,SAN=*.cheese.org,*.cheese.net,cheese.in,test@cheese.org,test@cheese.net,10.0.1.0,10.0.1.2``` + +<4> `traefik.ingress.kubernetes.io/rate-limit` example: ```yaml extractorfunc: client.ip @@ -199,16 +229,10 @@ rateset: burst: 18 ``` -<3> `traefik.ingress.kubernetes.io/rule-type` +<5> `traefik.ingress.kubernetes.io/rule-type` Note: `ReplacePath` is deprecated in this annotation, use the `traefik.ingress.kubernetes.io/request-modifier` annotation instead. Default: `PathPrefix`. -<4> `traefik.ingress.kubernetes.io/app-root`: -Non-root paths will not be affected by this annotation and handled normally. -This annotation may not be combined with other redirect annotations. -Trying to do so will result in the other redirects being ignored. -This annotation can be used in combination with `traefik.ingress.kubernetes.io/redirect-permanent` to configure whether the `app-root` redirect is a 301 or a 302. - -<5> `traefik.ingress.kubernetes.io/service-weights`: +<6> `traefik.ingress.kubernetes.io/service-weights`: Service weights enable to split traffic across multiple backing services in a fine-grained manner. Example: @@ -236,7 +260,7 @@ For each path definition, this annotation will fail if: See also the [user guide section traffic splitting](/user-guide/kubernetes/#traffic-splitting). -<6> `traefik.ingress.kubernetes.io/whitelist-source-range`: +<7> `traefik.ingress.kubernetes.io/whitelist-source-range`: All source IPs are permitted if the list is empty or a single range is ill-formatted. Please note, you may have to set `service.spec.externalTrafficPolicy` to the value `Local` to preserve the source IP of the request for filtering. Please see [this link](https://kubernetes.io/docs/tutorials/services/source-ip/) for more information. diff --git a/provider/kubernetes/annotations.go b/provider/kubernetes/annotations.go index 03805a4a7..c1b4abde3 100644 --- a/provider/kubernetes/annotations.go +++ b/provider/kubernetes/annotations.go @@ -22,7 +22,8 @@ const ( annotationKubernetesWhiteListSourceRange = "ingress.kubernetes.io/whitelist-source-range" annotationKubernetesWhiteListUseXForwardedFor = "ingress.kubernetes.io/whitelist-x-forwarded-for" annotationKubernetesPreserveHost = "ingress.kubernetes.io/preserve-host" - annotationKubernetesPassTLSCert = "ingress.kubernetes.io/pass-tls-cert" + annotationKubernetesPassTLSCert = "ingress.kubernetes.io/pass-tls-cert" // Deprecated + annotationKubernetesPassTLSClientCert = "ingress.kubernetes.io/pass-client-tls-cert" annotationKubernetesFrontendEntryPoints = "ingress.kubernetes.io/frontend-entry-points" annotationKubernetesPriority = "ingress.kubernetes.io/priority" annotationKubernetesCircuitBreakerExpression = "ingress.kubernetes.io/circuit-breaker-expression" diff --git a/provider/kubernetes/builder_configuration_test.go b/provider/kubernetes/builder_configuration_test.go index ccb82505c..7ec4a9f94 100644 --- a/provider/kubernetes/builder_configuration_test.go +++ b/provider/kubernetes/builder_configuration_test.go @@ -382,12 +382,34 @@ func limitPeriod(period time.Duration) func(*types.Rate) { } } +// Deprecated func passTLSCert() func(*types.Frontend) { return func(f *types.Frontend) { f.PassTLSCert = true } } +func passTLSClientCert() func(*types.Frontend) { + return func(f *types.Frontend) { + f.PassTLSClientCert = &types.TLSClientHeaders{ + PEM: true, + Infos: &types.TLSClientCertificateInfos{ + NotAfter: true, + NotBefore: true, + Subject: &types.TLSCLientCertificateSubjectInfos{ + Country: true, + Province: true, + Locality: true, + Organization: true, + CommonName: true, + SerialNumber: true, + }, + Sans: true, + }, + } + } +} + func routes(opts ...func(*types.Route) string) func(*types.Frontend) { return func(f *types.Frontend) { f.Routes = make(map[string]types.Route) diff --git a/provider/kubernetes/kubernetes.go b/provider/kubernetes/kubernetes.go index 94f8d825d..0aa07d85e 100644 --- a/provider/kubernetes/kubernetes.go +++ b/provider/kubernetes/kubernetes.go @@ -62,7 +62,7 @@ type Provider struct { Token string `description:"Kubernetes bearer token (not needed for in-cluster client)"` CertAuthFilePath string `description:"Kubernetes certificate authority file path (not needed for in-cluster client)"` DisablePassHostHeaders bool `description:"Kubernetes disable PassHost Headers" export:"true"` - EnablePassTLSCert bool `description:"Kubernetes enable Pass TLS Client Certs" export:"true"` + EnablePassTLSCert bool `description:"Kubernetes enable Pass TLS Client Certs" export:"true"` // Deprecated Namespaces Namespaces `description:"Kubernetes namespaces" export:"true"` LabelSelector string `description:"Kubernetes Ingress label selector to use" export:"true"` IngressClass string `description:"Value of kubernetes.io/ingress.class annotation to watch for" export:"true"` @@ -275,22 +275,23 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error) } passHostHeader := getBoolValue(i.Annotations, annotationKubernetesPreserveHost, !p.DisablePassHostHeaders) - passTLSCert := getBoolValue(i.Annotations, annotationKubernetesPassTLSCert, p.EnablePassTLSCert) + passTLSCert := getBoolValue(i.Annotations, annotationKubernetesPassTLSCert, p.EnablePassTLSCert) // Deprecated entryPoints := getSliceStringValue(i.Annotations, annotationKubernetesFrontendEntryPoints) frontend = &types.Frontend{ - Backend: baseName, - PassHostHeader: passHostHeader, - PassTLSCert: passTLSCert, - Routes: make(map[string]types.Route), - Priority: priority, - WhiteList: getWhiteList(i), - Redirect: getFrontendRedirect(i, baseName, pa.Path), - EntryPoints: entryPoints, - Headers: getHeader(i), - Errors: getErrorPages(i), - RateLimit: getRateLimit(i), - Auth: auth, + Backend: baseName, + PassHostHeader: passHostHeader, + PassTLSCert: passTLSCert, + PassTLSClientCert: getPassTLSClientCert(i), + Routes: make(map[string]types.Route), + Priority: priority, + WhiteList: getWhiteList(i), + Redirect: getFrontendRedirect(i, baseName, pa.Path), + EntryPoints: entryPoints, + Headers: getHeader(i), + Errors: getErrorPages(i), + RateLimit: getRateLimit(i), + Auth: auth, } } @@ -532,22 +533,23 @@ func (p *Provider) addGlobalBackend(cl Client, i *extensionsv1beta1.Ingress, tem } passHostHeader := getBoolValue(i.Annotations, annotationKubernetesPreserveHost, !p.DisablePassHostHeaders) - passTLSCert := getBoolValue(i.Annotations, annotationKubernetesPassTLSCert, p.EnablePassTLSCert) + passTLSCert := getBoolValue(i.Annotations, annotationKubernetesPassTLSCert, p.EnablePassTLSCert) // Deprecated priority := getIntValue(i.Annotations, annotationKubernetesPriority, 0) entryPoints := getSliceStringValue(i.Annotations, annotationKubernetesFrontendEntryPoints) templateObjects.Frontends[defaultFrontendName] = &types.Frontend{ - Backend: defaultBackendName, - PassHostHeader: passHostHeader, - PassTLSCert: passTLSCert, - Routes: make(map[string]types.Route), - Priority: priority, - WhiteList: getWhiteList(i), - Redirect: getFrontendRedirect(i, defaultFrontendName, "/"), - EntryPoints: entryPoints, - Headers: getHeader(i), - Errors: getErrorPages(i), - RateLimit: getRateLimit(i), + Backend: defaultBackendName, + PassHostHeader: passHostHeader, + PassTLSCert: passTLSCert, + PassTLSClientCert: getPassTLSClientCert(i), + Routes: make(map[string]types.Route), + Priority: priority, + WhiteList: getWhiteList(i), + Redirect: getFrontendRedirect(i, defaultFrontendName, "/"), + EntryPoints: entryPoints, + Headers: getHeader(i), + Errors: getErrorPages(i), + RateLimit: getRateLimit(i), } templateObjects.Frontends[defaultFrontendName].Routes["/"] = types.Route{ @@ -1084,6 +1086,22 @@ func getRateLimit(i *extensionsv1beta1.Ingress) *types.RateLimit { return rateLimit } +func getPassTLSClientCert(i *extensionsv1beta1.Ingress) *types.TLSClientHeaders { + var passTLSClientCert *types.TLSClientHeaders + + passRaw := getStringValue(i.Annotations, annotationKubernetesPassTLSClientCert, "") + if len(passRaw) > 0 { + passTLSClientCert = &types.TLSClientHeaders{} + err := yaml.Unmarshal([]byte(passRaw), passTLSClientCert) + if err != nil { + log.Error(err) + return nil + } + } + + return passTLSClientCert +} + func templateSafeString(value string) error { _, err := strconv.Unquote(`"` + value + `"`) return err diff --git a/provider/kubernetes/kubernetes_test.go b/provider/kubernetes/kubernetes_test.go index 4c04ce750..d1aba1ad7 100644 --- a/provider/kubernetes/kubernetes_test.go +++ b/provider/kubernetes/kubernetes_test.go @@ -728,6 +728,7 @@ func TestGetPassHostHeader(t *testing.T) { assert.Equal(t, expected, actual) } +// Deprecated func TestGetPassTLSCert(t *testing.T) { ingresses := []*extensionsv1beta1.Ingress{ buildIngress(iNamespace("awesome"), @@ -1102,6 +1103,20 @@ func TestIngressAnnotations(t *testing.T) { buildIngress( iNamespace("testing"), iAnnotation(annotationKubernetesPassTLSCert, "true"), + iAnnotation(annotationKubernetesPassTLSClientCert, ` +pem: true +infos: + notafter: true + notbefore: true + sans: true + subject: + country: true + province: true + locality: true + organization: true + commonname: true + serialnumber: true +`), iAnnotation(annotationKubernetesIngressClass, traefikDefaultRealm), iRules( iRule( @@ -1500,13 +1515,7 @@ rateset: ), frontend("other/sslstuff", passHostHeader(), - passTLSCert(), - routes( - route("/sslstuff", "PathPrefix:/sslstuff"), - route("other", "Host:other")), - ), - frontend("other/sslstuff", - passHostHeader(), + passTLSClientCert(), passTLSCert(), routes( route("/sslstuff", "PathPrefix:/sslstuff"), diff --git a/templates/kubernetes.tmpl b/templates/kubernetes.tmpl index 57e5e6344..c522ffae5 100644 --- a/templates/kubernetes.tmpl +++ b/templates/kubernetes.tmpl @@ -129,6 +129,28 @@ {{end}} {{end}} + {{if $frontend.PassTLSClientCert }} + [frontends."{{ $frontendName }}".passTLSClientCert] + pem = {{ $frontend.PassTLSClientCert.PEM }} + {{ $infos := $frontend.PassTLSClientCert.Infos }} + {{if $infos }} + [frontends."{{ $frontendName }}".passTLSClientCert.infos] + notAfter = {{ $infos.NotAfter }} + notBefore = {{ $infos.NotBefore }} + sans = {{ $infos.Sans }} + {{ $subject := $infos.Subject }} + {{if $subject }} + [frontends."{{ $frontendName }}".passTLSClientCert.infos.subject] + country = {{ $subject.Country }} + province = {{ $subject.Province }} + locality = {{ $subject.Locality }} + organization = {{ $subject.Organization }} + commonName = {{ $subject.CommonName }} + serialNumber = {{ $subject.SerialNumber }} + {{end}} + {{end}} + {{end}} + {{if $frontend.Headers }} [frontends."{{ $frontendName }}".headers] SSLRedirect = {{ $frontend.Headers.SSLRedirect }}