From c66d9de759b4bdebf8e170539eac1bcdd1295894 Mon Sep 17 00:00:00 2001 From: Tiscs Sun Date: Thu, 7 Dec 2017 05:26:03 +0800 Subject: [PATCH] Custom headers by service labels for docker backends --- autogen/gentemplates/gen.go | 12 +++++ docs/configuration/backends/docker.md | 24 +++++++++ provider/docker/config.go | 24 +++++---- provider/docker/config_service.go | 14 +++++ provider/docker/config_service_test.go | 72 ++++++++++++++++++++++++++ provider/label/label.go | 37 +++++++------ templates/docker.tmpl | 12 +++++ 7 files changed, 169 insertions(+), 26 deletions(-) diff --git a/autogen/gentemplates/gen.go b/autogen/gentemplates/gen.go index dd7db4c23..c70d8fe14 100644 --- a/autogen/gentemplates/gen.go +++ b/autogen/gentemplates/gen.go @@ -187,6 +187,18 @@ var _templatesDockerTmpl = []byte(`{{$backendServers := .Servers}} {{end}}] [frontends."frontend-{{getServiceBackend $container $serviceName}}".routes."service-{{$serviceName | replace "/" "" | replace "." "-"}}"] rule = "{{getServiceFrontendRule $container $serviceName}}" + {{if hasServiceRequestHeaders $container $serviceName}} + [frontends."frontend-{{getServiceBackend $container $serviceName}}".headers.customrequestheaders] + {{range $k, $v := getServiceRequestHeaders $container $serviceName}} + {{$k}} = "{{$v}}" + {{end}} + {{end}} + {{if hasServiceResponseHeaders $container $serviceName}} + [frontends."frontend-{{getServiceBackend $container $serviceName}}".headers.customresponseheaders] + {{range $k, $v := getServiceResponseHeaders $container $serviceName}} + {{$k}} = "{{$v}}" + {{end}} + {{end}} {{end}} {{else}} [frontends."frontend-{{$frontend}}"] diff --git a/docs/configuration/backends/docker.md b/docs/configuration/backends/docker.md index 2dfd9fdd7..9c7f0a2a7 100644 --- a/docs/configuration/backends/docker.md +++ b/docs/configuration/backends/docker.md @@ -215,6 +215,30 @@ Services labels can be used for overriding default behaviour | `traefik..frontend.rule` | Overrides `traefik.frontend.rule`. | | `traefik..frontend.redirect` | Overrides `traefik.frontend.redirect`. | +#### Security Headers + +| Label | Description | +|-------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `traefik..frontend.headers.allowedHosts=EXPR` | Provides a list of allowed hosts that requests will be processed. Format: `Host1,Host2` | +| `traefik..frontend.headers.customRequestHeaders=EXPR ` | Provides the container with custom request headers that will be appended to each request forwarded to the container. Format: HEADER:value||HEADER2:value2 | +| `traefik..frontend.headers.customResponseHeaders=EXPR` | Appends the headers to each response returned by the container, before forwarding the response to the client. Format: HEADER:value||HEADER2:value2 | +| `traefik..frontend.headers.hostsProxyHeaders=EXPR ` | Provides a list of headers that the proxied hostname may be stored. Format: `HEADER1,HEADER2` | +| `traefik..frontend.headers.SSLRedirect=true` | Forces the frontend to redirect to SSL if a non-SSL request is sent. | +| `traefik..frontend.headers.SSLTemporaryRedirect=true` | Forces the frontend to redirect to SSL if a non-SSL request is sent, but by sending a 302 instead of a 301. | +| `traefik..frontend.headers.SSLHost=HOST` | This setting configures the hostname that redirects will be based on. Default is "", which is the same host as the request. | +| `traefik..frontend.headers.SSLProxyHeaders=EXPR` | Header combinations that would signify a proper SSL Request (Such as `X-Forwarded-For:https`). Format: HEADER:value||HEADER2:value2 | +| `traefik..frontend.headers.STSSeconds=315360000` | Sets the max-age of the STS header. | +| `traefik..frontend.headers.STSIncludeSubdomains=true` | Adds the `IncludeSubdomains` section of the STS header. | +| `traefik..frontend.headers.STSPreload=true` | Adds the preload flag to the STS header. | +| `traefik..frontend.headers.forceSTSHeader=false` | Adds the STS header to non-SSL requests. | +| `traefik..frontend.headers.frameDeny=false` | Adds the `X-Frame-Options` header with the value of `DENY`. | +| `traefik..frontend.headers.customFrameOptionsValue=VALUE` | Overrides the `X-Frame-Options` header with the custom value. | +| `traefik..frontend.headers.contentTypeNosniff=true` | Adds the `X-Content-Type-Options` header with the value `nosniff`. | +| `traefik..frontend.headers.browserXSSFilter=true` | Adds the X-XSS-Protection header with the value `1; mode=block`. | +| `traefik..frontend.headers.contentSecurityPolicy=VALUE` | Adds CSP Header with the custom value. | +| `traefik..frontend.headers.publicKey=VALUE` | Adds pinned HTST public key header. | +| `traefik..frontend.headers.referrerPolicy=VALUE` | Adds referrer policy header. | +| `traefik..frontend.headers.isDevelopment=false` | This will cause the `AllowedHosts`, `SSLRedirect`, and `STSSeconds`/`STSIncludeSubdomains` options to be ignored during development.
When deploying to production, be sure to set this to false. | !!! note if a label is defined both as a `container label` and a `service label` (for example `traefik..port=PORT` and `traefik.port=PORT` ), the `service label` is used to defined the `` property (`port` in the example). diff --git a/provider/docker/config.go b/provider/docker/config.go index b3559587c..34fca901a 100644 --- a/provider/docker/config.go +++ b/provider/docker/config.go @@ -81,16 +81,20 @@ func (p *Provider) buildConfiguration(containersInspected []dockerData) *types.C "hasIsDevelopmentHeaders": hasFunc(label.TraefikFrontendIsDevelopment), "getIsDevelopmentHeaders": getFuncBoolLabel(label.TraefikFrontendIsDevelopment, false), - "hasServices": hasServices, - "getServiceNames": getServiceNames, - "getServicePort": getServicePort, - "getServiceWeight": getFuncServiceStringLabel(label.SuffixWeight, label.DefaultWeight), - "getServiceProtocol": getFuncServiceStringLabel(label.SuffixProtocol, label.DefaultProtocol), - "getServiceEntryPoints": getFuncServiceSliceStringLabel(label.SuffixFrontendEntryPoints), - "getServiceBasicAuth": getFuncServiceSliceStringLabel(label.SuffixFrontendAuthBasic), - "getServiceFrontendRule": p.getServiceFrontendRule, - "getServicePassHostHeader": getFuncServiceStringLabel(label.SuffixFrontendPassHostHeader, label.DefaultPassHostHeader), - "getServicePriority": getFuncServiceStringLabel(label.SuffixFrontendPriority, label.DefaultFrontendPriority), + "hasServices": hasServices, + "getServiceNames": getServiceNames, + "getServicePort": getServicePort, + "hasServiceRequestHeaders": hasFuncServiceLabel(label.SuffixFrontendRequestHeaders), + "getServiceRequestHeaders": getFuncServiceMapLabel(label.SuffixFrontendRequestHeaders), + "hasServiceResponseHeaders": hasFuncServiceLabel(label.SuffixFrontendResponseHeaders), + "getServiceResponseHeaders": getFuncServiceMapLabel(label.SuffixFrontendResponseHeaders), + "getServiceWeight": getFuncServiceStringLabel(label.SuffixWeight, label.DefaultWeight), + "getServiceProtocol": getFuncServiceStringLabel(label.SuffixProtocol, label.DefaultProtocol), + "getServiceEntryPoints": getFuncServiceSliceStringLabel(label.SuffixFrontendEntryPoints), + "getServiceBasicAuth": getFuncServiceSliceStringLabel(label.SuffixFrontendAuthBasic), + "getServiceFrontendRule": p.getServiceFrontendRule, + "getServicePassHostHeader": getFuncServiceStringLabel(label.SuffixFrontendPassHostHeader, label.DefaultPassHostHeader), + "getServicePriority": getFuncServiceStringLabel(label.SuffixFrontendPriority, label.DefaultFrontendPriority), } // filter containers filteredContainers := fun.Filter(func(container dockerData) bool { diff --git a/provider/docker/config_service.go b/provider/docker/config_service.go index ab0a5244f..ca84e4499 100644 --- a/provider/docker/config_service.go +++ b/provider/docker/config_service.go @@ -88,6 +88,12 @@ func getServicePort(container dockerData, serviceName string) string { // Service label functions +func getFuncServiceMapLabel(labelSuffix string) func(container dockerData, serviceName string) map[string]string { + return func(container dockerData, serviceName string) map[string]string { + return getServiceMapLabel(container, serviceName, labelSuffix) + } +} + func getFuncServiceSliceStringLabel(labelSuffix string) func(container dockerData, serviceName string) []string { return func(container dockerData, serviceName string) []string { return getServiceSliceStringLabel(container, serviceName, labelSuffix) @@ -114,6 +120,14 @@ func hasServiceLabel(container dockerData, serviceName string, labelSuffix strin return label.Has(container.Labels, label.Prefix+labelSuffix) } +func getServiceMapLabel(container dockerData, serviceName string, labelSuffix string) map[string]string { + if value, ok := getServiceLabels(container, serviceName)[labelSuffix]; ok { + lblName := label.GetServiceLabel(labelSuffix, serviceName) + return label.ParseMapValue(lblName, value) + } + return label.GetMapValue(container.Labels, label.Prefix+labelSuffix) +} + func getServiceSliceStringLabel(container dockerData, serviceName string, labelSuffix string) []string { if value, ok := getServiceLabels(container, serviceName)[labelSuffix]; ok { return label.SplitAndTrimString(value, ",") diff --git a/provider/docker/config_service_test.go b/provider/docker/config_service_test.go index 54808dc77..d6251d801 100644 --- a/provider/docker/config_service_test.go +++ b/provider/docker/config_service_test.go @@ -7,8 +7,80 @@ import ( "github.com/containous/traefik/provider/label" docker "github.com/docker/docker/api/types" + "github.com/stretchr/testify/assert" ) +func TestDockerGetFuncMapLabel(t *testing.T) { + serviceName := "myservice" + fakeSuffix := "frontend.foo" + fakeLabel := label.Prefix + fakeSuffix + + testCases := []struct { + desc string + container docker.ContainerJSON + suffixLabel string + expectedKey string + expected map[string]string + }{ + { + desc: "fallback to container label value", + container: containerJSON(labels(map[string]string{ + fakeLabel: "X-Custom-Header: ContainerRequestHeader", + })), + suffixLabel: fakeSuffix, + expected: map[string]string{ + "X-Custom-Header": "ContainerRequestHeader", + }, + }, + { + desc: "use service label instead of container label", + container: containerJSON(labels(map[string]string{ + fakeLabel: "X-Custom-Header: ContainerRequestHeader", + label.GetServiceLabel(fakeLabel, serviceName): "X-Custom-Header: ServiceRequestHeader", + })), + suffixLabel: fakeSuffix, + expected: map[string]string{ + "X-Custom-Header": "ServiceRequestHeader", + }, + }, + { + desc: "use service label with an empty value instead of container label", + container: containerJSON(labels(map[string]string{ + fakeLabel: "X-Custom-Header: ContainerRequestHeader", + label.GetServiceLabel(fakeLabel, serviceName): "X-Custom-Header: ", + })), + suffixLabel: fakeSuffix, + expected: map[string]string{ + "X-Custom-Header": "", + }, + }, + { + desc: "multiple values", + container: containerJSON(labels(map[string]string{ + fakeLabel: "X-Custom-Header: MultiHeaders || Authorization: Basic YWRtaW46YWRtaW4=", + })), + suffixLabel: fakeSuffix, + expected: map[string]string{ + "X-Custom-Header": "MultiHeaders", + "Authorization": "Basic YWRtaW46YWRtaW4=", + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + dData := parseContainer(test.container) + + values := getFuncServiceMapLabel(test.suffixLabel)(dData, serviceName) + + assert.EqualValues(t, test.expected, values) + }) + } +} + func TestDockerGetFuncServiceStringLabel(t *testing.T) { testCases := []struct { container docker.ContainerJSON diff --git a/provider/label/label.go b/provider/label/label.go index 12271269b..2f8bc029c 100644 --- a/provider/label/label.go +++ b/provider/label/label.go @@ -134,6 +134,26 @@ func GetSliceStringValueP(labels *map[string]string, labelName string) []string return GetSliceStringValue(*labels, labelName) } +// ParseMapValue get Map value for a label value +func ParseMapValue(labelName, values string) map[string]string { + mapValue := make(map[string]string) + + for _, parts := range strings.Split(values, mapEntrySeparator) { + pair := strings.SplitN(parts, mapValueSeparator, 2) + if len(pair) != 2 { + log.Warnf("Could not load %q: %q, skipping...", labelName, parts) + } else { + mapValue[http.CanonicalHeaderKey(strings.TrimSpace(pair[0]))] = strings.TrimSpace(pair[1]) + } + } + + if len(mapValue) == 0 { + log.Errorf("Could not load %q, skipping...", labelName) + return nil + } + return mapValue +} + // GetMapValue get Map value associated to a label func GetMapValue(labels map[string]string, labelName string) map[string]string { if values, ok := labels[labelName]; ok { @@ -143,22 +163,7 @@ func GetMapValue(labels map[string]string, labelName string) map[string]string { return nil } - mapValue := make(map[string]string) - - for _, parts := range strings.Split(values, mapEntrySeparator) { - pair := strings.SplitN(parts, mapValueSeparator, 2) - if len(pair) != 2 { - log.Warnf("Could not load %q: %q, skipping...", labelName, parts) - } else { - mapValue[http.CanonicalHeaderKey(strings.TrimSpace(pair[0]))] = strings.TrimSpace(pair[1]) - } - } - - if len(mapValue) == 0 { - log.Errorf("Could not load %q, skipping...", labelName) - return nil - } - return mapValue + return ParseMapValue(labelName, values) } return nil diff --git a/templates/docker.tmpl b/templates/docker.tmpl index d4a7c9a84..fa94c67d1 100644 --- a/templates/docker.tmpl +++ b/templates/docker.tmpl @@ -62,6 +62,18 @@ {{end}}] [frontends."frontend-{{getServiceBackend $container $serviceName}}".routes."service-{{$serviceName | replace "/" "" | replace "." "-"}}"] rule = "{{getServiceFrontendRule $container $serviceName}}" + {{if hasServiceRequestHeaders $container $serviceName}} + [frontends."frontend-{{getServiceBackend $container $serviceName}}".headers.customrequestheaders] + {{range $k, $v := getServiceRequestHeaders $container $serviceName}} + {{$k}} = "{{$v}}" + {{end}} + {{end}} + {{if hasServiceResponseHeaders $container $serviceName}} + [frontends."frontend-{{getServiceBackend $container $serviceName}}".headers.customresponseheaders] + {{range $k, $v := getServiceResponseHeaders $container $serviceName}} + {{$k}} = "{{$v}}" + {{end}} + {{end}} {{end}} {{else}} [frontends."frontend-{{$frontend}}"]