From e43bec8f5e81c2ebb068eb777a5b97344f247e5f Mon Sep 17 00:00:00 2001 From: candita Date: Wed, 21 Apr 2021 20:14:14 -0400 Subject: [PATCH] NE-310 Enhancement proposal for HSTS route admission plugin --- enhancements/ingress/global-admission-hsts.md | 728 ++++++++++++++++++ .../ingress/global-options-enable-hsts.md | 15 + 2 files changed, 743 insertions(+) create mode 100644 enhancements/ingress/global-admission-hsts.md diff --git a/enhancements/ingress/global-admission-hsts.md b/enhancements/ingress/global-admission-hsts.md new file mode 100644 index 00000000000..a3442033852 --- /dev/null +++ b/enhancements/ingress/global-admission-hsts.md @@ -0,0 +1,728 @@ +--- +title: global-options-enable-hsts +authors: + - "@cholman" + +reviewers: + - "@danehans" + - "@frobware" + - "@knobunc" + - "@Miciah" + - "@miheer" + - "@rfredette" + - "@sgreene570" + +approvers: + - "@frobware" + - "@knobunc" + +creation-date: 2021-04-16 + +last-updated: 2021-04-20 + +status: implementable + +see-also: + - https://bugzilla.redhat.com/show_bug.cgi?id=1512759 + - https://bugzilla.redhat.com/show_bug.cgi?id=1430035 + +replaces: + - enhancements/enhancements/ingress/global-options-enable-hsts.md + +superseded-by: + +--- + +# Global Admission Plugin for HTTP Strict Transport Security (HSTS) + +This enhancement provides the capability to enforce global automatic HSTS +for TLS routes. + +## Release Signoff Checklist + +- [ ] Enhancement is `implementable` +- [ ] Design details are appropriately documented from clear requirements +- [ ] Test plan is defined +- [ ] Operational readiness criteria is defined +- [ ] Graduation criteria for dev preview, tech preview, GA +- [ ] User-facing documentation is created in [openshift-docs](https://github.com/openshift/openshift-docs/) + +## Summary + +In 3.x and 4.x customers can [provide a per-route annotation to enable HSTS]( +https://docs.openshift.com/container-platform/4.4/networking/routes/route-configuration.html#nw-enabling-hsts_route-configuration). +For customers with many routes or regulatory compliance issues, the manual per-route annotation is seen as sub-optimal. + +This enhancement extends the `Ingress.config.openshift.io` API and adds a new `route` admission controller to the OpenShift API server +which together allow cluster administrators to enforce HSTS globally. This enhancement also provides a recommendation for batch route annotation configuration. + +This enhancement supersedes a previous enhancement documented in +[global-options-enable-hsts.md](https://github.com/openshift/enhancements/blob/master/enhancements/ingress/global-options-enable-hsts.md), +for reasons provided within this proposal. Thanks to David Eads and Miciah Masters for their insights and contributions to the updated proposal. + +## Motivation + +HSTS ([RFC 6797](https://tools.ietf.org/html/rfc6797)) policy enforces the use +of HTTPS in client requests to the host, without having to make use of HTTP redirects. +HSTS provides user protection and is concerned with minimizing security threats based on +network traffic eavesdropping and man-in-the-middle attacks. Using a response header +called `Strict-Transport-Security`, the HTTP response informs clients that a website +can only be accessed via HTTPS. + +Administrators who are tasked with route management and/or regulatory compliance face a +number of issues with regard to enforcing HSTS. For efficiency and protection against +configuration errors, they have requested to automatically and globally enable HSTS +on the basis of the cluster `Ingress` domains. However, because enabling HSTS automatically +and globally can cause disruption of service on a wide scale, they should also be able to +audit and change HSTS configuration without further outages. Finally, they should be able +to consistently apply the same HSTS configuration and predict the outcome on any cluster. + +### Goals + +This proposal allows administrators to: +- Continue to enable HSTS per-route (retain the existing feature functionality) +- Provide HSTS verification per-domain +- Audit HSTS configurations on any cluster +- Predict HSTS enforcement on any cluster with a single configuration manifest + +### Non-Goals + +- HSTS cannot be applied to non-TLS routes, even if HSTS is requested for all routes globally. As +mentioned in [RFC 6797 Section 8.1](https://tools.ietf.org/html/rfc6797#section-8.1), if an HTTP +response is received over insecure transport, clients must ignore any present STS headers. + +## Proposal +Each goal is addressed in the subsections below. + +### Continue to enable HSTS per-route (retain the existing feature functionality) +The `Strict-Transport-Security` response header is currently applied per-route via an +annotation called `haproxy.router.openshift.io/hsts_header`. This annotation has a +required directive `max-age`, and optional directives `includeSubDomains` and `preload`, +with the following definitions: +- `max-age`: (required) delta time range in seconds during which the host is to be regarded as an HSTS host + - If set to 0, it negates the effect and the host is no longer regarded as an HSTS host, including any + of the matching subdomains if `includeSubDomains` is specified + - `max-age` is a time-to-live value, so if a client makes a request for the route's host + and this period of time elapses before the client makes another request for the same host, the + HSTS policy will expire on that client +- `preload`: (optional) if present, tells the client to include the route's host in its host preload list so that + it never needs to do an initial load to get the HSTS header (note that this is not defined in RFC 6797 + and is therefore client browser implementation-dependent) +- `includeSubDomains`: (optional) if present, the HSTS Policy applies to any hosts with subdomains of the host's + domain name. + - E.g., subdomains `bar.foo.com` and `baz.bar.foo.com` both match the superdomain `foo.com`. If the route + with host `foo.com` specified a HSTS Policy with`includeSubDomains`, then the route with host `bar.foo.com` + would inherit the HSTS Policy of `foo.com`. + - If the route admission policy with host `foo.com` allows subdomain wildcards it should use the + `includeSubDomains` directive only if all subdomains of that route should share the same HSTS Policy. There + cannot be a mix of plain HTTP and HTTPS routes covered by the route. + - If the route admission policy with host `foo.com` does not allow subdomain wildcards, it may not use + the `includeSubDomains` directive, because it would unintentionally convey its HSTS Policy to any routes having + `foo.com` as a superdomain. A route configured without allowing subdomain wildcards will be rejected if it + contains `includeSubdomains` in its HSTS Policy. + +HSTS is currently implemented in the HAProxy template and applied to `edge` and +`reencrypt` routes that have the `haproxy.router.openshift.io/hsts_header` annotation: +```gotemplate +{{- /* hsts header in response: */}} + {{- $hstsOptionalTokenPattern := `(?:includeSubDomains|preload)` }} + {{- $hstsPattern := printf `(?:%[1]s[;])*max-age=(?:\d+|"\d+")(?:[;]%[1]s)*` $hstsOptionalTokenPattern -}} +... +{{- if matchValues (print $cfg.TLSTermination) "edge" "reencrypt" }} + {{- with $hsts := firstMatch $hstsPattern (index $cfg.Annotations "haproxy.router.openshift.io/hsts_header") }} + http-response set-header Strict-Transport-Security {{$hsts}} + {{- end }}{{/* hsts header */}} + {{- end }}{{/* is "edge" or "reencrypt" */}} +``` +The HAProxy template will not change for this enhancement. All routes must still be configured with +the HSTS annotation. + +#### Provide HSTS verification per-domain +This proposal allows cluster administrators to configure HSTS verification on a per-domain basis with +the addition of a new openshift/api-server validating admission plugin for the router, called +`route.openshift.io/RequiredRouteAnnotations`. Each route must be configured with a required route +annotation, which will be verified against the global setting on the cluster `Ingress` configuration. + +The administrator will interact with the `RequiredRouteAnnotations` plugin by configuring the +`Ingress.Spec.RequiredHSTSPolicies` with one or more `RequiredHSTSPolicy`. `RequiredHSTSPolicy` is +a new type that will be added to the API `openshift/api/config/v1/types_ingress.go`, to capture the +configuration of the required HSTS Policy. With `RequiredHSTSPolicy`, administrators can configure namespace +selector filter and/or domains to use for matching routes. They can also configure the HSTS maximum age, +preload policy, and whether the HSTS policy should include subdomains of the configured route's host. + +````go +type RequiredHSTSPolicy struct { + // NamespaceSelector filters only those objects that pass the key-value selection conditions + // Defaults to the empty LabelSelector, which matches everything. + // +optional + NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector,omitempty" protobuf:"bytes,5,opt,name=namespaceSelector"` + + // domainPatterns is an optional list of domains for which these annotations are required. + // If domainPatterns is specified and a route is created with a spec.host matching one of the domains, + // the route must specify the annotations specified in requiredAnnotations. If domainPatterns is empty, + // the specified annotations are IGNORED + // + // The use of wildcards is allowed like this: *.foo.com matches everything under foo.com. + // foo.com only matches foo.com, so to cover foo.com and everything under it, you must specify *both* + // +optional + DomainPatterns []string + + // maxAge is the delta time range in seconds during which hosts are regarded as HSTS hosts. + // If set to 0, it negates the effect and hosts are no longer regarded as HSTS hosts. + // maxAge is a time-to-live value, and if this policy is not refreshed on a client, the HSTS + // policy will eventually expire on that client. + // +required + MaxAge MaxAgePolicy + + // preloadPolicy directs the client to include hosts in its host preload list so that + // it never needs to do an initial load to get the HSTS header (note that this is not defined + // in RFC 6797 and is therefore client implementation-dependent). + // +optional + PreloadPolicy PreloadPolicy + + // includeSubDomainsPolicy means the HSTS Policy applies to any subdomains of the host's + // domain name, when the route admission policy allows wildcards. If wildcard routes aren't allowed, + // this directive has no effect. + // +optional + IncludeSubdomainsPolicy IncludeSubdomainsPolicy +} + +type MaxAgePolicy struct { + // zero means no opinion + // omitempty + LargestMaxAge int32 + // zero means no opinion + // omitempty + SmallestMaxAge int32 +} + +type PreloadPolicy string + +var ( + RequirePreloadPolicy PreloadPolicy = "RequirePreload" + RequireNoPreloadPolicy PreloadPolicy = "RequireNotPreload" + NoOpinionPreloadPolicy PreloadPolicy = "NoOpinion" + DefaultPreloadPolicy PreloadPolicy = "" +) + +type IncludeSubdomainsPolicy string + +var ( + RequireIncludeSubdomains IncludeSubdomainsPolicy = "RequireIncludeSubdomains" + RequireNotIncludeSubdomains IncludeSubdomainsPolicy = "RequireNotIncludeSubdomains" + NoOpinionIncludeSubdomains IncludeSubdomainsPolicy = "NoOpinion" + DefaultIncludeSubdomains IncludeSubdomainsPolicy = "" +) +```` +A new type `RequiredHSTSPolicies` will be added to `IngressSpec` to contain any configured required HSTS Policies: +```go +type IngressSpec struct { + ... + // RequiredHSTSPolicies specifies HSTS policies that are required to be set on + // newly created routes matching some criteria. + // If a route specifies a HSTS annotation that matches one of the RequiredHSTSPolicies, + // the route is admitted. If the route doesn't match a RequiredHSTSPolicy, the route + // is rejected with a message indicating the suggested policy value. + // If there are no RequiredHSTSPolicies, any route annotation will be admitted. + // +optional + RequiredHSTSPolicies []RequiredHSTSPolicy `json:"requiredHSTSPolicies,omitempty"` +} + +``` +The `route.openshift.io/RequiredRouteAnnotations` route validating admission plugin would follow the current OpenShift +API server design pattern for admission plugins and additionally validate that routes matching +`RequiredHSTSPolicy` `NamespaceSelector` and/or +`DomainPatterns`, are configured with the required HSTS policy. If there are no `RequiredHSTSPolicies`, any route +annotation will be valid. + +Some code is listed here, not all the trivial helper functions defined. +````go +... +type requiredRouteAnnotations struct { + *admission.Handler + routeClient routetypedclient.RoutesGetter + configClient configtypedclient.IngressesGetter + nsLister corev1listers.NamespaceLister + nsListerSynced func() bool +} + +// Ensure that the required OpenShift admission interfaces are implemented. +var _ = initializer.WantsExternalKubeInformerFactory(&requiredRouteAnnotations{}) +var _ = admissionrestconfig.WantsRESTClientConfig(&requiredRouteAnnotations{}) +var _ = admission.ValidationInterface(&requiredRouteAnnotations{}) + +const hstsAnnotation = "haproxy.router.openshift.io/hsts_header" + +// Validate ensures that routes specify required annotations, and returns nil if valid. +func (o *requiredRouteAnnotations) Validate(ctx context.Context, a admission.Attributes, _ admission.ObjectInterfaces) (err error) { + ... + if o.getRequiredHSTS(ctx) == nil { + return nil + } + newRoute := a.GetObject().(*routeapi.Route) + ... + ingress, err = o.configClient.Ingresses().Get(ctx, "cluster", metav1.GetOptions{}) + if err != nil { + return admission.NewForbidden(a, err) + } + namespace, err := o.nsLister.Get(newRoute.Namespace) + if err != nil { + return admission.NewForbidden(a, err) + } + + if err := isRouteHSTSAllowed(ingress, newRoute, namespace); err != nil { + return admission.NewForbidden(a, err) + } + return nil +} + +// isRouteHSTSAllowed returns nil if the route is allowed. Otherwise, returns details and a suggestion in the error +func isRouteHSTSAllowed(ingress *v1.Ingress, newRoute *routeapi.Route, namespace *corev1.Namespace) error { + requirements := ingress.Spec.RequiredHSTSPolicies + for _, requirement := range requirements { + if matches, err := requiredHSTSMatchesRoute(requirement, newRoute, namespace); err != nil { + return err + } else if !matches { + continue + } + + routeHSTS, err := hstsConfigFromRoute(newRoute) + if err != nil{ + return err + } + + requirementErr := routeHSTS.meetsRequirements(requirement) + if requirementErr != nil { + return requirementErr + } + // Validation only checks the first matching required HSTS rule. + return nil + } + + // None of the requirements matched this route's domain/namespace, HSTS is not required for it + return nil +} + +type hstsConfig struct { + maxAge int32 + preload bool + includeSubdomains bool +} + +// Parse out the hstsConfig fields from the annotation +func hstsConfigFromRoute(route *routeapi.Route) (hstsConfig, error) { + ret := hstsConfig{} + tokens := strings.Split(route.Annotations[hstsAnnotation], ";") + for _, token := range tokens{ + trimmed := strings.Trim(token, " ") + // TODO - add case insensitivity + if trimmed == "includeSubDomains"{ + ret.includeSubdomains = true + } + if trimmed == "preload"{ + ret.preload = true + } + if strings.HasPrefix(trimmed, "max-age="){ + age, err := strconv.ParseInt(trimmed[len("max-age="):], 10, 32) + if err != nil{ + return hstsConfig{}, err + } + ret.maxAge = age + } else { + return hstsConfig{}, fmt.Errorf("max-age is required in the HSTS Policy") + } + } + if ret != nil { + // Invalid if a HSTS Policy is specified but this route is not TLS + termination := route.Spec.TLS.Termination + if (termination != routev1.TLSTerminationEdge) || (termination != routev1.TLSTerminationReencrypt) { + return nil, fmt.Errorf("HSTS Policy not accepted for termination type: %s", termination) + } + // Invalid if includeSubDomains is indicated in the HSTS Policy, but subdomain wildcards are not allowed + if ret.includeSubdomains && route.Spec.WildcardPolicy != routev1.WildcardPolicySubdomain { + return nil, fmt.Errorf("Route must allow wildcard subdomains if HSTS has includeSubDomains") + } + } + return ret, nil +} + +// Make sure the given requirement meets the configured HSTS policy, validating: +// - range for maxAge +// - preloadPolicy +// - includeSubdomainsPolicy +func (c hstsConfig) meetsRequirements(requirement RequiredHSTSPolicy) error { + configHasMaxAge := c.maxAge != 0 + requirementHasMaxAge := requirement.MaxAge.LargestMaxAge != 0 || requirement.MaxAge.SmallestMaxAge != 0 + if requirementHasMaxAge && !configHasMaxAge { + return fmt.Errorf("max age is required") + } + if requirement.MaxAge.LargestMaxAge != 0 && c.maxAge > requirement.MaxAge.LargestMaxAge { + return fmt.Errorf("does not match max age") + } + if requirement.MaxAge.SmallestMaxAge != 0 && c.maxAge < requirement.MaxAge.SmallestMaxAge { + return fmt.Errorf("does not match minimum age") + } + + switch requirement.PreloadPolicy { + case DefaultPreloadPolicy, NoOpinionPreloadPolicy: + // anything is allowed, do nothing + case RequirePreloadPolicy: + if !c.preload { + return fmt.Errorf("preload must be true") + } + case RequireNoPreloadPolicy: + if c.preload { + return fmt.Errorf("preload must be unspecified") + } + } + + switch requirement.IncludeSubdomainsPolicy { + case DefaultIncludeSubdomains, NoOpinionIncludeSubdomains: + // anything is allowed, do nothing + case RequireIncludeSubdomains: + if !c.includeSubdomains { + return fmt.Errorf("includeSubdomains must be true") + } + case RequireNotIncludeSubdomains: + if c.includeSubdomains { + return fmt.Errorf("includeSubdomains must be unspecified") + } + } + + return nil +} +// Check if the route matches the required domain/namespace in the HSTS Policy +func requiredHSTSMatchesRoute(requirement RequiredHSTSPolicy, route *routeapi.Route, namespace *corev1.Namespace) (bool, error) { + matchesNamespace, err := matchesNamespaceSelector(requirement.NamespaceSelector, namespace) + if err != nil { + return false, err + } + if !matchesNamespace { + return false, nil + } + routeDomains := []string{route.Spec.Host} + for _, ingress := range route.Status.Ingress { + routeDomains = append(routeDomains, ingress.Host) + } + if matchesDomain(requirement.DomainPatterns, routeDomains) { + return true, nil + } + + return false, nil +} +```` + +Formatting of the HSTS Annotation field on a `route`: +```yaml +max-age=[;includeSubDomains][;preload]` +``` + +The `RequiredHSTSPolicies` variable can be configured by the administrator in the `Ingress`, as shown in this example, where +there is one HSTS Policy is for domain abc.com, expires after one year, includes subdomains, and requests preload: +```yaml +apiVersion: config.openshift.io/v1 +kind: Ingress +metadata: + name: cluster +spec: + domain: apps.abc.com + requiredHSTSPolicies: + - domainPatterns: + - abc.com + maxAge: + smallestMaxAge: 1 + largestMaxAge: 31536000 + preloadPolicy: "RequirePreload" + includeSubdomainsPolicy: "RequireIncludeSubdomains" +``` + +#### Route Administration + +To handle upgraded clusters with non-compliant HSTS routes, the best solution is to update +the manifests at the source and apply the updates. However, in upgrade clusters, administrators +can update the routes directly via the API. + +To apply HSTS to all routes in the cluster, use a command like this. (This command requires +an updated API that supports the `--all-namespaces` flag on the `oc annotate` verb): +```shell +$ oc annotate route --all --all-namespaces --overwrite=true "haproxy.router.openshift.io/hsts_header"="max-age=31536000" +``` +To apply HSTS to all routes in a particular namespace, use a command like this: +```shell +$ oc annotate route --all -n my-namespace --overwrite=true "haproxy.router.openshift.io/hsts_header"="max-age=31536000" +``` +To remove HSTS from all routes in a particular namespace, set the `max-age` to 0, using a command like this: +```shell +$ oc annotate route --all -n my-namespace --overwrite=true "haproxy.router.openshift.io/hsts_header"="max-age=0" +``` + +To apply more selective policy to running resources, cluster administrators can use this simple +command-line script, `batch_annotate.sh` run with an input file of routes: +````shell +#!/bin/bash + +# Usage: ./batch_annotate.sh filename.txt +# Where filename.txt lines are formatted: route_name annotation_key annotaion_value namespace overwrite +# For example: console haproxy.router.openshift.io/hsts_header max-age=600 openshift-console true + +CR=$'\n' +# Annotate routes with a given route name (but will not overwrite existing) +function annotate_route () { + local route_name=${1} + local annotation_key=${2} + local annotation_value=${3} + local namespace=${4} + local overwrite=${5} + echo "${CR}[cmd]: oc annotate route --overwrite=${overwrite} ${route_name} ${annotation_key}=${annotation_value} -n ${namespace}" + oc annotate route ${route_name} --overwrite=${overwrite} ${annotation_key}=${annotation_value} -n ${namespace} + return $? +} + +function check_result () { + local r=${1} + if [[ $r -eq 0 ]]; then + echo "Success" + else + echo "Failure: error code $r" + fi +} + +if [[ -z ${1} ]]; then + echo "Usage: must enter filename" + exit 1 +else + echo "Reading from ${1}..." +fi + +while IFS= read -r line; do + IFS=' ' read -r -a fields <<< "$line" + annotate_route ${fields[0]} ${fields[1]} ${fields[2]} ${fields[3]} ${fields[4]} + result=$? + check_result $result +done < "${1}" + +echo "Done" +```` +Input file example (filename.txt) for the script: +```text +console haproxy.router.openshift.io/hsts_header max-age=900 openshift-console true +myBigRoute haproxy.router.openshift.io/hsts_header max-age=31536000;preload;includeSubdomains openshift-console true +``` +Usage example: +```shell +$ ./batch_annotate.sh filename.txt +```` + +#### Testing +Accompanying unit and e2e test changes will be added to exercise the `RequiredHSTSPolicies` type. The API guarantees: +* Routes with HSTS without TLS Termination of edge or re-encrypt will not be validated +* Routes with HSTS includeSubDomain without wildcard policy subdomains allowed will not be validated +* Routes with hosts that match the `domainPatterns` of the HSTS will be validated +* Routes with hosts that match the `domainPatterns` and `namespaceSelector` of the HSTS will be validated +* Routes that only match the `namespaceSelector` will not be validated +* Routes that are validated will validate that the `maxAge` exists and falls within the range of the HSTS `maxAgePolicy` +* Routes that are validated will validate that the `preloadPolicy` and `includeSubdomainPolicy` match the HSTS Policy if they exist + +#### Audit HSTS configurations on any cluster +This proposal offers the cluster administrator a way to review the HSTS configurations guaranteed by the admission plugin. +For example, to review the maxAge set for required HSTS Policies: +```shell +$ oc get clusteroperator/ingress -n openshift-ingress-operator -o jsonpath='{range .spec.requiredHSTSPolicies}{.spec.requiredHSTSPolicies.maxAgePolicy.largestMaxAge}{"\n"}{end}' +``` +To review the HSTS annotations on all routes: +````shell +$ oc get route --all-namespaces -o go-template='{{range .items}}{{if .metadata.annotations}}{{$a := index .metadata.annotations "haproxy.router.openshift.io/hsts_header"}}{{$n := .metadata.name}}{{with $a}}Name: {{$n}} HSTS: {{$a}}{{"\n"}}{{else}}{{""}}{{end}}{{end}}{{end}}' + +Name: myBigRoute HSTS: max-age=31536000;preload;includeSubdomains +```` +#### Predict HSTS enforcement on any cluster with a single configuration manifest +This proposal offers the cluster administrator a way to configure and enforce HSTS configurations on routes by configuring +the `Ingress` using templates that substitute only the for the difference between clusters. For example, for +a development cluster with domain `dev.abc.com` and production cluster with `prod.abc.com` domains, administrators can use a single +`Ingress` configuration template that substitutes the domain: + +```yaml +apiVersion: config.openshift.io/v1 +kind: Ingress +metadata: + name: cluster +spec: + domain: abc.com + requiredHSTSPolicies: + - domainPatterns: + - ${DOMAIN} + maxAge: + smallestMaxAge: 1 + largestMaxAge: 31536000 + preloadPolicy: "RequirePreload" + includeSubdomainsPolicy: "RequireIncludeSubdomains" +``` +All routes that require HSTS will then use the same required Annotation on either cluster: +````yaml +apiVersion: v1 +kind: Route +metadata: + annotations: + haproxy.router.openshift.io/hsts_header: max-age=31536000;preload;includeSubdomains +... +spec: + host: def.abc.com + tls: + termination: "reencrypt" + ... + wildcardPolicy: "Subdomain" +```` +After applying the annotation, a route can be checked for the proper header like this: +```shell +$ curl -sik https://foo.com | grep strict-transport-security +strict-transport-security: max-age=31536000 +```` + +### User Stories + +#### As a cluster administrator, I want to verify HSTS globally, for all TLS routes in domain `foo.com` +Update the `Ingress` configuration spec like the example below: +```yaml +spec: + domain: abc.com + requiredHSTSPolicies: + - domainPatterns: + - *.foo.com + - foo.com + maxAge: + smallestMaxAge: 1 + largestMaxAge: 31536000 +``` +Also update the route for domain `foo.com` with the matching annotation: +````yaml +apiVersion: v1 +kind: Route +metadata: + annotations: + haproxy.router.openshift.io/hsts_header: max-age=31536000 +```` +To audit the routes that match host `foo.com`, use a command like this: +```shell +oc get route --all-namespaces -o go-template='{{range .items}}{{if .metadata.annotations}} +{{$a := index .metadata.annotations "haproxy.router.openshift.io/hsts_header"}}{{$h := .spec.host}}{{if $a}}{{if $h}} +{{if eq $h "foo.com" }}Host: {{ $h}} HSTS: {{$a}} +{{"\n"}}{{end}}{{end}}{{end}}{{end}}{{end}}' +Host: foo.com HSTS: max-age=31536000 +``` +#### As a cluster administrator, I want to verify HSTS for only the domain `kibana.foo.com` +Update the `Ingress` configuration spec like the example below: +```yaml +spec: + domain: abc.com + requiredHSTSPolicies: + - domainPatterns: + - kibana.foo.com + maxAge: + smallestMaxAge: 1 + largestMaxAge: 31536000 +``` +Also update the route for domain `kibana.foo.com` with the matching annotation: +````yaml +apiVersion: v1 +kind: Route +metadata: + annotations: + haproxy.router.openshift.io/hsts_header: max-age=31536000 +```` +### Implementation Details/Notes/Constraints +Implementing this enhancement requires changes in the following repositories: +- openshift/api/config/v1 +- openshift/api-server/pkg/route/apiserver/admission +- openshift/route + +### Risks and Mitigations +As previously mentioned, use of the `includeSubDomains` directive may cause problems unless the user +is aware of its encompassing implications. As described in + [RFC 6797 Section 11.4](https://tools.ietf.org/html/rfc6797#section-11.4), it is possible for at least two + complex problems to arise. +- it is possible for a HSTS host to offer unsecured services on alternate ports or different subdomains, if the + HSTS Policy is set to `includeSubDomains` but there are non-matching or non-existent HSTS Policies required + for those different subdomains +- if different web applications are offered on different subdomains of a HSTS host with `includeSubDomains`, + there may be different policies stored by the user for different web applications. E.g. if the same + superdomain is used by different routes, and the HSTS Policy is not the same it will alternate between + various policies for the same subdomain in one browser + +To mitigate issues, it is recommended to refrain from using `includeSubDomains` in the case that HTTPS and HTTP +route share the same domain, or to ensure HTTP based services are also offered via HTTPS for the same subdomain. +In the case that different web applications are offered on different subdomains of a HSTS host, and some use HTTP +while others use HTTPS, each domain should be configured separately instead of using `includeSubDomains` on a superdomain. +This will be incorporated into validation code that enforces the use of a Wildcard Policy for subdomains for each +route that attempts to use `includeSubDomains` in its HSTS Policy. If a route does not have a Wildcard Policy of +subdomain, it will not be admitted if it tries to use a HSTS Policy with `includeSubDomains`. + +Additionally, the current implementation has a `preload` directive for the `Strict-Transport-Security` +header. This is not an RFC 6797 directive and therefore its implementation may vary by +user agent. It can be implemented, but support may vary by user agent implementation and therefore no specifications +are made here. + +## Design Details +[TBD] +## Drawbacks +N/A +## Alternatives +N/A + +### Open Questions +Version Skew Strategy - not clear to me if that is required here. + +### Test Plan +HAProxy and Router have unit test coverage; for this enhancement, the unit tests are +expanded to cover the additional goals. + +The operator has end-to-end tests; for this enhancement, add the following tests: + +#### Enable global HSTS and validate a route +1. Create an Ingress config that enables global HSTS for a domain +2. Create a Route that annotates for HSTS in this domain and verify that it is admitted +3. Open a connection to this route using the domain and send a request +4. Verify that a response is received and that the headers include HSTS as configured in step 1 + +#### Enable global HSTS and invalidate a route +1. Create an Ingress config that enables global HSTS for a domain +2. Create a Route that does NOT annotate for HSTS in this domain and verify that it is NOT admitted + +#### Audit HSTS configurations +1. Create an Ingress config that enables global HSTS for a domain +2. Query the API server for the new HSTS configuration and verify that it is there + +### Graduation Criteria +N/A + +### Upgrade / Downgrade Strategy + +On upgrade, any previous per-route HSTS configuration remains in effect ONLY for domains that are not +otherwise configured for HSTS. Because per-route HSTS annotations were never validated +in the past, it is possible for a previously-admitted route to no longer be admitted if it falls under +a governed domain but doesn't match the new HSTS Policy for that domain. The recommendation will be for +administrators to take a survey of route annotations prior to applying a new HSTS Policy, then inspect +and correct routes in the domain/s of the new HSTS Policy. + +If a cluster administrator applied any of the new HSTS configuration options +in 4.9, then downgraded to 4.8, the HSTS configuration settings would no longer be validated +as a part of admission control. The administrator would be responsible +for removing any per-route HSTS configurations that were no longer applicable. + +From a user perspective, after a downgrade an end-user's browser +may continue to access the configured routes via the previously configured HSTS Policy, +until the HSTS Policy expires. + +### Version Skew Strategy +N/A +## Implementation History +N/A +### Placeholder for lint +N/A +#### Dev Preview -> Tech Preview +N/A +#### Tech Preview -> QA +N/A +#### Tech Preview -> GA +N/A +#### Removing a deprecated feature +N/A diff --git a/enhancements/ingress/global-options-enable-hsts.md b/enhancements/ingress/global-options-enable-hsts.md index 46b93fbd5f3..e0dfc1d6644 100644 --- a/enhancements/ingress/global-options-enable-hsts.md +++ b/enhancements/ingress/global-options-enable-hsts.md @@ -29,6 +29,7 @@ see-also: replaces: superseded-by: + - enhancements/enhancements/ingress/global-admission-hsts.md --- @@ -414,6 +415,10 @@ because it may not be clear to the user that a global setting cannot override a Otherwise, it would not be possible to use a per-route setting. ## Design Details +## Drawbacks +N/A +## Alternatives +N/A ### Open Questions Version Skew Strategy - not clear to me if that is required here. @@ -463,3 +468,13 @@ N/A (TBD) ## Implementation History N/A +### Placeholder for lint +N/A +#### Dev Preview -> Tech Preview +N/A +#### Tech Preview -> QA +N/A +#### Tech Preview -> GA +N/A +#### Removing a deprecated feature +N/A