Guide to integrating Tornjak with Keycloak for access control to SPIRE

Maia Iyer
Universal Workload Identity
7 min readDec 8, 2022

--

Simplified Tornjak-OAuth2.0 server integration architecture

This is a step-by-step guide to integrating Tornjak with Keycloak as an example OAuth2.0 server. For more background information, please see the following blogs:

  1. The introductory blog for an overview of the architecture for Tornjak with integrated IAM .
  2. Tutorial that shows step-by-step how to configure a Keycloak instance that can integrate with Tornjak. This instance satisfies all OAuth2.0 server requirements for Tornjak.

Today, we will focus on the configuration and deployment of Tornjak locally on Minikube and Docker to integrate with a separate OAuth2.0 server component.

Recall that Tornjak architecture has two main components: the frontend and the backend. Here is how the architecture will look as a whole when connected with the OAuth2.0 server:

More detailed architecture of Tornjak explained in our introductory blog

For more details on why this architecture was chosen, and how the information flow works, please see our introductory blog.

In order to integrate with the OAuth2.0 server, the frontend component needs to know where to obtain the access token, and the backend component needs to know the JSON Web Key Set to validate the authenticity of this token.

Requirements

Required Setup

We will be using Minikube and Docker for this tutorial, but the same principles apply when components deployed on the cloud. We assume the OAuth2.0 server is configured like the one in the previous post, which means the following:

  • The OAuth2.0 server implements the OAuth2.0 Standard Authorization Code Flow
  • It has two specific realm roles: the tornjak-admin-realm-role and the tornjak-viewer-realm-role to differentiate between user types. These roles are presented as claims in the access token given.

Required Information

We require two pieces of information to configure the backend:

  • A JSON Web Key Set (JWKS) where the public keys of the OAuth2.0 server (e.g., Keycloak) are exposed. This is essential for the Tornjak backend to be able to validate access tokens provided by any request.
  • A redirect URL for login. This will tell the frontend (or anyone calling the backend directly) where to login to receive an access token that the backend will accept. This functionality is really only useful for confidential client applications, which we will see in an upcoming blog.

If you followed the tutorial in the previous post to set up Keycloak locally, for this tutorial, the relevant information is as follows:

(NOTE: If you used a different realm name, you will need to change tornjak to this realm name. The same goes for client_id of Tornjak-React-auth. )

Deploying SPIRE and Tornjak

We currently have a Tornjak quickstart tutorial in our Github documentation, which goes through step-by-step how to deploy SPIRE on Minikube and then how to configure and deploy Tornjak. Please follow the steps in the tutorial before continuing.

After this is done, we can secure the Tornjak backend very easily! We need only edit the configuration to have the backend require an access token signed by the trusted OAuth2.0 server.

Editing the Tornjak Config with IAM Integration

If you followed the tutorial, you will have created a basic ConfigMap for the Tornjak backend configuration. The only missing piece is an additional plugin defined in the configuration that enables User Management. This is easily added as follows:

# % cat tornjak-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: tornjak-agent
namespace: spire
data:
server.conf: |
server {
metadata = "insert metadata"
}

plugins {
DataStore "sql" {
plugin_data {
drivername = "sqlite3"
filename = "./agentlocaldb"
}
}

### BEGIN NEW AUTH PLUGIN SECTION ###
UserManagement "KeycloakAuth" {
plugin_data {
jwksURL = “<INSERT JWKS URL>”
redirectURL = “<INSERT REDIRECT URL>”
}
}
### END NEW AUTH PLUGIN SECTION ###
}

From the above ConfigMap, the main thing to note is the section that enables authorization, which is the section plugins>UserManagement. Please fill in the JWKS and redirect URL as obtained in the previous section. For a concrete example of the config for a local instance with notes on cloud deployment, please refer to this documentation. For more Tornjak configuration documentation, please refer to this documentation.

Deployment

Now we will redeploy the ConfigMap and reset the Tornjak agent. This involves applying the ConfigMap, then deleting the existing StatefulSet pod so that it restarts the Tornjak backend with the correct configuration:

% kubectl apply -f tornjak-configmap.yaml
configmap/tornjak-agent configured

% kubectl delete po -n spire spire-server-0
pod "spire-server-0" deleted

% kubectl get statefulset --namespace spire
NAME READY AGE
spire-server 1/1 5d1h

