Deploy Ory Kratos on Kubernetes

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.

Was this page helpful?

Most Viewed Articles