diff --git a/internal/ingress/annotations/annotations.go b/internal/ingress/annotations/annotations.go index 302f0b4b1..88c248078 100644 --- a/internal/ingress/annotations/annotations.go +++ b/internal/ingress/annotations/annotations.go @@ -20,6 +20,7 @@ import ( "dario.cat/mergo" "k8s.io/ingress-nginx/internal/ingress/annotations/canary" + "k8s.io/ingress-nginx/internal/ingress/annotations/customheaders" "k8s.io/ingress-nginx/internal/ingress/annotations/disableproxyintercepterrors" "k8s.io/ingress-nginx/internal/ingress/annotations/modsecurity" "k8s.io/ingress-nginx/internal/ingress/annotations/opentelemetry" @@ -82,6 +83,7 @@ type Ingress struct { Canary canary.Config CertificateAuth authtls.Config ClientBodyBufferSize string + CustomHeaders customheaders.Config ConfigurationSnippet string Connection connection.Config CorsConfig cors.Config @@ -133,6 +135,7 @@ func NewAnnotationExtractor(cfg resolver.Resolver) Extractor { "Canary": canary.NewParser(cfg), "CertificateAuth": authtls.NewParser(cfg), "ClientBodyBufferSize": clientbodybuffersize.NewParser(cfg), + "ClientHeaders": customheaders.NewParser(cfg), "ConfigurationSnippet": snippet.NewParser(cfg), "Connection": connection.NewParser(cfg), "CorsConfig": cors.NewParser(cfg), diff --git a/internal/ingress/annotations/customheaders/main.go b/internal/ingress/annotations/customheaders/main.go new file mode 100644 index 000000000..08efbd6aa --- /dev/null +++ b/internal/ingress/annotations/customheaders/main.go @@ -0,0 +1,83 @@ +/* +Copyright 2015 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 customheaders + +import ( + "fmt" + "regexp" + + "k8s.io/klog/v2" + + networking "k8s.io/api/networking/v1" + + "k8s.io/ingress-nginx/internal/ingress/annotations/parser" + ing_errors "k8s.io/ingress-nginx/internal/ingress/errors" + "k8s.io/ingress-nginx/internal/ingress/resolver" +) + +// Config returns external authentication configuration for an Ingress rule +type Config struct { + Headers map[string]string `json:"headers,omitempty"` +} + +var ( + headerRegexp = regexp.MustCompile(`^[a-zA-Z\d\-_]+$`) +) + +// ValidHeader checks is the provided string satisfies the header's name regex +func ValidHeader(header string) bool { + return headerRegexp.Match([]byte(header)) +} + +type customHeaders struct { + r resolver.Resolver +} + +// NewParser creates a new authentication request annotation parser +func NewParser(r resolver.Resolver) parser.IngressAnnotation { + return customHeaders{r} +} + +// ParseAnnotations parses the annotations contained in the ingress +// rule used to use an Config URL as source for authentication +func (a customHeaders) Parse(ing *networking.Ingress) (interface{}, error) { + clientHeadersConfigMapName, err := parser.GetStringAnnotation("custom-headers", ing) + if err != nil { + klog.V(3).InfoS("client-headers annotation is undefined and will not be set") + } + + var headers map[string]string + + if clientHeadersConfigMapName != "" { + clientHeadersMapContents, err := a.r.GetConfigMap(clientHeadersConfigMapName) + if err != nil { + return nil, ing_errors.NewLocationDenied(fmt.Sprintf("unable to find configMap %q", clientHeadersConfigMapName)) + } + + for header := range clientHeadersMapContents.Data { + if !ValidHeader(header) { + return nil, ing_errors.NewLocationDenied("invalid client-headers in configmap") + } + } + + headers = clientHeadersMapContents.Data + } + + return &Config{ + Headers: headers, + }, nil +} diff --git a/internal/ingress/controller/controller.go b/internal/ingress/controller/controller.go index a43c143e3..2e782f485 100644 --- a/internal/ingress/controller/controller.go +++ b/internal/ingress/controller/controller.go @@ -1504,6 +1504,7 @@ func (n *NGINXController) createServers(data []*ingress.Ingress, func locationApplyAnnotations(loc *ingress.Location, anns *annotations.Ingress) { loc.BasicDigestAuth = anns.BasicDigestAuth loc.ClientBodyBufferSize = anns.ClientBodyBufferSize + loc.CustomHeaders = anns.CustomHeaders loc.ConfigurationSnippet = anns.ConfigurationSnippet loc.CorsConfig = anns.CorsConfig loc.ExternalAuth = anns.ExternalAuth diff --git a/pkg/apis/ingress/types.go b/pkg/apis/ingress/types.go index 2ad17ec3d..269814d34 100644 --- a/pkg/apis/ingress/types.go +++ b/pkg/apis/ingress/types.go @@ -27,6 +27,7 @@ import ( "k8s.io/ingress-nginx/internal/ingress/annotations/authtls" "k8s.io/ingress-nginx/internal/ingress/annotations/connection" "k8s.io/ingress-nginx/internal/ingress/annotations/cors" + "k8s.io/ingress-nginx/internal/ingress/annotations/customheaders" "k8s.io/ingress-nginx/internal/ingress/annotations/fastcgi" "k8s.io/ingress-nginx/internal/ingress/annotations/globalratelimit" "k8s.io/ingress-nginx/internal/ingress/annotations/ipallowlist" @@ -263,7 +264,8 @@ type Location struct { BasicDigestAuth auth.Config `json:"basicDigestAuth,omitempty"` // Denied returns an error when this location cannot not be allowed // Requesting a denied location should return HTTP code 403. - Denied *string `json:"denied,omitempty"` + Denied *string `json:"denied,omitempty"` + CustomHeaders customheaders.Config `json:"customHeaders,omitempty"` // CorsConfig returns the Cors Configuration for the ingress rule // +optional CorsConfig cors.Config `json:"corsConfig,omitempty"` diff --git a/rootfs/etc/nginx/template/nginx.tmpl b/rootfs/etc/nginx/template/nginx.tmpl index b3b3f8683..e87c836d3 100644 --- a/rootfs/etc/nginx/template/nginx.tmpl +++ b/rootfs/etc/nginx/template/nginx.tmpl @@ -1489,6 +1489,13 @@ stream { {{ $all.Cfg.LocationSnippet }} {{ end }} + {{ if $location.CustomHeaders }} + # Custom Response Headers + {{ range $k, $v := $location.CustomHeaders.Headers }} + more_set_headers {{ printf "%s: %s" $k $v | quote }}; + {{ end }} + {{ end }} + {{/* if we are sending the request to a custom default backend, we add the required headers */}} {{ if (hasPrefix $location.Backend "custom-default-backend-") }} proxy_set_header X-Code 503;