Keycloak with istio and Oauth2-Proxy.

Chris Haessig
8 min readSep 23, 2024

--

What the heck are we trying to do ?

Setting up Istio with Keycloak and OAuth2 Proxy is a common pattern for adding authentication and authorization to your microservices architecture. Each component plays a crucial role in securing access to resources while maintaining flexibility and scalability.

  • Keycloak acts as an identity provider (IdP) and OAuth2 authorization server. It manages user authentication, including multi-factor authentication (MFA), single sign-on (SSO), and federation.
  • By integrating OAuth2 Proxy, you can convert the OAuth2 authentication flow from Keycloak into HTTP headers that are passed to backend services. This decouples services from handling authentication logic, allowing centralized security management.

A JWT is a compact, URL-safe token format used for securely transmitting information between two parties. It’s commonly used in authentication and authorization systems because of its self-contained nature, allowing for efficient and stateless verification of data.

JWTs are self-contained, meaning all the information needed to verify the token is included within the token itself. This allows for stateless authentication, where the server doesn’t need to store session data. The server simply verifies the token signature and reads the claims.

JWTs can include custom claims that allow services to make decisions based on specific user data. For example, roles and permissions can be embedded within the token, enabling role-based access control (RBAC) without requiring additional database lookups.

Cool beans ! Let’s set this up to protect a test pod ( httpbin ).

Initial istio setup

To start, we setup some namespaces, we create the nginx proxy, and httpbin pod first. We label namespaces that istio will attach too. We also expose the deployments to create a service out of them.

kubectl create ns web
kubectl create ns httpbin
kubectl create ns keycloak

kubectl label ns httpbin istio-injection=enabled
kubectl label ns web istio-injection=enabled

kubectl create deploy httpbin --image=kennethreitz/httpbin -n httpbin
kubectl create deploy web --image=nginx -n web
kubectl expose deploy httpbin --port 80 -n httpbin
kubectl expose deploy web --port 80 -n web

Install Keycloak

Using helm, lets install keycloak in the keycloak namespace.

helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
helm install keycloak bitnami/keycloak -n keycloak

Or you can apply this yaml from the keycloak site.

kubectl create -f https://raw.githubusercontent.com/keycloak/keycloak-quickstarts/refs/heads/main/kubernetes/keycloak.yaml

Install the oauth-proxy

Then we install the oauth2-proxy also in the keycloak namespace.

helm repo add oauth2-proxy https://oauth2-proxy.github.io/manifests
helm repo update
helm install oauth2-proxy oauth2-proxy/oauth2-proxy -n keycloak

Once done everything should be running.

kubectl get pods -n keycloak 

NAME READY STATUS RESTARTS AGE
keycloak-f858787c-cxgtd 1/1 Running 0 6d20h
nginx-proxy-8678f56795-vgr4k 1/1 Running 0 21h
oauth2-proxy-7bf7d55fcd-zr54x 1/1 Running 0 19h

Configure Keycloak

Now that keycloak is installed, we will need to configure it to handle our requests. We port-forward to the keycloak admin UI. ( username “admin” and password “admin ) to make some changes.

kubectl port-forward svc/keycloak 8080:80 -n keycloak 

Once logged in, click on the realms section to create a new realm. I will call mine “istio. Click on users and add a user with a email and set a password.

Create a new client, I called mine “istio”, make sure to copy the client secret as it’s needed in a bit.

Some of these settings are up to you, but I use Client authentication which makes the most sense for my setup.

Click client scope , click on the <your name>-dedicated assigned client scope. then click “Add Mapper” and “”By Configuration” and select Audience .

It should look like this.

We will use the audience later to map our request.

We also need to tell the client where to go once we authenticated. This will be the location of the oauth2-proxy.

Save it, now keycloak should be all set.

We deployed oauth2-proxy with helm already but need to reconfigure it to interact with our newly configured keycloak. Replace the cliet-id , client-secret and relevant urls to make sure it matches up with our keycloak install. Upstream for example is where forward too, once the we receive our token.

kubectl edit deploy oauth2-proxy -n keycloak
containers:
- args:
- --http-address=0.0.0.0:4180
- --https-address=0.0.0.0:4443
- --provider=oidc
- --client-id=istio
- --cookie-secure=false
- --client-secret=<changetosecret>
- --oidc-issuer-url=http://keycloak.keycloak.svc.cluster.local/realms/istio
- --redirect-url=http://oauth2-proxy.keycloak.svc.cluster.local/oauth2/callback
- --insecure-oidc-allow-unverified-email=true
- --email-domain=*
- --cookie-domain=".svc.cluster.local"
- --cookie-secret=d1h0XXNabU1Wa3lkOZZ5MzwwNVhtSFgzNzBxdW5CU0o=
- --upstream=http://httpbin.httpbin.svc.cluster.local
- --whitelist-domain=".svc.cluster.local"
- --pass-authorization-header=true
- --set-authorization-header=true
- --reverse-proxy=true

Protect our httpbin pod.

We don’t want anyone to be able to access our httpbin pod without authentication and authorization , so we apply a RequestAuthentication and AuthorizationPolicy kubernetes CRD.

With the RequestAuthentication, we verify that keycloak signed our payload. The “istio” realms issuer holds the public key.

Once the token is verified, we use the AuthorizationPolicy to parse the token and confirm the correct data was returned. We can do things like confirm the user logged in and may belong to a specific group or has a certain email for example

The CRD below use request.auth.claims[aud] , which ensures the token was signed and is apart of the “istio” audience.

