MAQPNADocs

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.
  • kubectl access to create cluster-scoped Tenant objects.
  • 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}' shows Ready, KeyReady and Isolated as True.
  • kubectl get ns acme-prod --show-labels shows maqpna.com/tenant=acme.
  • maqpna tenants list shows acme with a key ID and a growing HEAD SEQ after 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#