
Ory Keto is an open-source permission and access control server that implements Relationship-Based Access Control (ReBAC) modeled on Google's Zanzibar authorization system. Instead of hardcoding access rules, Keto stores relationships as relation tuples and answers permission questions through a dedicated Application Programming Interface (API). This approach scales from direct ownership checks to complex nested permissions like group membership, without changing application code.
This article explains how to deploy Ory Keto on a Linux server using Docker Compose, PostgreSQL, and Traefik as a reverse proxy that provides automatic HTTPS through Let's Encrypt. It covers defining a permission model with the Ory Permission Language (OPL), binding Keto's read, write, and metrics APIs to the loopback interface, and exposing only a permission-enforcing application over HTTPS, so that no Keto API is reachable from the public internet.
Before you begin, you need to:
keto.example.com).Ory Keto requires a project directory that holds the server configuration, the permission model, a small backend application, and an environment variables file. Database credentials stay in the environment variables file to keep them out of the main configuration.
Create the project directory with all required subdirectories.
The command creates four subdirectories:
config/: Stores the Keto configuration and the permission model.reference-app/: Stores a Node.js backend that enforces permissions through the Keto read API.data/postgres/: Persists PostgreSQL database files across container restarts.letsencrypt/: Stores the Traefik ACME certificate files for automatic HTTPS renewal.Navigate to the project directory.
Create the Keto configuration file.
Add the following content.
Save and close the file. The namespaces.location setting points Keto to the permission model inside the container, and the serve block defines the read, write, and metrics APIs. Each API binds to 0.0.0.0 inside the container so Docker can forward it. The loopback restriction is applied later in the Docker Compose port mapping.
The Data Source Name (DSN) that connects Keto to PostgreSQL is not stored in this file. It is passed through the DSN environment variable in the Docker Compose stack to keep the database password out of the configuration file.
Create the permission model file.
Add the following content.
Save and close the file. This model defines the User, Group, and Document namespaces. A document has owners and viewers, where viewers accepts either a direct user or a group's members, which grants an entire group view access with one relationship. The permits block grants view to owners and viewers, and edit to owners only. For the full syntax, see the Ory Permission Language reference.
Create the package descriptor for the backend resource server. This Node.js service demonstrates how an application calls the Keto read API instead of hardcoding access rules.
Add the following content.
Save and close the file.
Create the backend service file.
Add the following content.
Save and close the file. This Express backend is a policy enforcement point: on every request to /files/:id, it asks the Keto read API whether the subject can view the document and returns the content only when Keto reports "allowed": true. The X-User-Id header stands in for a subject already authenticated by an identity provider such as Ory Kratos or Ory Hydra. The canView function fails closed: if the read API is unreachable or returns an error, it denies access rather than leaking the file.
The service receives only KETO_READ_URL and never credentials for the write API, so it can check permissions but cannot create or delete relationships.
Create the environment variables file.
Add the following content. Replace keto.example.com with your domain name, admin@example.com with your email address for Let's Encrypt, and EXAMPLE_DB_PASSWORD with a strong, unique database password.
Save and close the file.
Verify the complete directory structure.
The output lists the following files:
Docker Compose orchestrates the Keto server, PostgreSQL database, backend application, and Traefik reverse proxy as a single deployment. Traefik obtains and renews a Let's Encrypt certificate automatically, so no manual certificate steps are required.
Create the Docker Compose file.
Add the following content.
Save and close the file. Five services make up the stack:
LETSENCRYPT_EMAIL. With exposedbydefault=false, it routes only containers that set traefik.enable=true, which is keto-app alone./var/lib/postgresql rather than the /var/lib/postgresql/data path used by earlier versions.0. The keto service waits for it to complete before starting.4466), write (4467), and metrics (4468) ports are all bound to 127.0.0.1, so no API is reachable from outside the server. It carries no Traefik labels, so Traefik never exposes it. Other containers on keto-network still reach the read API at http://keto:4466, which is how the application performs checks.Host(keto.example.com) requests under the /files path to it on port 3000, making it the single public entry point. Most services also set deploy.resources.limits to cap CPU and memory usage.
The PostgreSQL DSN uses sslmode=disable because the connection travels over the internal Docker bridge network. If you move PostgreSQL to a separate host, change sslmode=disable to sslmode=require and configure TLS on the database server.
Start all services in detached mode.
Wait about 30 seconds for the database to initialize and migrations to complete.
Check the status of all containers.
The output looks similar to the following. The keto-migrate container shows Exited (0), and all other containers show Up.
Confirm the server started by checking the write API health endpoint on the loopback interface.
A successful response returns {"status":"ok"}. If it fails, the database may still be initializing; wait a few seconds and run docker compose restart keto. For more information on managing Docker Compose stacks, see the How To Use Docker Compose article.
These checks confirm that Keto's APIs stay private on the loopback interface and that Traefik exposes only the enforcement application over HTTPS. You then configure a firewall to restrict inbound traffic.
Verify that the read API is healthy on the loopback interface.
A successful response returns {"status":"ok"}.
Confirm the public application responds over HTTPS. Replace keto.example.com with your domain. Traefik obtains the certificate on the first HTTPS request, which can take a few seconds. Because the request omits the X-User-Id header, the backend returns 401, which confirms that Traefik terminates TLS and routes to the application.
The output is 401.
Verify that the metrics endpoint is accessible on the loopback interface.
The output contains Prometheus-formatted metrics that a monitoring system can scrape.
Allow Secure Shell (SSH) traffic through the Uncomplicated Firewall (UFW) so you keep remote access after enabling the firewall.
Allow HTTP traffic on port 80.
Allow HTTPS traffic on port 443.
Docker modifies iptables rules directly, which can bypass UFW. In this deployment, only the Traefik container publishes ports to all interfaces (80 and 443), and Keto's read, write, and metrics APIs bind to 127.0.0.1. Even if Docker bypasses UFW, no Keto API is exposed on a public interface.
Enable UFW. When prompted, type y to confirm.
Verify the firewall rules.
The output lists ports 22/tcp, 80/tcp, and 443/tcp with an ALLOW action for both IPv4 and IPv6.
Confirm the read API is not reachable from outside the server. From your local machine, replace YOUR_SERVER_IP with your server's IP address.
The request times out or is refused.
Confirm the write API is not reachable either.
The request times out or is refused, confirming that neither the read API nor the write API is reachable from the internet.
With Keto running, you populate the permission model with relation tuples through the write API, evaluate permissions through the read API, and confirm that the backend application enforces those permissions. Because the read and write APIs bind to the loopback interface, run the management and check commands on the server. For the full endpoint reference, see the Ory Keto REST API documentation.
Send a batch of relation tuples to the write API on 127.0.0.1:4467.
The command inserts three relationships: alice owns the readme document, bob is a member of the engineering group, and the engineering group's members are viewers of the roadmap document. On success, the write API returns an empty body with HTTP status 204 No Content.
Confirm the tuples were stored by listing the Document namespace through the read API.
The response lists the readme owner and the roadmap viewer subject set.
Check whether alice can view the readme document.
alice is an owner, so the response returns {"allowed":true}.
Check whether bob can view the roadmap document. bob has no direct relationship to roadmap, but is a member of the engineering group, which is a viewer.
Keto resolves the subject set and returns {"allowed":true}. This is the core strength of ReBAC: a single group membership grants access without a direct relationship to the document.
Check whether bob can edit the roadmap document. bob is a viewer through group membership, but viewers cannot edit.
The edit permission requires ownership, so the response returns {"allowed":false}.
Inspect the full set of subjects that hold the viewers relation on the roadmap document.
The response contains a nested JSON tree with the engineering group's members subject set as a child node, which helps you audit who has access and why.
The keto-app service is the only publicly exposed component, and it reuses the relationships created earlier. These requests confirm that a real backend enforces the permission model through the Keto read API instead of embedding access rules in code. Replace keto.example.com with your domain.
Request the readme document as alice, who owns it.
Keto confirms that alice is an owner, so the backend returns the file content:
Request the roadmap document as bob, who inherits view access through the engineering group.
The response returns the file content, confirming that the backend enforces group-inherited permissions even though server.js contains no group-specific logic.
Request the roadmap document as carol, who has no relationship to the document or the group.
The response returns 403 Forbidden. Because the permission logic lives entirely in the OPL model and the Keto database, changing who can view roadmap never requires modifying or redeploying server.js.
Request a file without the X-User-Id header.
The response returns 401 Unauthorized. In production, an upstream identity provider or API gateway sets this header only after verifying the caller's identity.
You have deployed Ory Keto on a Linux server with Docker Compose, PostgreSQL, and Traefik for automatic HTTPS through Let's Encrypt. You have defined a Relationship-Based Access Control model with the Ory Permission Language, secured the deployment by binding Keto's APIs to the loopback interface and exposing only a permission-enforcing application over HTTPS, and verified the model end to end by creating relationships and running checks through both the read API and the backend. For more information, see the official Ory Keto documentation.
0 Comments
Be the first to comment and share your perspective with the community.