Multi-Tenancy
Multi-Tenancy
Nexus GSLB supports multiple isolated tenants on a single instance. Each tenant has its own pools, members, services, health checks, and geo rules; API calls from one tenant cannot read or modify another tenant's resources.
License: Creating additional tenants requires the enterprise tier. All installations ship with one default tenant usable on any tier. Contact licensing@gslb.nexus to upgrade.
Concepts
| Term | Description |
|---|---|
| System admin | The operator who controls the Nexus GSLB instance. Either the holder
of the GSLB_API_KEY env var, or a
logged-in user with the tenant_admin role on the
"default" tenant. Can manage the tenant list and has access
to all "default" tenant resources. |
| Tenant | An isolated account. Identified by a randomly-generated API key. Cannot see or modify other tenants' resources. |
| Default tenant | When multi-tenancy is not in use, all resources belong to
"default". Existing single-tenant deployments continue to
work unchanged. |
Authentication flow
Request → authMiddleware
├─ /api/v1/health → exempt (no auth required)
├─ GSLB_API_KEY unset → treated as default-tenant admin (system admin)
├─ Bearer <token> == GSLB_API_KEY → system admin, default tenant
├─ session (cookie/Bearer) → user's tenant + role; tenant_admin on the
│ "default" tenant is promoted to system admin
├─ SHA-256(Bearer <token>) matches tenants.api_key_hash → that tenant (operator perms)
└─ anything else → 401 Unauthorized
System-admin status is stored in request context
(isSystemAdmin(tenantID, role) ==
tenantID=="default" && role=="tenant_admin") and
checked in tenant-management endpoints. It is also surfaced on
GET /api/v1/auth/me as isSystemAdmin so the
WebUI can gate the Tenants page.
Tenant management (system-admin-only)
All three endpoints require system admin: either a
Bearer token matching GSLB_API_KEY, or a logged-in
tenant_admin on the "default" tenant. A
tenant_admin on any other tenant is scoped to its own
tenant and cannot manage the tenant list (it receives
403 admin access required).
Create a tenant
POST /api/v1/tenants
Authorization: Bearer <admin-key>
Content-Type: application/json
{
"name": "acme",
"rpsLimit": 500
}
Response (201):
{
"tenant": { "id": "abc123", "name": "acme", "rpsLimit": 500, "createdAt": 1714000000 },
"apiKey": "a1b2c3d4...64hexchars"
}apiKey is shown once only — it is not
stored. The SHA-256 hash of the key is stored in the
tenants table. If the key is lost, delete and re-create the
tenant.
List tenants
GET /api/v1/tenants
Authorization: Bearer <admin-key>
Returns the tenant list without API keys (keys are never retrievable after creation).
Delete a tenant
DELETE /api/v1/tenants/{id}
Authorization: Bearer <admin-key>
Deletes the tenant record. The tenant's pools, services, etc. remain
in the database (the tenant_id column references are not
cascade-deleted). Prune orphaned resources manually if needed.
Using a tenant API key
Pass the tenant's API key as a Bearer token. All CRUD operations are automatically scoped to that tenant:
GET /api/v1/pools
Authorization: Bearer a1b2c3d4...64hexchars
Tenant A will never see pools, members, services, or geo rules created by Tenant B, even if they share the same pool name.
Isolation model
| Layer | Scope |
|---|---|
pools |
Filtered by tenant_id on all reads, writes, and
deletes. |
services |
Filtered by tenant_id on all reads, writes, and
deletes. |
members |
No tenant_id column; scoped via JOIN with
pools on ownership checks. |
health_checks |
Scoped via pool-ownership pre-check. |
geo_rules |
Scoped via pool-ownership pre-check on create/list; subquery on delete. |
health_status |
Internal table keyed by pool_id — indirectly isolated
since pool IDs are UUIDs. |
| DNS resolution | GetServiceByDomain queries across all tenants (domains
are globally unique). |
Known limitations (v1)
- Pool and service names are globally unique — the
UNIQUE(name)constraint onpoolsandservicesis not yet scoped per-tenant. Two tenants creating a pool named"web-prod"will get a conflict error. A future migration will change this toUNIQUE(name, tenant_id). Workaround: prefix names with a tenant identifier (e.g."acme-web-prod"). - Deleting a tenant does not cascade-delete its
resources — pool/service rows remain with their
tenant_idset but no corresponding tenant record. This is intentional (allows recovery by recreating the tenant with the same ID), but operators should manually clean up if the tenant is permanently deleted. - DNS is not per-tenant — the DNS server serves all
tenants' domains from a shared namespace. Domain names must be globally
unique across tenants (enforced by the
idx_services_domainunique index). - No per-tenant rate limiting — the
rpsLimitfield is stored but not yet plumbed into the DNS RPS limiter. Per-tenant RPS enforcement is planned.
Database schema
CREATE TABLE tenants (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
api_key_hash TEXT NOT NULL UNIQUE, -- SHA-256 hex of the bearer token
rps_limit INTEGER NOT NULL DEFAULT 1000,
created_at INTEGER NOT NULL
);
-- pools and services gain tenant_id via additive migration:
ALTER TABLE pools ADD COLUMN tenant_id TEXT NOT NULL DEFAULT 'default';
ALTER TABLE services ADD COLUMN tenant_id TEXT NOT NULL DEFAULT 'default';Existing rows default to 'default', preserving all data
for single-tenant deployments upgrading to a multi-tenant release.
Security notes
- API keys are 32 random bytes (256 bits), hex-encoded to 64 characters — brute force is not feasible.
- Only the SHA-256 hash is stored; a DB compromise does not expose live keys.
- The admin key (
GSLB_API_KEY) is never stored in the database; it is read from the environment at request time. - Each mutating API call is recorded in the audit log with the
tenantfield set to the caller's tenant ID.
Code references
internal/storage/tenant.go:Tenantstruct,GenerateAPIKey,HashAPIKey,TenantDBwrapper with all scoped CRUD methods.internal/api/server.go:authMiddleware— tenant resolution and context injection;tenantFromCtx,isAdminFromCtx.internal/api/handlers.go:tdb(r)helper — all handlers obtain a*TenantDBscoped to the request's tenant.