Article

Published: August 24, 2022

Dexter Codo

Edited: September 05, 2022

Deploy Ory Kratos on Kubernetes

Discover a nice way to deploy Ory Kratos on Kubernetes.

User management is an essential module required by almost any application, and while it’s easy to store an email along with an encrypted password in a database table, such implementation may not shield your application from common security pitfalls.

Throughout my career, I’ve developed multiple identity management modules that address the unique needs of different projects, but I’ve always had the dream of developing a reusable module that can fit into any given situation as well as cater for future needs. It is a dream that never came true until recently. 

Of late, I stumbled upon an open source project by the Ory team called Kratos. Ory offers a complete suite of authentication and authorisation modules that can complement your application and provide best practices when it comes to securing your application.

In my opinion, Ory’s documentation is written under the assumption that its readers have a complete understanding of security best practices, and hence may not be beginner-friendly and can take a few days of trial and error (with some frustration) to get started. I hope this article, which is written based on my opinion and experience, can simplify a thing or two for you.

Note: This article does not explain how Kratos works for I’ve written a separate article on that. Instead, I’m going to focus on the steps needed to deploy Kratos to a Kubernetes environment.

Prerequisites

  • A Kubernetes cluster running somewhere either locally or on a cloud
  • kubectl cli configured and ready to use
  • A database server. For this demo I’ll be using PostgreSQL
  • A database; for this demo I’ll name my database kratos-staging
  • A valid domain name
  • Optional, an SSL certificate (letsEncrypt can be used)

Steps to deploy Kratos on Kubernetes

To deploy Kratos, I’m going to first demonstrate the creation of Kubernetes yaml files, then applying them in the correct order. So let’s get coding.

Step 1: 

To begin, Kratos requires an identity schema in the form of a JSON file that will be used to create the database. User profile is stored in the form of traits like first_name, last_name, date_of_birth and so on. Email addresses and phone numbers that that can be verified can also be stored as verifiable_addresses. 

I will be using the generic schema provided by the starter guide to create a config map on Kubernetes and save it to a file. Let's call it kratos-identity-schema.yml

apiVersion: v1 kind: ConfigMap metadata: name: identity-schema-config namespace: kratos-staging data: identity.schema.json: | { "$id": "https://schemas.ory.sh/presets/kratos/quickstart/email-password/identity.schema.json", "$schema": "http://json-schema.org/draft-07/schema#", "title": "Person", "type": "object", "properties": { "traits": { "type": "object", "properties": { "email": { "type": "string", "format": "email", "title": "E-Mail", "minLength": 3, "ory.sh/kratos": { "credentials": { "password": { "identifier": true } }, "verification": { "via": "email" }, "recovery": { "via": "email" } } }, "name": { "type": "object", "properties": { "first": { "title": "First Name", "type": "string" }, "last": { "title": "Last Name", "type": "string" } } } }, "required": [ "email" ], "additionalProperties": false } } }

Step 2: 

The behaviour of Kratos should be configured, including the type of database you’d be using and the connectivity credentials. This can be done using one of two ways, a yaml file or environment variables. I personally prefer to let static configuration be left in a yaml file, while having more sensitive information like DB credentials be injected at run time as an env var. 

First, let’s create a file and name it kratos-config.yml which contains a config map as follows:

