If you use Envoy Proxy as part of your service mesh - e.g. if you use Istio - you can turn user-facing 403 access denied responses into 302 redirects, which can then point users to a form where they can request access. It's a much nicer experience for your users than showing them an error screen, giving them a chance to do something about not having access to the web app that they are attempting to use.
To do this you'll need to create an EnvoyFilter
. The full Envoy documentation is pretty extensive and a little intimidating. Fortunately, the Istio docs are nice enough to provide some simplified documentation that covers this use case.
Let's start with the basics, by creating the first set of fields for the Envoy filter:
apiVersion: networking.istio.io/v1alpha3 # note that this is the Istio API
kind: EnvoyFilter
metadata:
name: modify-403-to-302 # feel free to change this
namespace: <target-namespace> # whatever namespace you'd like this to apply to
It's worth noting that the apiVersion
is specific to Istio. If you're using Envoy Proxy without Istio, then modify this to the correct CRD API.
How and Where to Apply the EnvoyFilter
Envoy Proxy comes with a ton of pluggable functionality, including types of filters at both Layer 4 and Layer 7, as well as context configurations and filter chain injection into various points of the request lifecycle.
If we're thinking about this logically, we only need to intercept the response that's going back out to the user, when something upstream (e.g. Open Policy Agent) has already decided to 403 the request. We also need to modify the HTTP headers, so we're going to need to tap into the Envoy HTTP_FILTER
, specifically the envoy.filters.network
.http_connection_manager
filter chain:
spec:
configPatches:
- applyTo: HTTP_FILTER
match:
listener:
filterChain:
filter:
name: "envoy.filters.network.http_connection_manager"
subFilter:
name: "envoy.filters.http.router"
You can read more about the applyTo
and match
fields here and here, respectively. There are additional configuration options you may want to set for your implementation, e.g. match.context
which determines how this applies to inbound vs outbound traffic.
Filter Functionality as Inline Code
Envoy filters support Lua scripts as inline code, so we can write a quick manipulation block to modify the headers and replace the 403 response status with a 302 status. To do that, we'll need to define the envoy_on_response
function that Envoy calls, well, on response, as part of its Stream Handle API.
Here's the Lua block I wrote:
function envoy_on_response(response_handle)
if (response_handle:headers():get(":status") == "403") then
response_handle:logInfo("Got status 403, redirect to landing page...")
response_handle:headers():replace(":status", "302")
response_handle:headers():add("location", "https://example.com/request-access/")
end
end
I had to validate it against an online runner, because my Lua is, to say the least, not great.
When you plug this inline code block into the EnvoyFilter
configuration YAML, it will look like this:
spec:
configPatches:
- applyTo: HTTP_FILTER
match: ... # condensed for simplicity
patch:
operation: ADD
value:
name: envoy.filters.http.lua
typed_config:
"@type": "type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua"
# the inline code block
# replace the location header value with your URL
inline_code: |
function envoy_on_response(response_handle)
if (response_handle:headers():get(":status") == "403") then
response_handle:logInfo("Got status 403, redirect to landing page...")
response_handle:headers():replace(":status", "302")
response_handle:headers():add("location", "https://example.com/request-access/")
end
end
For the full YAML file, see this gist.
And that's it! Apply this to your cluster, and your 403s will become 302s. Be sure to only do this for user-facing routes, as doing this for API calls may result in unhappy API consumers.