Turn Those 403s into 302s Using EnvoyFilters and Inline Lua Scripts

Turn Those 403s into 302s Using EnvoyFilters and Inline Lua Scripts

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.