apiVersion: v1 kind: ConfigMap metadata: name: kratos-config namespace: kratos-staging data: kratos.yml: | version: v0.10.1 dsn: memory dev: false serve: public: base_url: https://api.example.com/kratos/ cors: enabled: true allowed_origins: - https://app.example.com - https://*.example.com - https://example.com admin: base_url: http://kratos-service:444/ selfservice: default_browser_return_url: https://app.example.com/ allowed_return_urls: - http://app.example.com methods: password: enabled: true flows: error: ui_url: http://app.example.com/error settings: ui_url: http://app.example.com/settings privileged_session_max_age: 15m recovery: enabled: true ui_url: http://app.example.com/recovery verification: enabled: true ui_url: http://app.example.com/verification after: default_browser_return_url: http://app.example.com/ logout: after: default_browser_return_url: http://app.example.com/login login: ui_url: http://app.example.com/login lifespan: 10m registration: lifespan: 10m ui_url: http://app.example.com/registration after: password: hooks: - hook: session log: level: debug format: text leak_sensitive_values: false secrets: cookie: - REPLACE_ME_WITH_A_TOKEN cipher: - REPLACE_ME_WITH_A_TOKEN ciphers: algorithm: xchacha20-poly1305 hashers: algorithm: bcrypt bcrypt: cost: 8 identity: default_schema_id: default schemas: - id: default url: file:///etc/config/kratos/identity.schema.json courier: smtp: connection_uri: smtps://test:test@mailslurper:1025/?skip_ssl_verify=true

Now, we can move on to the more sensitive information and treat them as a secret, so let’s create a file and name it kratos-env.yml

apiVersion: v1 kind: Secret metadata: name: kratos-env namespace: kratos-staging type: Opaque data: DSN: >- cHJvZHVjdGlvbg== ENV: cHJvZHVjdGlvbg== LOG_LEVEL: dHJhY2U= SQA_OPT_OUT: dHJ1ZQ==

Step 3:

At this point, Kratos has nearly everything it needs to run. But before that, we need to create the database using migrations based on the schema we had defined in Step 1. For that, we can create a Kubernetes job to run once. So let’s create a file and call it kratos-migration-job.yml

apiVersion: batch/v1 kind: Job metadata: name: kratos-migration namespace: kratos-staging spec: backoffLimit: 0 ttlSecondsAfterFinished: 150 parallelism: 1 template: spec: restartPolicy: Never containers: - name: kratos-migration image: oryd/kratos:v0.10.1 command: ["kratos", "-c", "/etc/config/kratos/kratos.yml", "migrate", "sql", "-e", "--yes"] imagePullPolicy: IfNotPresent envFrom: - secretRef: name: kratos-env volumeMounts: - name: kratos-identity-schema mountPath: /etc/config/kratos/identity.schema.json subPath: identity.schema.json - name: kratos-config mountPath: /etc/config/kratos/kratos.yml subPath: kratos.yml volumes: - name: kratos-identity-schema configMap: name: identity-schema-config defaultMode: 420 - name: kratos-config configMap: name: kratos-config defaultMode: 420 terminationGracePeriodSeconds: 10

Step 4:

We’re now ready to create the deployment file. This is the part that contains the actual pod that can be scaled. Create a file and name it kratos-deployment.yml. It should contain the following configuration:

apiVersion: apps/v1 kind: Deployment metadata: name: kratos namespace: kratos-staging labels: app: kratos spec: selector: matchLabels: app: kratos replicas: 1 revisionHistoryLimit: 1 strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0 template: metadata: labels: app: kratos spec: containers: - name: kratos image: oryd/kratos:v0.10.1 command: ["kratos", "-c", "/etc/config/kratos/kratos.yml", "serve"] imagePullPolicy: IfNotPresent envFrom: - secretRef: name: kratos-env ports: - containerPort: 4433 protocol: TCP - containerPort: 4434 protocol: TCP volumeMounts: - name: kratos-identity-schema mountPath: /etc/config/kratos/identity.schema.json subPath: identity.schema.json - name: kratos-config mountPath: /etc/config/kratos/kratos.yml subPath: kratos.yml volumes: - name: kratos-identity-schema configMap: name: identity-schema-config defaultMode: 420 - name: kratos-config configMap: name: kratos-config defaultMode: 420 restartPolicy: Always terminationGracePeriodSeconds: 30

Step 5:

By now we have everything we need for Kratos to be up and running, but we still do not have a way to access it. So, we need to create a Kubernetes service. This network service can be used by all other micro-services your application has to reach Kratos and validate sessions. Create a file and call it kratos-service.yml

apiVersion: v1 kind: Service metadata: name: kratos-service namespace: kratos-staging labels: app: kratos spec: type: ClusterIP ports: - name: https port: 443 targetPort: 4433 - name: http-admin port: 444 targetPort: 4434 selector: app: kratos

