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:

configs/tenants/acme/tenant.yaml
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}$']
  • 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 (bind_host, bind_port, transports)Domains and DIDs
Media settings (codecs, RTP port range)Accounts and extensions
Carrier trunksRing groups
Global inbound overridesInbound routes
Global outbound routes and default_trunkOutbound 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:

configs/lyno.yaml
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

  1. Create configs/tenants/<name>/tenant.yaml with at least a domains entry. The tenant's name defaults to the directory name if omitted.
  2. Create configs/tenants/<name>/extensions/ with one file per extension.
  3. Claim the tenant's numbers under dids, or add a global inbound_overrides entry pointing at them.
  4. Validate (-validate) and restart the PBX.
Tenant files hold real passwords

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.