-
apiVersion: security.istio.io/v1beta1
kind: RequestAuthentication
metadata:
name: jwt-confirm
namespace: httpbin
spec:
selector:
matchLabels:
app: httpbin
jwtRules:
- issuer: "http://keycloak.keycloak.svc.cluster.local/realms/istio"
jwksUri: "http://keycloak.keycloak.svc.cluster.local/realms/istio/protocol/openid-connect/certs"
audiences:
- "istio"
---
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: verify-jwt
namespace: httpbin
spec:
selector:
matchLabels:
app: httpbin
action: ALLOW
rules:
- from:
- source:
requestPrincipals: ["http://keycloak.keycloak.svc.cluster.local/realms/istio/*"]
when:
- key: request.auth.claims[aud]
values: ["istio"]

Configure Istio and Ingress

Keep in mind I am using a k8s cluster with kind. But I need to be able to resolve nginx, keycloak, and oauth hostnames and point them to the correct destination. Of course feel free to use your own.

I made this happen by setting up my /etc/hosts file.


127.0.0.1 nginx.web.svc.cluster.local
127.0.0.1 oauth2-proxy.keycloak.svc.cluster.local
127.0.0.1 keycloak.keycloak.svc.cluster.local

My gateway routes are defined below, your wills greatly differ. Listening on port 3333, depending on the hostname, will route to the appropriate locations.

---
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
name: nginx
namespace: istio-gateways
spec:
selector:
istio: ingressgateway
servers:
- port:
number: 3333
name: http
protocol: HTTP
hosts:
- nginx.web.svc.cluster.local
- keycloak.keycloak.svc.cluster.local
- oauth2-proxy.keycloak.svc.cluster.local
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: oauth2-proxy
namespace: istio-gateways
spec:
gateways:
- nginx
hosts:
- "nginx.web.svc.cluster.local"
http:
- match:
- uri:
prefix: "/"
route:
- destination:
host: nginx.web.svc.cluster.local
port:
number: 80
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: oauth2-proxy-keycloak
namespace: istio-gateways
spec:
gateways:
- nginx
hosts:
- "keycloak.keycloak.svc.cluster.local"
http:
- match:
- uri:
prefix: "/"
route:
- destination:
host: keycloak.keycloak.svc.cluster.local
port:
number: 80
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: oauth2-proxy-proxy
namespace: istio-gateways
spec:
gateways:
- nginx
hosts:
- "oauth2-proxy.keycloak.svc.cluster.local"
http:
- match:
- uri:
prefix: "/"
route:
- destination:
host: oauth2-proxy.keycloak.svc.cluster.local
port:
number: 80

Now that the ingress should be working, we also need to apply an EnvoyFilter to our nginx pod. We want traffic to redirect to keycloak via the oauth2-proxy if a token is invalid or does not exist. Configuring Istio with a EnvoyFilter will achieve our goal by doing the redirection, we tie this logic to the pod with the app=nginx label.

---
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: oauth2-proxy-filter
namespace: web
spec:
workloadSelector:
labels:
app: nginx
configPatches:
- applyTo: HTTP_FILTER
match:
context: SIDECAR_INBOUND
listener:
filterChain:
filter:
name: "envoy.filters.network.http_connection_manager"
subFilter:
name: "envoy.filters.http.router"
patch:
operation: INSERT_BEFORE
value:
name: envoy.filters.http.ext_authz
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
http_service:
server_uri:
uri: http://oauth2-proxy.keycloak.svc.cluster.local:80
cluster: outbound|80||oauth2-proxy.keycloak.svc.cluster.local
timeout: 5s
authorization_request:
allowed_headers:
patterns:
- exact: "cookie"
- exact: "authorization"
authorization_response:
allowed_upstream_headers:
patterns:
- exact: "set-cookie"
- exact: "authorization"

Using an EnvoyFilter to apply the auth logic to the nginx pod is explained above, this made the most sense on my environment. You can also apply the external authorizer to the istio config globally and NOT use a EnvoyFiler.

Example below.

  meshConfig:
extensionProviders:
- name: "oauth2-authz"
envoyExtAuthzHttp:
service: "oauth2-proxy.keycloak.svc.cluster.local"
port: 80
includeHeadersInCheck:
- "authorization"
- "cookie"
- "x-forwarded-for"
- "x-request-id"
includeHeadersInResponse:
- "authorization"
- "set-cookie"
- "x-auth-user"
timeout: "5s"
defaultProviders:
authorization: "oauth2-authz"

Trying it out.

We should be set ! The first thing we should do is try and hit httpbin. Remember, our RequestAuthentication and AuthorizationPolicy CRDs will prevent us from accessing the pod as we are not sending a valid token and even if we did, the audience claim “istio” is not set.

curl httpbin.httpbin
RBAC: access denied

Perfect, this is what we want to see. How about if we open a browser and go to nginx.web.svc.cluster.local , looks like it redirects us to the oauth2-proxy then keycloak.

Then we login

And it redirects us to httpbin , without the “RBAC denied” message, sweet, it worked .

A quick walk through, we hit nginx.web.svc.cluster.local which redirects us to the oauth2-proxy via a EnvoyFilter. That redirected us to keycloak. We login to keycloak which then redirected us back to the oauth2-proxy which redirected us to the httpbin pod passing the Bearer token. Easy right :)

If you look at the network traffic, we can find the token returned.

The istio CRDs then parsed the token pulling out the respected claims, if you take the token and paste it into http://jwt.io we can see that it contains.

It is NOT a coincidence that the data in the token has data we populated with keycloak. Notice, our audience claim is referenced with the AuthorizationPolicy CRD as well.

Summary

Don’t be fooled, this stuff is complicated and we have only began to explore what is possible, but I hope this helps with your system.

Enjoy

Profit ?

Shoutout to https://picluster.ricsanfre.com/docs/sso/ for reminding me about the audience mapping.

--

--

Responses (4)