The next command exposes the backend API at https://localhost:10000.

% kubectl -n spire port-forward spire-server-0 10000:10000
Forwarding from 127.0.0.1:10000 -> 10000
Forwarding from [::1]:10000 -> 10000

Now, when we access the backend via a CURL request with no headers, we receive an error because the curl requires an Authorization header:

% curl http://localhost:10000/api/entry/list
Error authorizing request: Authorization header missing. Please obtain access token here: <your redirect URL>

This means our backend is secured!

Deploying the Frontend

If you followed the quickstart tutorial, you also may have started the Tornjak frontend locally at the very end. However, now if you access the same frontend, you will receive an error:

Error in a the browser when the frontend is not configured to obtain access tokens when calling a secured Tornjak backend

Again, this means our backend is deployed correctly, but the frontend, as deployed, does not currently obtain access tokens to make calls to the backend.

So next, we must restart the frontend with an extra parameter so it knows where to obtain the access tokens. To do so, we can run the following command which defines an additional environment variable for the container:

% docker run -p 3000:3000 \
-e REACT_APP_API_SERVER_URI='http://localhost:10000' \
-e REACT_APP_AUTH_SERVER_URI='http://localhost:8080' \
ghcr.io/spiffe/tornjak-fe:latest

> tornjak-frontend@0.1.0 start
> react-scripts --openssl-legacy-provider start

ℹ 「wds」: Project is running at http://172.17.0.3/
ℹ 「wds」: webpack output is served from
ℹ 「wds」: Content not from webpack is served from /usr/src/app/public
ℹ 「wds」: 404s will fallback to /
Starting the development server...

Compiled successfully!

You can now view tornjak-frontend in the browser.

Local: http://localhost:3000
On Your Network: http://172.17.0.3:3000

Note that the development build is not optimized.
To create a production build, use npm run build.

This command sets container environment variables to point to our Keycloak instance and the backend. It might take a couple minutes for the compilation to be successful.

This will start your application on a separate browser on port 3000. As before, go to your application at http://localhost:3000 and you should be redirected to the Keycloak login page. You should see something like this. And you should be able to see the application redirected to the Keycloak server (which serves as the Tornjak login page) as shown below.

Tornjak redirects to this login page at the OAuth2.0 server

Sign in with the username and password of a user you created as a Keycloak admin, and you should be able to access Tornjak either as a viewer or admin depending on the role associated with the user. This application will take care of creating and sending the required bearer token to the backend.

If signed in with admin user, you should be able to see the admin portal as below

Admin view in Tornjak browser

If signed in with viewer user, you should be able to see the viewer portal as below

Non-admin view in Tornjak browser

tl;dr — what did we just do?

First, given a correctly configured OAuth2.0 server, we did the Tornjak quickstart which did three things:

  1. Deployed SPIRE
  2. Created basic Tornjak Configuration
  3. Deployed Tornjak

Then we secured Tornjak by doing one thing to each Tornjak component:

  1. The Tornjak backend, a privileged application, was secured by altering the Tornjak configuration to require access tokens and redeploying.
  2. The Tornjak frontend, an unprivileged application, is configured to require user sign-in to obtain access tokens to make calls to the secure Tornjak backend.

Recommendations for this feature

It is important to note that the tutorial here is only to get started with secured Tornjak on local deployments. In order to ensure controlled access to SPIRE on cloud deployments, the secured Tornjak backend should be the only public access point to a SPIRE server.

We also recommend secure communication of access tokens by using TLS or mTLS to communicate with the OAuth2.0 server instead of the unsecured http protocol.

Conclusion — Contribute!

Tornjak is an open-source project actively under development. We have several exciting, relevant features coming up! If you are interested in helping make management of workload identities easier or simply have feedback, we are always looking for more contributors. For more information, please checkout the Tornjak github repo, or if you simply want to chat with us, we have a channel #tornjak within the CNCF SPIFFE community where you are free to reach out to us!

Maia Iyer — Tornjak Contributor (Medium: Maia Iyer, SPIFFE Slack: @Maia Iyer)
Mohammed Abdi — Tornjak Contributor (Medium: Mohammed M. Abdi, SPIFFE Slack: @Mohammed Abdi)
Mariusz Sabath — Tornjak Maintainer (Medium: Mariusz Sabath, SPIFFE Slack: @Mariusz Sabath)

--

--