Kubernetes Security – Secure-by-default Headers with Envoy and Istio
When organizations move to a new platform such as Kubernetes to build their applications upon, a lot of things have to be reconsidered. Security, access control and monitoring are just a few examples. Envoy and Istio bring a lot to the table when it comes to solving these challenges in a Kubernetes environment. Even Google’s envisioned Knative PaaS builds its foundation on Istio and Envoy running on Kubernetes. That’s why we selected a relatively simple security requirement – HTTP Security Headers – and want to exemplify how to implement it in a secure-by-default manner for this emerging technology stack.
Why use Security Headers?
Well, the web evolved a lot over the years. Many new technologies were introduced, but they were also accompanied by new attack vectors such as Clickjacking and Cross-site Scripting as well as privacy issues. Browser vendors addressed these problems with new security and privacy features. Unfortunately, enabling them by default would break existing websites. As a result, each site has to take the decision which features to enable and how to configure them. This is accomplished by sending respective HTTP response headers that inform the browser which features to enable for your site. The following snippet gives an example of setting a security header.
HTTP/1.1 200 OK Content-Length: 88 Content-Type: text/html X-Frame-Options: deny <html> ...
The given HTTP response tells the browser to not frame its contents by setting the
X-Frame-Options header. This prevents clickjacking attacks against your page. In case you really need to be framed by certain sites, you can explicitly allow them by replacing
deny with the according origin of the page you would like to be framed by. By explicitly allowing origins the risk of being framed by an attacker-controlled origin is minimized.
Why set Security Headers secure-by-default?
The problem of breaking legacy applications changes when you move to a new platform and build your applications from scratch – or at least you have to set them up for the new platform. The point is, there won’t be too many legacy applications which break because of the security and privacy features of the browser. These circumstances allow us to turn around the status quo and set security headers secure-by-default. Each application that requires different behavior can overrule this new default by explicitly setting more relaxed security headers.
Which Security Headers should be set?
When searching the internet for security headers you will find a lot of advice. OWASP provides best-practice guidelines and programming frameworks, which describe how to secure apps with security headers. There are even services that directly rate the security and privacy headers of your deployment. Although most of these guidelines fit our requirements, a few are also very application specific and cannot be set without context-specific knowledge of the app. Taking in mind that probably multiple applications run on a single cluster, the solution should be kept generic. Also, some headers handling HTTPS/TLS specifics are left out, certificates for load balancers exposing clusters are not configured by default.
Boiling it down to the security and privacy headers that can be set centrally for an entire cluster and do not handle HTTPS specifics:
|X-Frame-Options||deny||Prevents other sites from framing yours and running Clickjacking attacks (deprecated)|
|Content-Security-Policy||frame-ancestors none;||Prevents other sites from framing yours and running Clickjacking attacks|
|X-XXS-Protection||1; mode=block||Activates browsers’ XSS filter when available; Blocks rendering when XSS detected|
|X-Content-Type-Options||nosniff||Disables content-type sniffing of the browser|
|Referrer-Policy||no-referrer||Disables automatic sending the referrer header when links are followed|
|X-Download-Options||noopen||Disables automatic opening of downloads in older IE versions|
|X-DNS-Prefetch-Control||off||Disables speculative DNS resolving for external links on your page|
|Server||envoy||Automatically set by Istio’s ingress gateway|
|X-Powered-By||Removed to hide name and version of potentially vulnerable application servers|
|Restricts access to interfaces of the host machine for own page and all frames|
This list of default headers might be extended for productive usage with HTTPS enabled endpoints as well as stricter
Feature-Policiy settings. But for now, this is our baseline setting.
Where to implement Security Headers in Istio’s Architecture?
We have a list of security headers to be deployed cluster-wide and now the question is how to integrate them into Envoy resp. the Istio programming model. If you are completely new to Istio and Envoy and how they interact, take a look at the docs page. The following illustration describes how Istio handles Ingress and Egress via proxies and gateway and helps to get an overview where headers could be set.
Incoming requests to the cluster and the service mesh and outgoing responses route through the Ingress gateway. Service to service communication within the service mesh is handled through the Envoy sidecars placed in each pod. Outgoing requests and incoming responses route through the optional Egress gateway. Gateways, as well as sidecars, are instances of the Envoy proxy running in the cluster.
Service to service calls within the mesh does not require security headers interpreted by the browser. So, the Envoy sidecars are probably not the best place to check and set our headers when we don’t want to cause unnecessary processing and cluster-internal traffic overhead. Also, requests leaving the mesh through the optional Egress gateway do not communicate with browsers in a way security headers would make sense. The outbound handler of the Ingress gateway is where responses may end up in a browser and where we should set security headers. Therefore, the Ingress gateway(s) is the sweet spot to set security headers secure-by-default for the browser.
Be well aware: There are other ways of exposing services with Kubernetes, but this article focuses on Istio’s programming model!
Implementation as an Envoy Filter
As previously outlined, the Ingress gateway is actually realized as an Envoy proxy. In order to implement the secure-by-default header concept, functionality for response manipulation offered by Envoy can be leveraged. According to the Envoy documentation header manipulation can be accomplished through response handlers defined as a Lua filter. A Lua filter is basically a snippet of Lua code that is executed for each request or response it is registered for. With Istio, this Lua filter can be configured centrally and is distributed to the respective Envoy instance of the Ingress gateway. The documentation for using Envoy filters within Istio can be found here. The below resource gives an example of how to configure the secure-by-default header filter for the Ingress gateway via Istio:
apiVersion: networking.istio.io/v1alpha3 kind: EnvoyFilter metadata: name: security-by-default-header-filter spec: filters: - listenerMatch: listenerType: GATEWAY filterType: HTTP filterName: envoy.lua filterConfig: inlineCode: | function envoy_on_response(response_handle) function hasFrameAncestors(rh) s = rh:headers():get("Content-Security-Policy"); delimiter = ";"; defined = false; for match in (s..delimiter):gmatch("(.-)"..delimiter) do match = match:gsub("%s+", ""); if match:sub(1, 15)=="frame-ancestors" then return true; end end return false; end if not response_handle:headers():get("Content-Security-Policy") then csp = "frame-ancestors none;"; response_handle:headers():add("Content-Security-Policy", csp); elseif response_handle:headers():get("Content-Security-Policy") then if not hasFrameAncestors(response_handle) then csp = response_handle:headers():get("Content-Security-Policy"); csp = csp .. ";frame-ancestors none;"; response_handle:headers():replace("Content-Security-Policy", csp); end end if not response_handle:headers():get("X-Frame-Options") then response_handle:headers():add("X-Frame-Options", "deny"); end if not response_handle:headers():get("X-XSS-Protection") then response_handle:headers():add("X-XSS-Protection", "1; mode=block"); end if not response_handle:headers():get("X-Content-Type-Options") then response_handle:headers():add("X-Content-Type-Options", "nosniff"); end if not response_handle:headers():get("Referrer-Policy") then response_handle:headers():add("Referrer-Policy", "no-referrer"); end if not response_handle:headers():get("X-Download-Options") then response_handle:headers():add("X-Download-Options", "noopen"); end if not response_handle:headers():get("X-DNS-Prefetch-Control") then response_handle:headers():add("X-DNS-Prefetch-Control", "off"); end if not response_handle:headers():get("Feature-Policy") then response_handle:headers():add("Feature-Policy", "camera 'none';".. "microphone 'none';".. "geolocation 'none';".. "encrypted-media 'none';".. "payment 'none';".. "speaker 'none';".. "usb 'none';"); end if response_handle:headers():get("X-Powered-By") then response_handle:headers():remove("X-Powered-By"); end end
This configuration file defines an HTTP filter in contrast to the also available lower-level network filters. The HTTP filter should be executed on the
GATEWAY listener – the Ingress gateway. Further, the type indicates that a Lua script defines the behavior of the filter. The Lua script itself registers itself onto the
envoy_on_response event which means the filter is only invoked for outgoing traffic and not incoming requests. The logic of the Lua code is quite simple: When a security header is already defined by the application do nothing. Otherwise, add the security header with a strict setting. Take into account that only parts of the
Feature-Policy are set by the filter.
Just apply the upper YAML (
secure-http-headers.yaml) to an Istio cluster and the secure-by-default headers are ready to go.
$ kubectl apply -f secure-http-headers.yaml
You can see the installed Envoy filter by running.
$ kubectl get envoyfilter NAME AGE security-by-default-header-filter 1m
Now, the filter takes care that the headers are set strictly, but what if a single application requires standard behavior of the browser no matter which security implications it brings? – No problem! But in a secure-by-default environment, the application has to explicitly set the respective headers to the fallback value to inform browsers to return to their default behavior and turn security and privacy features off.
Test it yourself
Do you have an application running and exposed via the Istio gateway? Just deploy the filter and see how your app rates on securityheaders.com. Great service!
To have the basic HTTP security headers set secure-by-default on an Istio cluster’s Ingress gateway deploy the filter above (the big code snippet) with
kubectl apply. The secure-by-default headers can be overruled by setting them explicitly within your application.