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.