See Part I - Implementing API Gateway using Envoy for creation of the initial API gateway.
This post will demonstrate:
- Integration with Keycloak using the OpenId Connect protocol to allow the gateway to handle user authentication.
- Once authenticated, decode the JWT token and pull out the unique identity id (
sub
), add it to the request headers for backend services to use.
The Envoy configuration will be broken down to help explain it, so feel free to check out the full envoy.yaml at any point. It will be worth reviewing it at the end of the post to see how it is combined.
Three http_filters
will be added. These run on the application layer (L7) so this basically means the HTTP protocol i.e. what the end user sees. We're interested in these filters because I want to manipulate the HTTP request that our upstream backend services receive. Envoy filers are hierarchical and based on ordered network layers (L4 before L7). The focus here is on http filters.
The job of each filter is as follows:
- OAuth2 - Authenticate and return the JWT token to the client
- JWT - When a client request is made with a token, decode it and add it to Envoy's meta data (
payload_in_metadata
) - Lua - Using Envoy's meta data, pull out the subject claim (
sub
) from the token and add the value to the request headers to pass to upstream services
OAuth2 filter
To configure this filter, common information about the identity endpoints such as token endpoint url, client-id and secret are required. Read the docs for more information. It's worth noting the absence of any HTTP configuration and CSRF handling. This configuration is not suitable for production and only development purposes.
There is also a forward_bearer_token
config option which will pass the token to upstream services which might be useful if you want services to handle RBAC type of scenarios.
- name: envoy.filters.http.oauth2
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.oauth2.v3alpha.OAuth2
config:
token_endpoint:
cluster: keycloak
uri: http://127.0.0.1:8080/auth/realms/master/protocol/openid-connect/token
timeout: 5s
authorization_endpoint: http://127.0.0.1:8080/auth/realms/master/protocol/openid-connect/auth
redirect_uri: "http://%REQ(:authority)%/callback"
redirect_path_matcher:
path:
exact: /callback
signout_path:
path:
exact: /signout
credentials:
client_id: "burger-shop"
token_secret:
name: token
sds_config:
path: "/etc/envoy/token-secret.yaml"
hmac_secret:
name: hmac
sds_config:
path: "/etc/envoy/hmac-secret.yaml"
auth_scopes:
- openid
- profile
- email
- roles
JWT filter
The purpose of using this is purely to decode the token to gain access to it's values and add data needed to request headers.
An important part is - allow_missing_or_failed: {}
which means we do not want to fail the request in this filter, we're letting the oauth2
filter handle that side of things.
- name: envoy.filters.http.jwt_authn
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication
providers:
oidc_provider:
issuer: http://kubernetes.docker.internal:8080/auth/realms/master
audiences:
- master-realm
- account
payload_in_metadata: jwt_payload
forward_payload_header: x-jwt-payload
remote_jwks:
http_uri:
uri: http://127.0.0.1:8080/auth/realms/master/protocol/openid-connect/certs
cluster: keycloak
timeout: 5s
rules:
- match:
prefix: /api
requires:
requires_any:
requirements:
- provider_name: oidc_provider
- allow_missing_or_failed: {}
Lua filter
Next up is the Lua filter. This allows us to write some simple logic to pull out elements of the JWT token I added to Envoy's meta data.
As you'll see in the code below we're pulling out the "sub" field from the "jwt_payload" I stored in the previous filter (payload_in_metadata: jwt_payload
).
The general idea behind behind this is to only pass information upstream that is need and to not pass the whole token around all services if they don't need it. This is preferable from a security point of view. Depending on your situation you may need to forward the whole token.
- name: envoy.filters.http.lua
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
inline_code: |
function envoy_on_request(request_handle)
local payload = request_handle:streamInfo():dynamicMetadata():get("envoy.filters.http.jwt_authn")["jwt_payload"]
request_handle:headers():add("jwt-extracted-sub", payload.sub)
end
function envoy_on_response(response_handle)
end
- name: envoy.router
Adding the Keycloak service cluster
As demonstrated in the last post, a cluster for our Keycloak service is needed. Below is an extract of that.
clusters:
- name: keycloak
connect_timeout: 0.25s
type: strict_dns
lb_policy: round_robin
load_assignment:
cluster_name: keycloak
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address: { address: keycloak, port_value: 8080 }
And that is it. You'll now be able to demonstrate an authentication mechanism and access to backend services behind the gateway and pass data to those services such as a unique user id or claims. To see a full and working example, the code is all available here.