Multi-tenancy
A tenant is one customer of the PBX: its own phones, extension numbers, DIDs and dialplans. Tenants share the SIP listeners and the carrier trunks defined globally, and are isolated in everything else.
A tenant is identified two ways
Both must be configured, because they answer two different questions:
name: acme
domains: [acme.pbx.example.com] # which tenant is this phone?
dids:
numbers: ["+31201234567"] # which tenant is this incoming call for?
patterns: ['^\+312012345\d{2}$']
domainsattribute an endpoint to a tenant. A phone registers asalice@acme.pbx.example.com, and the domain in that request selects the tenant. Domains must be unique across tenants; validation refuses a duplicate.didsattribute an inbound trunk call to a tenant.numbersare exact claims,patternsare Go regular expressions covering whole number blocks.
Isolation guarantees
Extension numbers are only unique within a tenant. Registrations are keyed
tenant/extension throughout, so extension 100 in acme and extension 100 in
globex are unrelated entries that can both be online at once.
The digest realm is the tenant's domain, not a fixed string. That is what stops credentials issued to one tenant from working against another: the challenge a phone answers is scoped to the domain it registered with.
A dialplan is compiled per tenant. Ring groups, the timezone used by time conditions and the music-on-hold classes all resolve inside the tenant that owns the extension.
Tenant directories are self-contained. Everything a tenant needs lives under its own directory, so a tenant can be reviewed, copied to another system or removed as a unit.
What tenants share
| Shared | Per tenant |
|---|---|
SIP listeners (bind_host, bind_port, transports) | Domains and DIDs |
| Media settings (codecs, RTP port range) | Accounts and extensions |
| Carrier trunks | Ring groups |
| Global inbound overrides | Inbound routes |
Global outbound routes and default_trunk | Outbound routes (tried first) |
| Global music-on-hold classes | Music-on-hold classes (add to and override the global ones) |
| Global timezone | Timezone (overrides the global one) |
Everything that can be global or per tenant resolves the same way: the tenant value if it is set, otherwise the global one. There is no merging beyond that, except for music-on-hold classes, where the tenant's map is layered on top of the global one key by key.
A trunk that belongs to one tenant
By default a trunk is shared, and the DID decides who an inbound call belongs to. Naming a tenant on the trunk pins it instead:
trunks:
- name: acme-only
tenant: acme
host: sip.provider-b.example
Every inbound call on that trunk is now acme's, whatever number it arrives on.
This is useful when a tenant brings their own carrier account.
Calling between tenants
Off by default, and it takes two switches to enable — see Inter-tenant calling.
Adding a tenant
- Create
configs/tenants/<name>/tenant.yamlwith at least adomainsentry. The tenant'snamedefaults to the directory name if omitted. - Create
configs/tenants/<name>/extensions/with one file per extension. - Claim the tenant's numbers under
dids, or add a globalinbound_overridesentry pointing at them. - Validate (
-validate) and restart the PBX.
tenant.yaml contains SIP account passwords in plain text. Lyno writes
configuration files with mode 0600; keep them out of anything public and
treat the directory as a secret.