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:
configVersion: lyno/v1
kind: Tenant
metadata:
name: acme
spec:
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 (bindHost, bindPort, 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 defaultTrunk | 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:
configVersion: lyno/v1
kind: Trunk
metadata:
name: acme-only
spec:
tenant: acme # a spec field, not metadata: the trunk stays global
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
- Add a
Tenantdocument with at least one entry underdomains. - Add an
Accountdocument per phone, each carryingmetadata.tenant: <name>. - Add
Extensiondocuments for the numbers that need more than ringing. - Claim the tenant's numbers under
didsand add anInboundRouteper number, or point anInboundOverrideat them. - Validate (
-validate) and restart the PBX.
Where you put those documents is up to you: one file per tenant reads well and
lyno -configure will keep writing each one back where it found it.
Account documents contain SIP passwords in plain text, and Mailbox documents
contain PINs. Lyno writes configuration files with mode 0600; keep them out of
anything public and treat every file holding them as a secret.