First we show an example of plain istio authentication and access control using JWT. After that we try to apply the same to Knative services. To make the example self hosted, but still realistic, we use Keycloak.
- Install Istio
- Set up a sample pad
- Block access for unauthenticated users
- Install Keycloak
- Set up a Realm and OpenID Connect client
- Retrieve access token
- Gain access to the test pod using Bearer token
- Lock ourselves out again using Istio authorization
- Log in as the user that is granted access
A special thanks goes to Keycloak's blog post about Istio.
A few things will vary depending on your cluster setup. You might be able to access the istio-ingressgateway
over NodePort or an actual public IP/FQDN. If not you can run tests in-cluster, using an alias or function.
# maybe
function istiocurl() {
kubectl run --restart=Never -t -i --rm --image=solsson/curl istiocurl -- --connect-to :80:istio-ingressgateway.istio-system.svc.cluster.local:80 $@
}
# or maybe
alias istiocurl="curl --connect-to :80:$(minikube ip):31380"
Soon enough you'll get to verify that your alias works.
We're using Knative's Istio configuration.
kubectl apply -f https://github.com/knative/serving/releases/download/v0.2.1/istio-crds.yaml
kubectl apply -f https://github.com/knative/serving/releases/download/v0.2.1/istio.yaml
We use the Istio Authentication Policy example, but without the mutual TLS part.
kubectl apply -f ./02-istio-httpbin/
istioctl kube-inject -f ./02-istio-httpbin/httpbin.yaml | kubectl -n foo apply -f -
If Istio injection worked you'll have three containers in the pod. Try kubectl -n foo logs -l app=httpbin -c istio-proxy
.
Check that the service responds through Istio
$ istiocurl http://x/headers -w '\n'
{
"headers": {
...
"X-Envoy-Decorator-Operation": "httpbin.foo.svc.cluster.local:8000/*",
...
We already exposed httpbin to the public, so let's hurry up with:
kubectl apply -f ./03-authentication
Note in the Policy that we're refering to the Keycloak service that is yet to be created.
Now, once the policy has propagated to you sidecars, access should be denied
$ istiocurl http://x/headers -w '\n'
Origin authentication failed.
Any installation is fine, but let's use the helm chart because it can automatically create an admin user:
helm install --namespace keycloak --name demo stable/keycloak \
--set keycloak.ingress.enabled=true \
--set keycloak.service.type=NodePort \
--set keycloak.service.nodePort="30080" \
--set keycloak.username=admin \
--set keycloak.password=password
Use kubectl -n keycloak get service demo-keycloak-http
to verify existence of the service that the authentication step (above) depends on.
Use appropriate means of accessing the UI in a browser, for example the NodePort enabled by the helm options above (minikube service -n keycloak demo-keycloak-http
).
The Keycloak UI tends to malfunction over kubectl port-forward
.
You should see a login page where the username and password generated above works.
- Create a realm named
demo
. - Under the
Login
tabRequire SSL
selectnone
to allow plain http. - Create two users
test1
andtest2
and use theCredentials
tab to set their passwords totest
withTemporary
set toOFF
. - Create an OpenID Connect "client" named
myapp
in your demo realm.- Any "Root URL" is fine for this example
- In the client make sure "Acces Type" is
public
. We want this demo setup is as insecure as possible 🙂.
Finally there's some excitement! You may test browser login first, but we can do even better and brute force it (assuming $(minikube ip):30080
is your keycloak host):
$ curl -X POST "http://$(minikube ip):30080/auth/realms/demo/protocol/openid-connect/token" \
-H "Host: demo-keycloak-http.keycloak" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d 'username=test1&password=test&grant_type=password&client_id=myapp' -s \
| jq -r '.access_token'
# a long base64 string
Try the same command for user test2
, as you'll need to alternate between these logins shortly.
You can inspect and verify tokens using for example jwt.io. It'll need the key: echo "-----BEGIN RSA PUBLIC KEY-----"; curl -s http://$(minikube ip):30080/auth/realms/demo | jq -r '.public_key'; echo "-----END RSA PUBLIC KEY-----";
.
Tokens expire after a while so you may want these two lines, together with the alias created during preparations.
$ TOKEN=$(<the curl + jq above>)
$ istiocurl http://x/headers -w '\n' -H "Authorization: Bearer $TOKEN"
{ ... }
Now, does authentication only apply when going through Istio's gateway?
kubectl run --restart=Never -t -i --rm --image=gcr.io/cloud-builders/curl testcurl -- http://httpbin.foo:8000/ -w '\n' -H "Authorization: Bearer $TOKEN"
Indeed not.
See your decoded token for what you can use as claims. Here we try the user (UU)ID from keycloak. Dig into the Authorization spec for details. And don't forget the RbacConfig or your ServiceRole+binding will have zero effect.
kubectl apply -f ./08-authorization
# And then update the RBAC to allow one of your users in (see decoded token)
kubectl -n foo edit servicerolebinding jwt-binding
At this stage the Debugging Authorization section in Istio docs is a recommended read,
in particular how to access logs.
For example if you enable debug logging and run kubectl logs
as described, but with | grep auth | grep key
instead you can see requests to keycloak.
You should now be blocked when using one user's token and allowed in when using the other.
You can keep editing the Keycloak User (and/or Realm) and the ServiceRoleBinding to test other Properties for authorization.
kubectl delete namespace keycloak
kubectl