Securing serverless services in Kubernetes with Keycloak Gatekeeper

Vinícius Niche
6 min readNov 11, 2018

--

An endeavor to have role based authorization provisioned as service based on an yaml configuration

role based access control over serverless services

Requirements:

¹: generate keycloak-gatekeeper image

$ git clone https://github.com/keycloak/keycloak-gatekeeper
$ cd keycloak-gatekeeper
$ make docker-build
$ make docker
# in my case
...
Successfully tagged keycloak/keycloak-gatekeeper:v2.3.0

²: install kubeless in kubernetes

$ kubectl create ns kubeless
$ kubectl apply -f https://github.com/kubeless/kubeless/releases/download/v1.0.0/kubeless-v1.0.0.yaml

³: install keycloak in kubernetes

# $ kubectl create serviceaccount --namespace kube-system tiller
# $ kubectl create clusterrolebinding \
tiller-cluster-rule --clusterrole=cluster-admin \
--serviceaccount=kube-system:tiller
# $ helm init --service-account tiller
$ kubectl create ns keycloak
$ helm install --namespace keycloak stable/keycloak

for this sample, i’ve configured Keycloak with the following settings:

- realm: justice-league
client: service-gatekeeper
roles:
- edit (will be able to access isprime function)
- view (won't be authorized to access isprime function)
groups:
- privileged (is associated to service-gatekeeper's edit role)
- unprivileged (is associated to service-gatekeeper's view role)
users:
- superman (belongs to privileged group)
- clarkkent (belongs to unprivileged group)

ps.: remember to set users passwords, we’ll need it later.

every resource used in this article will versioned @ https://github.com/vniche/serverless-gatekeeper

Getting started:

Clone this demo repository and change directory to its folder:

$ git clone https://github.com/vniche/serverless-gatekeeper
$ cd serverless-gatekeeper/

Create a namespace, i’ll use poc:

$ kubectl create ns poc

Deploy a function with kubeless cli, let’s use bitnami’s for example (provided in the repo):

$ kubeless function deploy isprime \
--from-file handler.go \
--dependencies Gopkg.toml \
--handler handler.IsPrime \
--namespace poc \
--runtime go1.10

Gatekeeper

First, give gatekeeper.yaml some attention, replace these with some real values we’ve configured in Keycloak:

# uncomment if client is confidential
# client-secret: <client_secret>
discovery-url: http://<keycloak_address>/auth/realms/justice-league
encryption_key: <random_generated_secret>
upstream-url: http://<service_address>

for example, proxying keycloak and gatekeeper pods ports to local host would look like this:

client-secret: 0f19c44b-ec60–4ff0-aff2–01e3edb327e5
discovery-url: http://127.0.0.1:8080/auth/realms/justice-league
enable-default-deny: true
encryption_key: AgXa7xRcoClDEU0ZDSH4X0XhL5Qy2Z2j
listen: 127.0.0.1:3000
upstream-url: http://isprime.poc.svc:8080

as second step, we can just check the resource policy used for this demo so we can understand how things work:

...
resources:
- uri: /isprime
methods:
- POST
roles:
- service-gatekeeper:edit
require-any-role: true

running gatekeer in kubernetes it’s pretty straightforward. Start by creating the secret that will store the config file:

$ kubectl create secret generic gatekeeper --from-file=./gatekeeper.yaml -n poc

then create the service-gatekeeper deployment that will use the secret:

$ kubectl apply -f .k8s/deployment.yaml -n poc

where deployment basically consists in using the yaml we stored as a secret and use it as runtime configuration file and increase logging and verbosity:

spec:
containers:
- name: service-gatekeeper
image: keycloak/keycloak-gatekeeper:v2.3.0
imagePullPolicy: IfNotPresent
args:
- --config=/etc/secrets/gatekeeper.yaml
- --enable-logging=true
- --enable-json-logging=true
- --verbose=true
volumeMounts:
- name: secrets
mountPath: /etc/secrets
volumes:
- name: secrets
secret:
secretName: gatekeeper

if everything is OK, things should look like this:

$ kubectl get pods -n poc
NAME READY STATUS RESTARTS AGE
isprime-64896fcf8d-rs96f 1/1 Running 0 19h
service-gatekeeper-7f... 1/1 Running 0 19h
$ kubectl get pods -n keycloak
NAME READY STATUS RESTARTS AGE
orbiting-chinchilla-0 1/1 Running 0 21h
orbiting-chinchilla-postg... 1/1 Running 0 21h

Now let’s make dreams come true — i’ve been personally looking for similar solution on available service meshes and couldn’t successfully achieve the goal here — by requesting tokens and consuming endpoints.

To request token as clarkkent:

$ curl -X POST \
http://127.0.0.1:8080/auth/realms/justice-league/protocol/openid-connect/token' \
-H “Content-Type: application/x-www-form-urlencoded” \
-d ‘username=clarkkent&password=<clark_password>&grant_type=password&client_id=service-gatekeeper’

will return a response like:

{"access_token":"...8vMTI3LjAuMC4xiJiYWNjNmNkYy05M...", "expires...

so let’s grab the access token to use it as Authorization header token in service request:

$ curl -H 'Authorization: Bearer ...8vMTI3LjAuMC4xiJiYWNjNmNkYy05M...' --proxy http://127.0.0.1:3000 http://isprime.poc.svc:8080/isprime -d '5' -v
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 3000 (#0)
> POST http://isprime.poc.svc:8080/isprime HTTP/1.1
> Host: isprime.poc.svc:8080
> User-Agent: curl/7.58.0
> Accept: */*
> Proxy-Connection: Keep-Alive
> Authorization: Bearer ...8vMTI3LjAuMC4xiJiYWNjNmNkYy05M...
> Content-Length: 1
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 1 out of 1 bytes
< HTTP/1.1 403 Forbidden
< Date: Sun, 11 Nov 2018 17:18:13 GMT
< Content-Length: 0
<
* Connection #0 to host 127.0.0.1 left intact

where gatekeeper explicitly forbids (observable by returned status code 403) the request from reaching the endpoint, the why is explained in gatekeeper’s log:

$ kubectl logs service-gatekeeper-7f... -n poc
...
{"level":"debug","ts":1541957308.256586,"caller":"keycloak-gatekeeper/session.go:51","msg":"found the user identity","id":"bacc6cdc-93f0-49d0-9407-15a716a5bcf2","name":"clarkkent","email":"","roles":"uma_authorization,service-gatekeeper:view,account:manage-account,account:manage-account-links,account:view-profile"}
{“level”:”warn”,”ts”:1541957308.2612984,”caller”:”keycloak-gatekeeper/middleware.go:307",”msg”:”access denied, invalid roles”,”access”:”denied”,”email”:””,”resource”:”/isprime”,”roles”:”service-gatekeeper:edit”}
...

now, since accesses are by default denied (by enable-default-deny: true), let’s check for a permitted request, with superman user:

$ curl -X POST ‘http://127.0.0.1:8080/auth/realms/justice-league/protocol/openid-connect/token' -H “Content-Type: application/x-www-form-urlencoded” -d ‘username=superman&password=<superman_password>&grant_type=password&client_id=service-gatekeeper’
{“access_token”:”…oxNTQxOTYwMzg3LCJpc…”,”expires…

now as superman, let’s make a request to the same endpoint:

$ curl -H ‘Authorization: Bearer …oxNTQxOTYwMzg3LCJpc…’ --proxy http://127.0.0.1:3000 http://isprime.poc.svc:8080/isprime -d ‘5’ -v
* Trying 127.0.0.1…
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 3000 (#0)
> POST http://isprime.poc.svc:8080/isprime HTTP/1.1
> Host: isprime.poc.svc:8080
> User-Agent: curl/7.58.0
> Accept: */*
> Proxy-Connection: Keep-Alive
> Authorization: Bearer …oxNTQxOTYwMzg3LCJpc…
> Content-Length: 1
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 1 out of 1 bytes
< HTTP/1.1 200 OK
< Access-Control-Allow-Origin: *
< Content-Length: 10
< Content-Type: text/plain; charset=utf-8
< Date: Sun, 11 Nov 2018 18:30:50 GMT
<
* Connection #0 to host 127.0.0.1 left intact
5 is prime

and now not only by the obvious 5 is prime response, we can check gatekeeper logs also shows us the desired behavior:

$ kubectl logs service-gatekeeper-7f... -n poc
...
{“level”:”debug”,”ts”:1541960663.8114207,”caller”:”keycloak-gatekeeper/session.go:51",”msg”:”found the user identity”,”id”:”e8928946–74b8–4dfa-97e3–736ccc59dd3e”,”name”:”superman”,”email”:””,”roles”:”uma_authorization,service-gatekeeper:edit,account:manage-account,account:manage-account-links,account:view-profile”}
{“level”:”debug”,”ts”:1541960663.8118446,”caller”:”keycloak-gatekeeper/middleware.go:337",”msg”:”accesspermitted to resource”,”access”:”permitted”,”email”:””,”expires”:23.188156032,”resource”:”/isprime”}
...

That’s it people, RBAC authorization service (Gatekeeper) provisioned based on a yaml configuration file (gatekeeper.yaml). I’ve been trying to replicate cloud experiences, such AWS Authorizer function and AWS IAM Roles to kubernetes, with help of JWT Tokens and Kubeless and finally accomplished.
Hope this helps others to manage security without having too much of a headache and improve time spent on tasks that can be managed by pipeline itself or related controllers.

Bonus:
Hardware consumption is a big pro on serverless/FaaS approach (with a little help of Go, which both demo function and gatekeeper are made of), i made a stress test and here are the results:

$ hey -H ‘Authorization: Bearer …’ -d ‘5’ -n 2000 -c 4 -x http://127.0.0.1:3000 http://isprime.poc.svc/isprime -cpus 1Summary:
Total: 0.6474 secs
Slowest: 0.0054 secs
Fastest: 0.0008 secs
Average: 0.0013 secs
Requests/sec: 3089.0472

Total data: 20000 bytes
Size/request: 10 bytes
Response time histogram:
0.001 [1] |
0.001 [1108] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
0.002 [746] |■■■■■■■■■■■■■■■■■■■■■■■■■■■
0.002 [99] |■■■■
0.003 [22] |■
0.003 [8] |
0.004 [4] |
0.004 [3] |
0.004 [5] |
0.005 [2] |
0.005 [2] |
Latency distribution:
10% in 0.0010 secs
25% in 0.0011 secs
50% in 0.0012 secs
75% in 0.0014 secs
90% in 0.0016 secs
95% in 0.0018 secs
99% in 0.0029 secs
Details (average, fastest, slowest):
DNS+dialup: 0.0000 secs, 0.0008 secs, 0.0054 secs
DNS-lookup: 0.0000 secs, 0.0000 secs, 0.0000 secs
req write: 0.0000 secs, 0.0000 secs, 0.0010 secs
resp wait: 0.0012 secs, 0.0007 secs, 0.0042 secs
resp read: 0.0000 secs, 0.0000 secs, 0.0009 secs
Status code distribution:
[200] 2000 responses
hardware consumption of poc namespace

Feel free to contact or comment on any doubts ✌️

--

--