Multi-tenant setup for service providers
Host agents for several customers on one installation, each as a tenant with its own namespaces, trust domain, signing key, audit ledger, admin login and quotas.
flowchart TB T[Tenant acme<br/>kubectl apply] --> O[Operator] O --> N[Labels namespaces<br/>maqpna.com/tenant=acme] O --> K[Signing key + trust domain<br/>Secret maqpna-tenant-keys] O --> B[BudgetPolicy tenant-acme<br/>quotas] O --> C[ConfigMap maqpna-tenants] C --> G[Gateway: tenant tokens only,<br/>tenant ledger, tenant admin roles] K --> I[Identity broker:<br/>signs tenant session tokens] G --> U[maqpna tenants list<br/>maqpna usage --tenant]
Goal#
Run one MAQPNA installation for several customers. Each customer is a tenant: it gets its own namespaces, workload identity trust domain, token-signing key, hash-chained audit ledger, admin login through its own identity provider, and quotas. This is MAQPNA's multi-tenant ISV mode, for service providers (neoclouds, colocation providers, telcos, MSPs and ISVs).
Prerequisites#
- A Kubernetes cluster with MAQPNA installed (see production install with Helm). Tenancy runs in the operator, identity broker and gateway; there is no local (
maqpna dev) mode for it. kubectlaccess to create cluster-scopedTenantobjects.- For tenant admin logins: an OIDC client (audience) per tenant in the tenant's identity provider.
- For a shared ledger across gateway replicas:
state.backend: postgres(see scaling and HA).
What a tenant isolates#
| Isolation | How |
|---|---|
| Namespaces | spec.namespaces. A namespace belongs to at most one tenant; the operator labels it maqpna.com/tenant=<name> |
| Identity | Session tokens for a tenant namespace must be signed with the tenant's key and carry a subject in the tenant's trust domain. A token signed with a tenant key is accepted only for that tenant's namespaces |
| Audit ledger | Every record of a tenant namespace is copied into the tenant's own hash-chained ledger (<tenantLedgerDir>/<tenant>/audit.jsonl, or the chain tenant/<name> with PostgreSQL), linked to the platform ledger by ext.platformSeq. With WORM enabled it ships under the tenant's prefix |
| Admin API | The tenant's own OIDC tokens (spec.admin) get the mapped roles only in the tenant's namespaces |
| MCP servers | An MCP server shared from one tenant's namespace is refused for other tenants unless the owner lists them in allowSharingWith. The Tenant then reports IsolationViolation |
| Quotas | spec.quotas becomes a generated BudgetPolicy named tenant-<name> on every tenant namespace |
Steps#
1. Check the chart settings#
Tenancy is on by default in the chart. These are the relevant values and their defaults:
operator:
tenantTrustDomainSuffix: "" # default tenant trust domain is <tenant>.<suffix>
gateway:
tenantsConfigMap: maqpna-tenants # rendered by the operator (key tenants.json)
identity:
tenants:
enabled: true # broker reads maqpna-tenants and Secret maqpna-tenant-keys
reload: 10s
The gateway reads the tenants file from /etc/maqpna/tenants/tenants.json and writes tenant ledgers under /var/lib/maqpna/audit/tenants. Set gateway.auditStorage.persistent: true (or the PostgreSQL state backend) so tenant ledgers survive pod restarts.
2. Create the tenant's namespaces and the Tenant#
Generate a starting manifest with maqpna init tenant acme, or start from this sample (config/samples/tenant-acme.yaml):
apiVersion: maqpna.com/v1alpha1
kind: Tenant
metadata:
name: acme
spec:
displayName: ACME GmbH
namespaces: [acme-prod, acme-dev]
trustDomain: acme.agents.example.eu
auditWormPrefix: tenants/acme/audit
admin:
oidcAudience: maqpna-admin-acme
roles:
admin: [acme-platform-admins]
auditor: [acme-dpo]
approver: [acme-approvers]
quotas:
maxConcurrentSessions: 50
perDayUSD: "200"
onExceed: deny
| Field | Meaning |
|---|---|
namespaces |
1–256 namespaces the tenant owns |
trustDomain |
Trust domain of the tenant's session identities (default <name>.<broker trust domain>) |
signingKeySecretRef |
{namespace, name, key} of a Secret with the tenant's Ed25519 key (PKCS#8 PEM, key key.pem by default). When unset, the operator generates one. Use your own key when the tenant must hold it |
auditWormPrefix |
WORM object-key prefix of the tenant's ledger (default tenants/<name>/audit) |
admin.oidcIssuer, admin.oidcAudience |
The tenant's admin tokens. The audience must differ from the platform admin audience; the issuer defaults to the gateway's |
admin.roles |
Gateway role (admin, auditor, approver, viewer, killswitch) to groups or user:<name> of the tenant's identity provider |
quotas |
maxConcurrentSessions, perSessionUSD, perDayUSD, perMonthUSD, currency (must equal the gateway's budget currency) and onExceed (deny, require_approval or alert) |
allowSharingWith |
Tenants that may use MCP servers shared from this tenant's namespaces ("*" for any) |
Validate it offline, then apply it:
maqpna validate -f tenant-acme.yaml # 1 file(s), 1 object(s): 0 error(s), 0 warning(s)
kubectl create namespace acme-prod
kubectl create namespace acme-dev
maqpna apply -f tenant-acme.yaml
3. Check that the operator reconciled it#
kubectl get tenants
kubectl get tenant acme -o yaml
The status shows keyId (the RFC 7638 thumbprint of the tenant's signing key), the trustDomain in effect, the namespaces it owns, the generated budgetPolicy, any isolationViolations, and the conditions Ready, NamespacesOwned, KeyReady and Isolated.
4. List tenants at the gateway#
maqpna tenants list
The table has the columns NAME, NAMESPACES, TRUST DOMAIN, KEY ID, HEAD SEQ and HEAD HASH; the head is the tenant ledger's. On an installation without tenants (output from a local run):
No tenants
5. Give the tenant its own admin login#
Each tenant's people log in with their own identity provider. Point the CLI at the gateway and request a token for the tenant's audience:
maqpna login --gateway https://gateway.example.eu --issuer https://idp.acme.example --client-id maqpna-cli --audience maqpna-admin-acme
maqpna whoami
maqpna approvals list
A tenant approver sees and decides only approvals in acme-prod and acme-dev. The tenant's auditor can export the tenant's evidence (see audit ledger and evidence).
6. Meter and bill each tenant#
Usage buckets and reports group usage by tenant through Tenant.spec.namespaces:
maqpna usage buckets --from 24h --tenant acme
maqpna usage report --period 2026-10 --tenant acme --key file:///etc/maqpna/keys/checkpoint.pem --out reports/acme/
See usage metering and billing for signing, price files and verification.
Verify#
kubectl get tenant acme -o jsonpath='{.status.conditions}'showsReady,KeyReadyandIsolatedasTrue.kubectl get ns acme-prod --show-labelsshowsmaqpna.com/tenant=acme.maqpna tenants listshowsacmewith a key ID and a growingHEAD SEQafter the tenant's agents make calls.- A session token minted for another tenant, or by the platform key, is refused in
acme-prod.
Troubleshooting#
| Symptom | Cause | Fix |
|---|---|---|
Tenant not Ready, NamespacesOwned false |
A namespace is already owned by another tenant, or does not exist | Create the namespace, or remove it from the other tenant |
| Calls in a tenant namespace denied with an identity error | The token was signed by the platform key, or its trust domain is not the tenant's | Check that identity.tenants.enabled is true and that the broker loaded maqpna-tenant-keys |
isolationViolations lists an MCP server |
A tenant uses an MCP server shared from another tenant | Add the consumer to the owner's allowSharingWith, or give the tenant its own MCP server |
maqpna usage report --tenant acme reports zero |
Run with --no-sessions, so tenants were not read |
Run with cluster access |
| Budget currency error on the generated BudgetPolicy | quotas.currency differs from the gateway's budget currency |
Use the gateway's currency; amounts are never converted |
Next steps#
- Usage metering and billing: signed per-tenant usage reports.
- Licensing: the Operator edition and tenant limits.
- Sovereignty and confidential tiers: per-tenant keys you or your customer hold.
- Command reference:
maqpna tenants list,maqpna usage buckets.