Step 6:

Step 1 to 5 allow Kratos to run inside Kubernetes and make it accessible internally. We now need a way to expose it publicly. We can use Ingress rules for that, assuming you’re using NGINX Ingress. I’m going to use a dummy domain name called example.com along with letsEncrypt for SSL encryption. 

Let’s create the final file name it kratos-ingress.yml that contains something like the following configuration:

apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: api-ingress namespace: kratos-staging annotations: kubernetes.io/ingress.class: "nginx" cert-manager.io/issuer: "letsencrypt-nginx" nginx.ingress.kubernetes.io/cors-allow-headers: >- Keep-Alive, User-Agent, X-Requested-With, Cache-Control, Accept, Content-Type, Authorization, X-Forwarded-For, Strict-Transport-Security, Cookie, X-Kratos-Authenticated-Identity-Id, X-CSRF-Token, X-Session-Token nginx.ingress.kubernetes.io/cors-allow-methods: GET, PUT, POST, DELETE, PATCH, OPTIONS nginx.ingress.kubernetes.io/cors-allow-origin: https://staging-app.foxq.io nginx.ingress.kubernetes.io/cors-expose-headers: >- Content-Type, Content-Length, Set-Cookie, Authorization, X-Session-Token, X-CSRF-Token, Cookie nginx.ingress.kubernetes.io/enable-cors: 'true' nginx.ingress.kubernetes.io/rewrite-target: /$2 nginx.ingress.kubernetes.io/ssl-redirect: 'true' spec: tls: - hosts: - api.example.com secretName: api-example-com rules: - host: api.example.com http: paths: - path: /kratos(/|$)(.*) pathType: Prefix backend: service: name: kratos-service port: number: 443

Step 7:

Finally, we’re ready to apply all the files we’ve created in the previous steps and get Kratos up and running.

Let’s apply the config maps and secret files.

kubectl apply -f kratos-identity-schema.yml kubectl apply -f kratos-config.yml kubectl apply -f kratos-env.yml

Next, apply the migration job.

kubectl apply -f kratos-migration-job.yml

Wait for the migration job to complete. In the meantime, we can apply the service and Ingress rules.

kubectl apply -f kratos-service.yml kubectl apply -f kratos-ingress.yml

Finally, now that we have created the database, we can apply the deployment file.

kubectl apply -f kratos-deployment.yml

Well done, you’ve now successfully deployed an instance of Kratos on Kubernetes that’s ready to be used in a real life application. 

Verifying the deployment

Before we end this tutorial, let’s run a quick test to verify that Kratos is in fact working. We can use a simple curl on a terminal to call the public APIs. I’m going to attempt to initiate a registration flow using the following command:

curl -v -s -X GET -H "Accept: application/json" https://api.example.com/kratos/self-service/registration/browser

You should receive a response similar to the following:

OUTPUT { "id": "ee88a7e6-5329-4be3-b104-88b10ca5845a", "type": "browser", "expires_at": "2022-08-22T14:05:55.90290929Z", "issued_at": "2022-08-22T13:55:55.90290929Z", "request_url": "http://dev-api.foxq.io/self-service/registration/browser", "ui": { ... } }

Final thoughts

Kratos embraces the principle of separation of concerns, which can be seen through the lack of user interface, unlike competitors such as KeyCloak. I find this particularly appealing. Regardless of the provider you’d end up using, you’re likely to implement your own UI to match your product branding. 

Kratos is written in GoLang, a high performing low-level language that is contained in a package under 20MB in size. It is perfect for deployment on a low cost environment especially when you’re just getting started with your product and server cost is something you’d want to keep low.

In this article, we’ve explored a good way to deploy Kratos on Kubernetes. The one thing to keep in mind is that it’s not recommended to expose its admin API publicly. To consume it, you should create your own service that interacts with the admin API internally through Kubernetes service, applying your own authorisation measures to it, to make sure only a system admin can access it.