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:
-
scripts/bootstrap-climascore-catalog.sh(one-shot, outside the chart) :- creates a PG role
trino_readonlywithGRANT 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
.propertieswithreadOnly=true
- creates a PG role
-
helm install akko-tenant-climascore helm/akko/charts/akko-tenant -f helm/examples/values-tenant-climascore.yamlinstalls the K8s isolation layer:- namespace
akko-climascore database.enabled: false(climascore owns its own PG)- S3 bucket
climascore-warehouse+ read-only access toakko-shared - Keycloak client-scope: token attribute
tenant=climascore - OPA data scope restricted to catalog
climascore-pgonly - quotas 2 CPU / 4 GiB RAM / 50 GiB storage / 20 pods
- namespace
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 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.