Skip to content

Multi-tenancy

AKKO supports running multiple isolated tenants on a single cluster. A tenant can be a customer, a department, an internal project, or a third-party pilot (climascore is the reference simulation — see below).

Each tenant gets:

Layer Isolation primitive
Compute Dedicated Kubernetes namespace akko-<id>, enforced PSS restricted, ResourceQuota (CPU/RAM/storage/pods)
SQL data Dedicated PostgreSQL database + user inside akko-postgresql-data (or a standalone PG instance when tenant.database.dedicatedInstance=true)
Object storage Dedicated S3 bucket <id>-warehouse + IAM policy tenant-<id> + optional additional read-only buckets
Lakehouse Dedicated Iceberg namespace <id>.* on Polaris
Identity Either a full Keycloak realm (tenant.keycloak.mode=realm) or a dedicated client-scope with a tenant attribute added to tokens (tenant.keycloak.mode=client-scope)
Authorisation OPA ConfigMap fragment labelled akko.io/opa-tenant=<id> — loaded live by the OPA watcher sidecar, no rolling restart
Audit JSON lifecycle event emitted at each install/upgrade, scraped into logs layer / logs layer

Provision a tenant

Tenants are provisioned via the akko-tenant sub-chart (helm/akko/charts/akko-tenant). One Helm release per tenant:

helm install akko-tenant-<id> helm/akko/charts/akko-tenant \
  --namespace akko \
  -f helm/examples/values-tenant-<id>.yaml

All bootstrap steps (PostgreSQL database, S3 bucket, Keycloak client-scope, OPA policy fragment, audit event) run as idempotent post-install Helm hooks. Re-running the command is safe — existing resources are detected and reused.

Model example: climascore (first-client simulation)

Climascore is a third-party project that already owns its PostgreSQL database. We want AKKO users to federate climascore data through Trino read-only, without ever writing to the source. The provisioning is split in two:

  1. scripts/bootstrap-climascore-catalog.sh (one-shot, outside the chart) :

    • creates a PG role trino_readonly with GRANT SELECT
    • verifies that any write attempt is denied
    • computes an MD5 checksum of the source DB before and after, aborts if the data changed (defence-in-depth)
    • stores the credentials in K8s Secret climascore-pg
    • generates the Trino catalog .properties with readOnly=true
  2. helm install akko-tenant-climascore helm/akko/charts/akko-tenant -f helm/examples/values-tenant-climascore.yaml installs the K8s isolation layer:

    • namespace akko-climascore
    • database.enabled: false (climascore owns its own PG)
    • S3 bucket climascore-warehouse + read-only access to akko-shared
    • Keycloak client-scope: token attribute tenant=climascore
    • OPA data scope restricted to catalog climascore-pg only
    • quotas 2 CPU / 4 GiB RAM / 50 GiB storage / 20 pods

Adding a user to a tenant

Client-scope mode (default):

# From Keycloak admin UI, or via the admin REST API:
# Set user attribute `tenant = <id>` on the relevant user(s)
# then ensure the optional scope `tenant-<id>` is attached to the cockpit client.

The cockpit, Trino, Superset, OpenMetadata and MLflow all see a tenant claim in the user's JWT; OPA and the Catalog Manager Pro enforce that the user only sees resources tagged with that tenant.

Realm mode: the tenant users live in a dedicated realm. Map the realm as an identity provider federation in the main akko realm, or require users to log in on the dedicated realm only.

RBAC interaction

The existing Keycloak RBAC roles (akko-admin, akko-engineer, akko-analyst, akko-steward, akko-viewer) still apply inside each tenant. The tenant attribute is orthogonal to the role: an akko-analyst in tenant A sees analyst-level access on tenant A's data and nothing on tenant B's data.

Cross-tenant administration requires the akko-admin role explicitly without a tenant attribute (platform administrator).

Deleting a tenant

helm uninstall akko-tenant-<id> -n akko

helm uninstall removes the namespace (with every quota and OPA fragment) but leaves the PostgreSQL database and S3 bucket intact by default, so data is not deleted accidentally. To reclaim those resources:

# Connect to akko-postgresql-data and run:
DROP DATABASE "<id>_db";
DROP ROLE "<id>_user";

# Connect with mc and run:
mc rb --force akko/<id>-warehouse
mc admin policy rm akko tenant-<id>

Audit events

Each install/upgrade emits one JSON line to stdout, collected by log shipper and queryable in Dashboards/logs layer:

{
  "audit_type": "TENANT_LIFECYCLE",
  "decision": "PROVISIONED",
  "tenant": "climascore",
  "display_name": "Climascore (first client simulation)",
  "owner_email": "owner@climascore.local",
  "namespace": "akko-climascore",
  "release": "akko-tenant-climascore",
  "revision": "1",
  "hook": "install",
  "timestamp": "2026-04-19T14:37:02Z"
}

Testing isolation

# 1. Provision a pilot tenant
helm install akko-tenant-pilot helm/akko/charts/akko-tenant \
  -n akko -f my-pilot-values.yaml

# 2. Verify cross-tenant isolation
kubectl -n akko-climascore auth can-i get pods --as=system:serviceaccount:akko-pilot:default
# expected: no

Full integration tests live in tests/integration/test_multi_tenant_isolation.py.