Skip to main content

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}$']
  • domains attribute an endpoint to a tenant. A phone registers as alice@acme.pbx.example.com, and the domain in that request selects the tenant. Domains must be unique across tenants; validation refuses a duplicate.
  • dids attribute an inbound trunk call to a tenant. numbers are exact claims, patterns are 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

SharedPer tenant
SIP listeners (bindHost, bindPort, transports)Domains and DIDs
Media settings (codecs, RTP port range)Accounts and extensions
Carrier trunksRing groups
Global inbound overridesInbound routes
Global outbound routes and defaultTrunkOutbound routes (tried first)
Global music-on-hold classesMusic-on-hold classes (add to and override the global ones)
Global timezoneTimezone (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

  1. Add a Tenant document with at least one entry under domains.
  2. Add an Account document per phone, each carrying metadata.tenant: <name>.
  3. Add Extension documents for the numbers that need more than ringing.
  4. Claim the tenant's numbers under dids and add an InboundRoute per number, or point an InboundOverride at them.
  5. 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.

These documents hold real passwords

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.