diff --git a/staging/src/k8s.io/apiserver/pkg/server/config.go b/staging/src/k8s.io/apiserver/pkg/server/config.go index ae2f91cd225..92b0fd30a0b 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/config.go +++ b/staging/src/k8s.io/apiserver/pkg/server/config.go @@ -112,7 +112,7 @@ type Config struct { // to set values and determine whether its allowed AdmissionControl admission.Interface CorsAllowedOriginList []string - + HSTSDirectives []string // FlowControl, if not nil, gives priority and fairness to request handling FlowControl utilflowcontrol.Interface @@ -755,6 +755,7 @@ func DefaultBuildHandlerChain(apiHandler http.Handler, c *Config) http.Handler { handler = genericapifilters.WithAuditAnnotations(handler, c.AuditBackend, c.AuditPolicyChecker) handler = genericapifilters.WithWarningRecorder(handler) handler = genericapifilters.WithCacheControl(handler) + handler = genericfilters.WithHSTS(handler, c.HSTSDirectives) handler = genericapifilters.WithRequestReceivedTimestamp(handler) handler = genericfilters.WithPanicRecovery(handler, c.RequestInfoResolver) return handler diff --git a/staging/src/k8s.io/apiserver/pkg/server/filters/BUILD b/staging/src/k8s.io/apiserver/pkg/server/filters/BUILD index 5d5cefaf63a..05bf7342795 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/filters/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/server/filters/BUILD @@ -54,6 +54,7 @@ go_library( "cors.go", "doc.go", "goaway.go", + "hsts.go", "longrunning.go", "maxinflight.go", "priority-and-fairness.go", diff --git a/staging/src/k8s.io/apiserver/pkg/server/filters/hsts.go b/staging/src/k8s.io/apiserver/pkg/server/filters/hsts.go new file mode 100644 index 00000000000..46625381fa0 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/server/filters/hsts.go @@ -0,0 +1,40 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package filters + +import ( + "net/http" + "strings" +) + +// WithHSTS is a simple HSTS implementation that wraps an http Handler. +// If hstsDirectives is empty or nil, no HSTS support is installed. +func WithHSTS(handler http.Handler, hstsDirectives []string) http.Handler { + if len(hstsDirectives) == 0 { + return handler + } + allDirectives := strings.Join(hstsDirectives, "; ") + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + // Chrome and Mozilla Firefox maintain an HSTS preload list + // issue : golang.org/issue/26162 + // Set the Strict-Transport-Security header if it is not already set + if _, ok := w.Header()["Strict-Transport-Security"]; !ok { + w.Header().Set("Strict-Transport-Security", allDirectives) + } + handler.ServeHTTP(w, req) + }) +} diff --git a/staging/src/k8s.io/apiserver/pkg/server/options/server_run_options.go b/staging/src/k8s.io/apiserver/pkg/server/options/server_run_options.go index 1ee7060428a..b9080fa81d7 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/options/server_run_options.go +++ b/staging/src/k8s.io/apiserver/pkg/server/options/server_run_options.go @@ -19,10 +19,12 @@ package options import ( "fmt" "net" + "strings" "time" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apimachinery/pkg/util/errors" "k8s.io/apiserver/pkg/server" utilfeature "k8s.io/apiserver/pkg/util/feature" @@ -34,6 +36,7 @@ type ServerRunOptions struct { AdvertiseAddress net.IP CorsAllowedOriginList []string + HSTSDirectives []string ExternalHost string MaxRequestsInFlight int MaxMutatingRequestsInFlight int @@ -71,6 +74,7 @@ func NewServerRunOptions() *ServerRunOptions { // ApplyTo applies the run options to the method receiver and returns self func (s *ServerRunOptions) ApplyTo(c *server.Config) error { c.CorsAllowedOriginList = s.CorsAllowedOriginList + c.HSTSDirectives = s.HSTSDirectives c.ExternalAddress = s.ExternalHost c.MaxRequestsInFlight = s.MaxRequestsInFlight c.MaxMutatingRequestsInFlight = s.MaxMutatingRequestsInFlight @@ -143,9 +147,31 @@ func (s *ServerRunOptions) Validate() []error { errors = append(errors, fmt.Errorf("--max-resource-write-bytes can not be negative value")) } + if err := validateHSTSDirectives(s.HSTSDirectives); err != nil { + errors = append(errors, err) + } return errors } +func validateHSTSDirectives(hstsDirectives []string) error { + // HSTS Headers format: Strict-Transport-Security:max-age=expireTime [;includeSubDomains] [;preload] + // See https://tools.ietf.org/html/rfc6797#section-6.1 for more information + allErrors := []error{} + for _, hstsDirective := range hstsDirectives { + if len(strings.TrimSpace(hstsDirective)) == 0 { + allErrors = append(allErrors, fmt.Errorf("empty value in strict-transport-security-directives")) + continue + } + if hstsDirective != "includeSubDomains" && hstsDirective != "preload" { + maxAgeDirective := strings.Split(hstsDirective, "=") + if len(maxAgeDirective) != 2 || maxAgeDirective[0] != "max-age" { + allErrors = append(allErrors, fmt.Errorf("--strict-transport-security-directives invalid, allowed values: max-age=expireTime, includeSubDomains, preload. see https://tools.ietf.org/html/rfc6797#section-6.1 for more information")) + } + } + } + return errors.NewAggregate(allErrors) +} + // AddUniversalFlags adds flags for a specific APIServer to the specified FlagSet func (s *ServerRunOptions) AddUniversalFlags(fs *pflag.FlagSet) { // Note: the weird ""+ in below lines seems to be the only way to get gofmt to @@ -161,6 +187,10 @@ func (s *ServerRunOptions) AddUniversalFlags(fs *pflag.FlagSet) { "List of allowed origins for CORS, comma separated. An allowed origin can be a regular "+ "expression to support subdomain matching. If this list is empty CORS will not be enabled.") + fs.StringSliceVar(&s.HSTSDirectives, "strict-transport-security-directives", s.HSTSDirectives, ""+ + "List of directives for HSTS, comma separated. If this list is empty, then HSTS directives will not "+ + "be added. Example: 'max-age=31536000,includeSubDomains,preload'") + deprecatedTargetRAMMB := 0 fs.IntVar(&deprecatedTargetRAMMB, "target-ram-mb", deprecatedTargetRAMMB, "DEPRECATED: Memory limit for apiserver in MB (used to configure sizes of caches, etc.)") diff --git a/staging/src/k8s.io/apiserver/pkg/server/options/server_run_options_test.go b/staging/src/k8s.io/apiserver/pkg/server/options/server_run_options_test.go index 9f456318bbf..871af5f909e 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/options/server_run_options_test.go +++ b/staging/src/k8s.io/apiserver/pkg/server/options/server_run_options_test.go @@ -145,11 +145,27 @@ func TestServerRunOptionsValidate(t *testing.T) { }, expectErr: "--shutdown-delay-duration can not be negative value", }, + { + name: "Test when HSTSHeaders is valid", + testOptions: &ServerRunOptions{ + AdvertiseAddress: net.ParseIP("192.168.10.10"), + CorsAllowedOriginList: []string{"10.10.10.100", "10.10.10.200"}, + HSTSDirectives: []string{"fakevalue", "includeSubDomains", "preload"}, + MaxRequestsInFlight: 400, + MaxMutatingRequestsInFlight: 200, + RequestTimeout: time.Duration(2) * time.Minute, + MinRequestTimeout: 1800, + JSONPatchMaxCopyBytes: 10 * 1024 * 1024, + MaxRequestBodyBytes: 10 * 1024 * 1024, + }, + expectErr: "--strict-transport-security-directives invalid, allowed values: max-age=expireTime, includeSubDomains, preload. see https://tools.ietf.org/html/rfc6797#section-6.1 for more information", + }, { name: "Test when ServerRunOptions is valid", testOptions: &ServerRunOptions{ AdvertiseAddress: net.ParseIP("192.168.10.10"), CorsAllowedOriginList: []string{"10.10.10.100", "10.10.10.200"}, + HSTSDirectives: []string{"max-age=31536000", "includeSubDomains", "preload"}, MaxRequestsInFlight: 400, MaxMutatingRequestsInFlight: 200, RequestTimeout: time.Duration(2) * time.Minute,