Kubernetes security header annotations

This commit is contained in:
Daniel Tomcej 2017-11-28 06:36:03 -06:00 committed by Traefiker
parent ad07a6ab2b
commit 4cb6241e93
5 changed files with 251 additions and 60 deletions

View file

@ -421,6 +421,52 @@ var _templatesKubernetesTmpl = []byte(`[backends]{{range $backendName, $backend
whitelistSourceRange = [{{range $frontend.WhitelistSourceRange}} whitelistSourceRange = [{{range $frontend.WhitelistSourceRange}}
"{{.}}", "{{.}}",
{{end}}] {{end}}]
[frontends."{{$frontendName}}".headers]
SSLRedirect = {{$frontend.Headers.SSLRedirect}}
SSLTemporaryRedirect = {{$frontend.Headers.SSLTemporaryRedirect}}
SSLHost = "{{$frontend.Headers.SSLHost}}"
STSSeconds = {{$frontend.Headers.STSSeconds}}
STSIncludeSubdomains = {{$frontend.Headers.STSIncludeSubdomains}}
STSPreload = {{$frontend.Headers.STSPreload}}
ForceSTSHeader = {{$frontend.Headers.ForceSTSHeader}}
FrameDeny = {{$frontend.Headers.FrameDeny}}
CustomFrameOptionsValue = "{{$frontend.Headers.CustomFrameOptionsValue}}"
ContentTypeNosniff = {{$frontend.Headers.ContentTypeNosniff}}
BrowserXSSFilter = {{$frontend.Headers.BrowserXSSFilter}}
ContentSecurityPolicy = "{{$frontend.Headers.ContentSecurityPolicy}}"
PublicKey = "{{$frontend.Headers.PublicKey}}"
ReferrerPolicy = "{{$frontend.Headers.ReferrerPolicy}}"
IsDevelopment = {{$frontend.Headers.IsDevelopment}}
{{if $frontend.Headers.CustomRequestHeaders}}
[frontends."{{$frontendName}}".headers.customrequestheaders]
{{range $k, $v := $frontend.Headers.CustomRequestHeaders}}
{{$k}} = "{{$v}}"
{{end}}
{{end}}
{{if $frontend.Headers.CustomResponseHeaders}}
[frontends."{{$frontendName}}".headers.customresponseheaders]
{{range $k, $v := $frontend.Headers.CustomResponseHeaders}}
{{$k}} = "{{$v}}"
{{end}}
{{end}}
{{if $frontend.Headers.AllowedHosts}}
[frontends."{{$frontendName}}".headers.AllowedHosts]
{{range $frontend.Headers.AllowedHosts}}
"{{.}}"
{{end}}
{{end}}
{{if $frontend.Headers.HostsProxyHeaders}}
[frontends."{{$frontendName}}".headers.HostsProxyHeaders]
{{range $frontend.Headers.HostsProxyHeaders}}
"{{.}}"
{{end}}
{{end}}
{{if $frontend.Headers.SSLProxyHeaders}}
[frontends."{{$frontendName}}".headers.SSLProxyHeaders]
{{range $k, $v := $frontend.Headers.SSLProxyHeaders}}
{{$k}} = "{{$v}}"
{{end}}
{{end}}
{{range $routeName, $route := $frontend.Routes}} {{range $routeName, $route := $frontend.Routes}}
[frontends."{{$frontendName}}".routes."{{$routeName}}"] [frontends."{{$frontendName}}".routes."{{$routeName}}"]
rule = "{{$route.Rule}}" rule = "{{$route.Rule}}"

View file

@ -132,6 +132,32 @@ As known from nginx when used as Kubernetes Ingress Controller, a list of IP-Ran
An unset or empty list allows all Source-IPs to access. An unset or empty list allows all Source-IPs to access.
If one of the Net-Specifications are invalid, the whole list is invalid and allows all Source-IPs to access. If one of the Net-Specifications are invalid, the whole list is invalid and allows all Source-IPs to access.
#### Security annotations
The following security annotations can be applied to the ingress object to add security features:
| Annotation | Description |
|----------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `ingress.kubernetes.io/allowed-hosts:EXPR` | Provides a list of allowed hosts that requests will be processed. Format: `Host1,Host2` |
| `ingress.kubernetes.io/custom-request-headers:EXPR ` | Provides the container with custom request headers that will be appended to each request forwarded to the container. Format: `HEADER:value,HEADER2:value2` |
| `ingress.kubernetes.io/custom-response-headers:EXPR` | Appends the headers to each response returned by the container, before forwarding the response to the client. Format: `HEADER:value,HEADER2:value2` |
| `ingress.kubernetes.io/proxy-headers:EXPR ` | Provides a list of headers that the proxied hostname may be stored. Format: `HEADER1,HEADER2` |
| `ingress.kubernetes.io/ssl-redirect:true` | Forces the frontend to redirect to SSL if a non-SSL request is sent. |
| `ingress.kubernetes.io/ssl-temporary-redirect:true` | Forces the frontend to redirect to SSL if a non-SSL request is sent, but by sending a 302 instead of a 301. |
| `ingress.kubernetes.io/ssl-host:HOST` | This setting configures the hostname that redirects will be based on. Default is "", which is the same host as the request. |
| `ingress.kubernetes.io/ssl-proxy-headers:EXPR` | Header combinations that would signify a proper SSL Request (Such as `X-Forwarded-For:https`). Format: `HEADER:value,HEADER2:value2` |
| `ingress.kubernetes.io/hsts-max-age:315360000` | Sets the max-age of the HSTS header. |
| `ngress.kubernetes.io/hsts-include-subdomains:true` | Adds the IncludeSubdomains section of the STS header. |
| `ingress.kubernetes.io/hsts-preload:true` | Adds the preload flag to the HSTS header. |
| `ingress.kubernetes.io/force-hsts:false` | Adds the STS header to non-SSL requests. |
| `ingress.kubernetes.io/frame-deny:false` | Adds the `X-Frame-Options` header with the value of `DENY`. |
| `ingress.kubernetes.io/custom-frame-options-value:VALUE` | Overrides the `X-Frame-Options` header with the custom value. |
| `ingress.kubernetes.io/content-type-nosniff:true` | Adds the `X-Content-Type-Options` header with the value `nosniff`. |
| `ingress.kubernetes.io/browser-xss-filter:true` | Adds the X-XSS-Protection header with the value `1; mode=block`. |
| `ingress.kubernetes.io/content-security-policy:VALUE` | Adds CSP Header with the custom value. |
| `ingress.kubernetes.io/public-key:VALUE` | Adds pinned HTST public key header. |
| `ingress.kubernetes.io/referrer-policy:VALUE` | Adds referrer policy header. |
| `ingress.kubernetes.io/is-development:false` | This will cause the `AllowedHosts`, `SSLRedirect`, and `STSSeconds`/`STSIncludeSubdomains` options to be ignored during development.<br>When deploying to production, be sure to set this to false. |
### Authentication ### Authentication

View file

@ -0,0 +1,62 @@
package kubernetes
import (
"strings"
"github.com/containous/traefik/log"
"github.com/containous/traefik/provider"
"github.com/containous/traefik/types"
"k8s.io/client-go/pkg/apis/extensions/v1beta1"
)
func getBoolAnnotation(meta *v1beta1.Ingress, name string, defaultValue bool) bool {
annotationValue := defaultValue
annotationStringValue, ok := meta.Annotations[name]
switch {
case !ok:
// No op.
case annotationStringValue == "false":
annotationValue = false
case annotationStringValue == "true":
annotationValue = true
default:
log.Warnf("Unknown value '%s' for %s, falling back to %s", name, types.LabelFrontendPassTLSCert, defaultValue)
}
return annotationValue
}
func getStringAnnotation(meta *v1beta1.Ingress, name string) string {
value := meta.Annotations[name]
return value
}
func getSliceAnnotation(meta *v1beta1.Ingress, name string) []string {
var value []string
if annotation, ok := meta.Annotations[name]; ok && annotation != "" {
value = provider.SplitAndTrimString(annotation)
}
if len(value) == 0 {
log.Debugf("Could not load %v annotation, skipping...", name)
return nil
}
return value
}
func getMapAnnotation(meta *v1beta1.Ingress, name string) map[string]string {
value := make(map[string]string)
if annotation := meta.Annotations[name]; annotation != "" {
for _, v := range strings.Split(annotation, ",") {
pair := strings.Split(v, ":")
if len(pair) != 2 {
log.Debugf("Could not load annotation (%v) with value: %v, skipping...", name, pair)
} else {
value[pair[0]] = pair[1]
}
}
}
if len(value) == 0 {
log.Debugf("Could not load %v annotation, skipping...", name)
return nil
}
return value
}

View file

@ -36,6 +36,26 @@ const (
annotationKubernetesAuthSecret = "ingress.kubernetes.io/auth-secret" annotationKubernetesAuthSecret = "ingress.kubernetes.io/auth-secret"
annotationKubernetesRewriteTarget = "ingress.kubernetes.io/rewrite-target" annotationKubernetesRewriteTarget = "ingress.kubernetes.io/rewrite-target"
annotationKubernetesWhitelistSourceRange = "ingress.kubernetes.io/whitelist-source-range" annotationKubernetesWhitelistSourceRange = "ingress.kubernetes.io/whitelist-source-range"
annotationKubernetesSSLRedirect = "ingress.kubernetes.io/ssl-redirect"
annotationKubernetesHSTSMaxAge = "ingress.kubernetes.io/hsts-max-age"
annotationKubernetesHSTSIncludeSubdomains = "ingress.kubernetes.io/hsts-include-subdomains"
annotationKubernetesCustomRequestHeaders = "ingress.kubernetes.io/custom-request-headers"
annotationKubernetesCustomResponseHeaders = "ingress.kubernetes.io/custom-response-headers"
annotationKubernetesAllowedHosts = "ingress.kubernetes.io/allowed-hosts"
annotationKubernetesProxyHeaders = "ingress.kubernetes.io/proxy-headers"
annotationKubernetesSSLTemporaryRedirect = "ingress.kubernetes.io/ssl-temporary-redirect"
annotationKubernetesSSLHost = "ingress.kubernetes.io/ssl-host"
annotationKubernetesSSLProxyHeaders = "ingress.kubernetes.io/ssl-proxy-headers"
annotationKubernetesHSTSPreload = "ingress.kubernetes.io/hsts-preload"
annotationKubernetesForceHSTSHeader = "ingress.kubernetes.io/force-hsts"
annotationKubernetesFrameDeny = "ingress.kubernetes.io/frame-deny"
annotationKubernetesCustomFrameOptionsValue = "ingress.kubernetes.io/custom-frame-options-value"
annotationKubernetesContentTypeNosniff = "ingress.kubernetes.io/content-type-nosniff"
annotationKubernetesBrowserXSSFilter = "ingress.kubernetes.io/browser-xss-filter"
annotationKubernetesContentSecurityPolicy = "ingress.kubernetes.io/content-security-policy"
annotationKubernetesPublicKey = "ingress.kubernetes.io/public-key"
annotationKubernetesReferrerPolicy = "ingress.kubernetes.io/referrer-policy"
annotationKubernetesIsDevelopment = "ingress.kubernetes.io/is-development"
) )
const traefikDefaultRealm = "traefik" const traefikDefaultRealm = "traefik"
@ -53,7 +73,7 @@ type Provider struct {
lastConfiguration safe.Safe lastConfiguration safe.Safe
} }
func (p *Provider) newK8sClient() (Client, error) { func (p Provider) newK8sClient() (Client, error) {
withEndpoint := "" withEndpoint := ""
if p.Endpoint != "" { if p.Endpoint != "" {
withEndpoint = fmt.Sprintf(" with endpoint %v", p.Endpoint) withEndpoint = fmt.Sprintf(" with endpoint %v", p.Endpoint)
@ -166,18 +186,18 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error)
} }
} }
passHostHeader := getAnnotationPassHostHeader(i, p) passHostHeader := getBoolAnnotation(i, types.LabelFrontendPassHostHeader, !p.DisablePassHostHeaders)
passTLSCert := getAnnotationPassTLSCert(i, p) passTLSCert := getBoolAnnotation(i, types.LabelFrontendPassTLSCert, p.EnablePassTLSCert)
if realm := i.Annotations[annotationKubernetesAuthRealm]; realm != "" && realm != traefikDefaultRealm { if realm := i.Annotations[annotationKubernetesAuthRealm]; realm != "" && realm != traefikDefaultRealm {
log.Errorf("Value for annotation %q on ingress %s/%s invalid: no realm customization supported", annotationKubernetesAuthRealm, i.ObjectMeta.Namespace, i.ObjectMeta.Name) log.Errorf("Value for annotation %q on ingress %s/%s invalid: no realm customization supported", annotationKubernetesAuthRealm, i.ObjectMeta.Namespace, i.ObjectMeta.Name)
delete(templateObjects.Backends, r.Host+pa.Path) delete(templateObjects.Backends, r.Host+pa.Path)
continue continue
} }
entryPoints := getEntrypoints(i) entryPoints := getSliceAnnotation(i, types.LabelFrontendEntryPoints)
whitelistSourceRangeAnnotation := i.Annotations[annotationKubernetesWhitelistSourceRange] whitelistSourceRange := getSliceAnnotation(i, annotationKubernetesWhitelistSourceRange)
whitelistSourceRange := provider.SplitAndTrimString(whitelistSourceRangeAnnotation)
entryPointRedirect, _ := i.Annotations[types.LabelFrontendRedirect] entryPointRedirect, _ := i.Annotations[types.LabelFrontendRedirect]
@ -188,7 +208,30 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error)
continue continue
} }
priority := p.getPriority(pa, i) priority := getPriority(i)
headers := types.Headers{
CustomRequestHeaders: getMapAnnotation(i, annotationKubernetesCustomRequestHeaders),
CustomResponseHeaders: getMapAnnotation(i, annotationKubernetesCustomResponseHeaders),
AllowedHosts: getSliceAnnotation(i, annotationKubernetesAllowedHosts),
HostsProxyHeaders: getSliceAnnotation(i, annotationKubernetesProxyHeaders),
SSLRedirect: getBoolAnnotation(i, annotationKubernetesSSLRedirect, false),
SSLTemporaryRedirect: getBoolAnnotation(i, annotationKubernetesSSLTemporaryRedirect, false),
SSLHost: getStringAnnotation(i, annotationKubernetesSSLHost),
SSLProxyHeaders: getMapAnnotation(i, annotationKubernetesSSLProxyHeaders),
STSSeconds: getSTSSeconds(i),
STSIncludeSubdomains: getBoolAnnotation(i, annotationKubernetesHSTSIncludeSubdomains, false),
STSPreload: getBoolAnnotation(i, annotationKubernetesHSTSPreload, false),
ForceSTSHeader: getBoolAnnotation(i, annotationKubernetesForceHSTSHeader, false),
FrameDeny: getBoolAnnotation(i, annotationKubernetesFrameDeny, false),
CustomFrameOptionsValue: getStringAnnotation(i, annotationKubernetesCustomFrameOptionsValue),
ContentTypeNosniff: getBoolAnnotation(i, annotationKubernetesContentTypeNosniff, false),
BrowserXSSFilter: getBoolAnnotation(i, annotationKubernetesBrowserXSSFilter, false),
ContentSecurityPolicy: getStringAnnotation(i, annotationKubernetesContentSecurityPolicy),
PublicKey: getStringAnnotation(i, annotationKubernetesPublicKey),
ReferrerPolicy: getStringAnnotation(i, annotationKubernetesReferrerPolicy),
IsDevelopment: getBoolAnnotation(i, annotationKubernetesIsDevelopment, false),
}
templateObjects.Frontends[r.Host+pa.Path] = &types.Frontend{ templateObjects.Frontends[r.Host+pa.Path] = &types.Frontend{
Backend: r.Host + pa.Path, Backend: r.Host + pa.Path,
@ -200,6 +243,7 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error)
WhitelistSourceRange: whitelistSourceRange, WhitelistSourceRange: whitelistSourceRange,
Redirect: entryPointRedirect, Redirect: entryPointRedirect,
EntryPoints: entryPoints, EntryPoints: entryPoints,
Headers: headers,
} }
} }
if len(r.Host) > 0 { if len(r.Host) > 0 {
@ -312,37 +356,21 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error)
return &templateObjects, nil return &templateObjects, nil
} }
func getEntrypoints(i *v1beta1.Ingress) []string { func (p Provider) loadConfig(templateObjects types.Configuration) *types.Configuration {
entrypointsAnnotation, ok := i.Annotations[types.LabelFrontendEntryPoints] var FuncMap = template.FuncMap{}
if ok { configuration, err := p.GetConfiguration("templates/kubernetes.tmpl", FuncMap, templateObjects)
return strings.Split(entrypointsAnnotation, ",") if err != nil {
log.Error(err)
} }
return nil return configuration
} }
func getBoolAnnotation(meta v1.ObjectMeta, name string, defaultValue bool) bool { func getSTSSeconds(i *v1beta1.Ingress) int64 {
annotationValue := defaultValue value, err := strconv.ParseInt(i.ObjectMeta.Annotations[annotationKubernetesHSTSMaxAge], 10, 64)
annotationStringValue, ok := meta.Annotations[name] if err == nil && value > 0 {
switch { return value
case !ok:
// No op.
case annotationStringValue == "false":
annotationValue = false
case annotationStringValue == "true":
annotationValue = true
default:
log.Warnf("Unknown value '%s' for %s, falling back to %s", name, types.LabelFrontendPassTLSCert, defaultValue)
} }
return annotationValue return 0
}
func getAnnotationPassHostHeader(i *v1beta1.Ingress, p *Provider) bool {
return getBoolAnnotation(i.ObjectMeta, types.LabelFrontendPassHostHeader, p.getPassHostHeader())
}
func getAnnotationPassTLSCert(i *v1beta1.Ingress, p *Provider) bool {
return getBoolAnnotation(i.ObjectMeta, types.LabelFrontendPassTLSCert, p.getPassTLSCert())
} }
func getRuleForPath(pa v1beta1.HTTPIngressPath, i *v1beta1.Ingress) string { func getRuleForPath(pa v1beta1.HTTPIngressPath, i *v1beta1.Ingress) string {
@ -364,7 +392,7 @@ func getRuleForPath(pa v1beta1.HTTPIngressPath, i *v1beta1.Ingress) string {
return strings.Join(rules, ";") return strings.Join(rules, ";")
} }
func (p *Provider) getPriority(path v1beta1.HTTPIngressPath, i *v1beta1.Ingress) int { func getPriority(i *v1beta1.Ingress) int {
priority := 0 priority := 0
priorityRaw, ok := i.Annotations[types.LabelFrontendPriority] priorityRaw, ok := i.Annotations[types.LabelFrontendPriority]
@ -464,20 +492,3 @@ func shouldProcessIngress(ingressClass string) bool {
return false return false
} }
} }
func (p *Provider) getPassHostHeader() bool {
return !p.DisablePassHostHeaders
}
func (p *Provider) getPassTLSCert() bool {
return p.EnablePassTLSCert
}
func (p *Provider) loadConfig(templateObjects types.Configuration) *types.Configuration {
var FuncMap = template.FuncMap{}
configuration, err := p.GetConfiguration("templates/kubernetes.tmpl", FuncMap, templateObjects)
if err != nil {
log.Error(err)
}
return configuration
}

View file

@ -32,6 +32,52 @@
whitelistSourceRange = [{{range $frontend.WhitelistSourceRange}} whitelistSourceRange = [{{range $frontend.WhitelistSourceRange}}
"{{.}}", "{{.}}",
{{end}}] {{end}}]
[frontends."{{$frontendName}}".headers]
SSLRedirect = {{$frontend.Headers.SSLRedirect}}
SSLTemporaryRedirect = {{$frontend.Headers.SSLTemporaryRedirect}}
SSLHost = "{{$frontend.Headers.SSLHost}}"
STSSeconds = {{$frontend.Headers.STSSeconds}}
STSIncludeSubdomains = {{$frontend.Headers.STSIncludeSubdomains}}
STSPreload = {{$frontend.Headers.STSPreload}}
ForceSTSHeader = {{$frontend.Headers.ForceSTSHeader}}
FrameDeny = {{$frontend.Headers.FrameDeny}}
CustomFrameOptionsValue = "{{$frontend.Headers.CustomFrameOptionsValue}}"
ContentTypeNosniff = {{$frontend.Headers.ContentTypeNosniff}}
BrowserXSSFilter = {{$frontend.Headers.BrowserXSSFilter}}
ContentSecurityPolicy = "{{$frontend.Headers.ContentSecurityPolicy}}"
PublicKey = "{{$frontend.Headers.PublicKey}}"
ReferrerPolicy = "{{$frontend.Headers.ReferrerPolicy}}"
IsDevelopment = {{$frontend.Headers.IsDevelopment}}
{{if $frontend.Headers.CustomRequestHeaders}}
[frontends."{{$frontendName}}".headers.customrequestheaders]
{{range $k, $v := $frontend.Headers.CustomRequestHeaders}}
{{$k}} = "{{$v}}"
{{end}}
{{end}}
{{if $frontend.Headers.CustomResponseHeaders}}
[frontends."{{$frontendName}}".headers.customresponseheaders]
{{range $k, $v := $frontend.Headers.CustomResponseHeaders}}
{{$k}} = "{{$v}}"
{{end}}
{{end}}
{{if $frontend.Headers.AllowedHosts}}
[frontends."{{$frontendName}}".headers.AllowedHosts]
{{range $frontend.Headers.AllowedHosts}}
"{{.}}"
{{end}}
{{end}}
{{if $frontend.Headers.HostsProxyHeaders}}
[frontends."{{$frontendName}}".headers.HostsProxyHeaders]
{{range $frontend.Headers.HostsProxyHeaders}}
"{{.}}"
{{end}}
{{end}}
{{if $frontend.Headers.SSLProxyHeaders}}
[frontends."{{$frontendName}}".headers.SSLProxyHeaders]
{{range $k, $v := $frontend.Headers.SSLProxyHeaders}}
{{$k}} = "{{$v}}"
{{end}}
{{end}}
{{range $routeName, $route := $frontend.Routes}} {{range $routeName, $route := $frontend.Routes}}
[frontends."{{$frontendName}}".routes."{{$routeName}}"] [frontends."{{$frontendName}}".routes."{{$routeName}}"]
rule = "{{$route.Rule}}" rule = "{{$route.Rule}}"