This post will show how Istio can be used to force users to authenticate before accessing applications. Picture a use case were you are working on an application with a microservice architecture. The end user have multiple endpoints to connect to and you want all of them to be protected behind some access control. Instead of having each service provide its own authentication code you want some separate service to handle this for all your applications.
My solution for this is to use Istio. There are many posts and guides on different benefits and use cases for Istio but this is a rarer use case I could not find any detailed examples about. The solution comes down to using Istio and its authorization policies to route all requests to specific hostnames through an OAuth2-Proxy to any Identity provider (IDP) supporting OIDC.
Prerequisites
- A running Kubernetes cluster
- Two dns records pointing into the cluster. The guide uses a LoadBalancer behind a public IP, but a NodePort can also be used.
- Kubectl and helm3 cli installed.
- Clone/fork the repository vanneback/istio-external-auth-demo
In my example I used a kubernetes cluster in AKS. With a public IP exposed behind a LoadBalancer. I also have two dns-records which in the guide is replaced by httpbin.example.com
and dex.example.com
Installing the sample application
To demonstrate this an example application called httpbin is used. This application frequently occurs in the Istio guides which makes it a perfect app for this example. Start by installing namespaces and the application.
kubectl apply -f namespaces.yaml
kubectl apply -f httpbin-deploy.yaml
Now the application should be installed and accessible only through the cluster. Check installation with.
kubectl get pods -n demo
kubectl port-forward -n demo svc/httpbin 8000:8000
There should be one pod deployed in demo with only 1/1 containers ready. When installing Istio there will be a sidecar added here. Access the application on localhost:8000
Installing Istio
In this guide I was using Azures AKS which has the option to use the LoadBalancer service type with a static IP. If you also use Azure replace the IPs in istio-controlplane.yaml
with your public IP. If you are not using a provider with support for LoadBalancers you can replace this with NodePorts. An example of this is commented in the istio-controlplane.yaml
file. After the config is ready install Istio with:
kubectl apply -f istio-1.12.1/manifests/charts/base/crds/crd-all.gen.yaml
./istio-1.12.1/bin/istioctl operator init
kubectl apply -f istio-controlplane.yaml
./istio-1.12.1/bin/istioctl verify-install
The most interesting thing about this config is the meshConfig parameters:
meshConfig:
extensionProviders:
- name: "oauth2-proxy"
envoyExtAuthzHttp:
service: "oauth2-proxy.demo.svc.cluster.local"
port: "80"
headersToDownstreamOnDeny:
- content-type
- set-cookie
headersToUpstreamOnAllow:
- authorization
- cookie
- path
- x-auth-request-access-token
- x-forwarded-access-token
includeHeadersInCheck:
- "cookie"
- "x-forwarded-access-token"
This config is creating a new provider
with the name oauth2-proxy
. This can later be used in the authorization-policy to route requests through the oauth2-proxy. The rest of this config is basically what headers to include to make sure the correct tokens are passed along.
If you delete the httpbin-pod now it should restart with a sidecar. Check that it creates a pod with 2/2 containers ready:
kubectl delete pod -n demo --all
kubectl get pods -n demo
Create certificates and gateways
To make the example represent a correct installation we will use cert-manager to create some valid certificates for us. Install cert-manager with helm:
helm repo add jetstack https://charts.jetstack.io
helm repo update
helm install \
cert-manager jetstack/cert-manager \
--namespace cert-manager \
--version v1.4.0 \
--set installCRDs=true \
--set startupapicheck.enabled=false
Replace email
in cluster-issuer.yaml
then replace the occurrences of httpbin.example.com
with the url you want to use for your httpbin-app. Do this in both httpbin-istio-gw.yaml
and httpbin-tls-cert.yaml
then apply with:
kubectl apply -f cluster-issuer.yaml
kubectl apply -f httpbin-istio-gw.yaml
kubectl apply -f httpbin-tls-cert.yaml
One thing to note here is how the certificates are created in the istio-system namespace. This is a limitation when using Istio with cert-manager. For more information about Istio gateways and services go to the Istio docs
Validate
You can validate the installation by checking pods and certificates with:
kubectl get pods -n cert-manager
kubectl get pods -n demo
kubectl get certificate -n istio-system
This should show ready pods an a ready certificate. Then visit your url httpbin.example.com
. You can also go to httpbin.example.com/headers
to see the included headers. At the moment there should not be any header called Authorization
which should be added when the authentication flow is complete.
Installing Dex
For authentication any IDP which supports OIDC can be used. In this example Dex is installed which can in turn be connected to other AD sources see our blog post on how to connect dex to google for more information. In this post we will only use Dex static user as an example.
Install dex by replacing the occurrences of dex.example.com
with the url you want to use for dex. Do this in dex-values.yaml
, dex-istio-gw.yaml
and dex-tls-cert.yaml
. Then run:
helm repo add dex https://charts.dexidp.io
helm repo update
helm install \
--namespace demo \
--values dex-values.yaml \
--version 0.6.5 \
dex dex/dex
kubectl apply -f dex-istio-gw.yaml
kubectl apply -f dex-tls-cert.yaml
Installing oauth2-proxy
The final tool needed is OAuth2-Proxy. Oauth2-proxy is an open source software handling the authentication flow needed for OAuth2 or in this case OIDC. This will handle the Authentication flow and pass the needed token back to the application.
Install by replacing oidc_issuer_url
and cookie_domains
from oauth2-proxy-values.yaml
with your domain name then apply with:
helm repo add oauth2-proxy https://oauth2-proxy.github.io/manifests
helm repo update
helm install \
--namespace demo \
--values oauth2-proxy-values.yaml \
--version 5.0.6 \
oauth2-proxy oauth2-proxy/oauth2-proxy
Apply authorization policy
Finally apply the authorization-policy to tell Istio what requests should be routed through the oauth2-proxy.
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: oauth-policy
namespace: istio-system
spec:
selector:
matchLabels:
istio: ingressgateway
action: CUSTOM
provider:
name: "oauth2-proxy"
rules:
- to:
- operation:
hosts:
- "httpbin.example.com"
The most important part of this config is the provider name which has to match the provider created in the meshConfig part of the istio-controlplane.yaml
and the hosts
list which is the host names that the policy applies to.
Apply by replacing httpbin.example.com
with you app url in authorization-policy.yaml
then run:
kubectl apply -f authorization-policy.yaml
The authorization policy will trigger when trying to access the hostname configured. When the policy is triggered it will use the extensionProvider from the istio-controlplane.yaml
config. This will cause a redirect to the oauth2-proxy which in turn will go to dex for authentication.
Validation
You should now be able to access httpbin on your url for httpbin.example.com
. This should take you directly to the Dex login page were you can authenticate with:
Username: admin
Password: password
To check the authorization token visit httpbin.example.com/headers
and it should be a header named Authorization
with a jwt token.
Authentication and Authorization
If you have made it this far you have probably heard of authentication vs authorization before. I just want to make it clear that this use case of Istio is only used for Authentication. Istio will route to the authentication service and if a valid token is presented it will pass you on to the application. This provides some security for all the applications but the application will still have to be responsible for Authorization and any form of RBAC.
ID Token vs Access Token
To be brief ID Tokens
are passed by the Authentication server as a proof that you have been authenticated. It can be used to get information about the user such as name, email etc. It should however not be the token used by the Applications to decide what you are allowed to do. For this an Access Token
should be used. For a more detailed explanation see this blog post.
An issue with the OAuth2-Proxy is that it sends the ID Token
under the Authorization
header. And its difficult to even access the Access Token
. A solution for this is first to enable the options under config.configFile
in the oauth2-proxy helm chart:
set_xauthrequest = true
set_authorization_header = true
pass_authorization_header = true
pass_host_header = true
pass_access_token = true
The entire config is in oauth2-proxy-values.yaml
. The second part is to enable Istio to pass this header on to the end applications by adding this line to the meshConfig in istio-controlplane.yaml
:
includeAdditionalHeadersInCheck:
authorization: '%REQ(x-auth-request-access-token)%'
The final part which is not displayed in these examples is by having istio replacing the Authorization
header in the virtualService resource. A full example of this is displayed below.
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
namespace: demo
name: httpbin-vsvc
spec:
hosts:
- "httpbin.example.com"
gateways:
- httpbin-gateway
http:
- match:
- uri:
prefix: /
route:
- destination:
port:
number: 8000
host: httpbin.demo.svc.cluster.local
headers:
request:
set:
Authorization: 'Bearer %REQ(x-auth-request-access-token)%'
Even if this was not the default I included in the examples it can be important to know and was not trivial to find how to make this replacement. The optimal solution would be if this is included as an option or even the default in the OAuth2-proxy. But as of writing this post this is not the case.
Conclusion
Hopefully this blog gives an insight on how Istio together with OAuth2 Proxy can be used as layer in front of applications were authentication is needed. At least I hope it provides some clarity how to configure Istio to do this, and perhaps it can help make your decision on how to handle authentication in microservices easier.