Deploying Multi-Tenancy
Standing up a multi-tenant SysManage deployment — the databases, OpenBAO, the migration chains, and tenant provisioning — for development and production.
What you will set up
A multi-tenant deployment introduces databases beyond the single application database of a normal install:
| Database | Partition | Purpose |
|---|---|---|
| Registry / bootstrap | registry_* | The tenant catalog and per-tenant database placements. This is the database sysmanage.yaml points at. |
| Shared | shared_* | Canonical reference data identical for every tenant (collapses onto the bootstrap database until the dedicated shared engine is configured). |
| Tenant (one per tenant) | unprefixed | Each tenant's host-scoped operational data, in its own database. |
Three things make this work: PostgreSQL (one or more instances), OpenBAO (to broker per-tenant database credentials), and the licensed multitenancy engine (the routing logic). Without the engine loaded, a deployment configured for multi-tenancy fails loudly with a licensing error rather than misrouting.
Development setup
The fastest way to develop against multi-tenancy locally. By default a dev checkout runs collapsed — multi-tenancy off, all three migration chains applied to one database — which is the normal single-tenant experience. Turn it on explicitly:
-
Install the dev environment and PostgreSQL as in the Server Installation guide (clone,
make install-dev, create thesysmanagerole and database). -
Enable multi-tenancy in
sysmanage-dev.yaml. Point the bootstrap/registry block at your registry database, turn the feature on, and configure the local OpenBAO (the dev config already ships avault:block):registry: host: localhost port: 5432 name: sysmanage_registry user: sysmanage password: DEV_Change_Me_ABC123! multitenancy: enabled: true self_service_provisioning: true vault: # OpenBAO brokers per-tenant database credentials url: http://localhost:8200 bootstrap: config_file: ./openbao.hcl data_path: ./data/openbao -
Run the migrations.
make migratedrivesscripts/sysmanage_migrate.py— the same tool operators run in production. With multi-tenancy on it automatically starts OpenBAO for the per-tenant fan-out, applies the registry, shared, and tenant chains, then stops OpenBAO again:make migrate -
Provision the bootstrap provisioner identity — a scoped Postgres role plus OpenBAO policy so the server can create tenants without holding superuser:
make provision-bootstrap ARGS='--bao-token $BAO_TOKEN' -
Start the stack and create a tenant.
make start, then create a tenant from the control-plane UI (Settings → Tenants). Withself_service_provisioningon, the server provisions the tenant's database and OpenBAO credentials for you. -
Fan the tenant chain out to the newly created tenant database(s):
make migrate-tenants
scripts/buildMultiTenantTestNetwork.sh, which provisions separate VMs for the registry, shared, and per-tenant databases plus a control plane — a faithful multi-database environment for exercising real routing instead of the collapsed single-database dev default.
Production setup
Production follows the same shape, with real databases, a hardened OpenBAO, and least-privilege provisioning.
-
Provision the registry database and a login role. On your PostgreSQL instance, create the
sysmanagerole and the registry database it owns (see the Server Installation database step). The server connects as this role; it is not a superuser. -
Stand up OpenBAO and configure its database secrets engine so it can issue short-lived, per-tenant PostgreSQL credentials. OpenBAO is the broker that keeps the server from ever holding a long-lived, shared database password. (Build and start it with the repository's
scripts/build-openbao.shandscripts/start-openbao.sh, or your own managed instance.) -
Configure
/etc/sysmanage.yaml— point the registry block at the registry database, enable multi-tenancy, and configure the vault:registry: host: db-registry.internal port: 5432 name: sysmanage_registry user: sysmanage password: <from your secret store> multitenancy: enabled: true self_service_provisioning: true vault: url: https://openbao.internal:8200 # token / auth configured per your OpenBAO setupLicensing: per-tenant routing requires the licensed multitenancy engine (theMULTITENANT_SAAStier). Without it, the server refuses to run multi-tenant rather than silently misrouting — this is intentional. -
Run the migrations with the operator tool.
make migrate(orscripts/sysmanage_migrate.pydirectly) applies the registry, shared, and tenant chains and shows per-database progress; with multi-tenancy on it brings OpenBAO up for the fan-out:make migrate -
Provision the least-privilege provisioner identity so self-service tenant creation works without cluster-superuser rights:
make provision-bootstrap ARGS='--bao-token $BAO_TOKEN' - Create your tenants from the control-plane UI (Settings → Tenants). Each tenant gets its own database and its own OpenBAO-brokered credentials. Place each tenant's database where it needs to live — by host, region, or jurisdiction.
-
Fan the tenant chain out to every provisioned tenant database (OpenBAO must be running):
make migrate-tenants -
Bind hosts to tenants. Issue a tenant-scoped enrollment token per tenant (Tenants → token), drop it into that host's agent configuration under
security.enrollment_token, and register the agent. From then on the host's data lands in its tenant's database automatically.
Migrations in production
Schema changes use an expand-contract discipline so a migration is always safe to apply while the previous version of the server is still running. Each partition (registry, shared, tenant) has its own linear, idempotent Alembic chain with a separate version table, and the operator tool applies them across the bootstrap database and every tenant database, reporting progress. The same sysmanage_migrate.py you run in development is what runs in production — there is no separate, riskier production path.
Verify routing
Confirm a bound host's data really lands in its tenant database rather than the bootstrap one — switch the active tenant in the UI and compare host counts, or query the tenant databases directly:
psql 'postgresql://sysmanage@db-tenant-a:5432/tenant_a' -c 'SELECT count(*) FROM hosts;'
psql 'postgresql://sysmanage@db-tenant-b:5432/tenant_b' -c 'SELECT count(*) FROM hosts;'
Each tenant's hosts appear only in that tenant's database. If the router cannot resolve a tenant for a piece of work, it logs loudly with full context rather than guessing — check the server logs.
Related documentation
🏗️ Architecture
The control plane, partitions, routing seam, and topology diagram.
Multi-tenancy architecture →🔐 Security
Why per-database isolation and OpenBAO credentials are hard to attack.
Multi-tenant security →🖥️ Server Installation
The base single-tenant install: prerequisites, database setup, and configuration.
Server installation →