# Authentication and security

Before Signals carries a call from your PBX, it must be sure the call really comes from you.
Otherwise anyone who found your trunk could place calls on your account. This page explains the two
ways a trunk recognises your PBX, how to manage credentials safely, and how to protect signalling
and audio in transit.

Every change on this page is live once the API accepts it and takes effect within a minute. There is
nothing to publish. Each edit needs the trunk's current ETag in `If-Match` and returns a new one.
See [Saving changes](/sip-trunks/overview#saving-changes).

## Choose an authentication mode

A trunk uses one mode, set by `auth_mode` when you create it and changeable with
`PATCH /v1/trunks/{trunk_id}`:

| | IP allow list (`ip_allow_list`) | Digest (`digest`) |
| --- | --- | --- |
| How Signals recognises you | The public IP address the call comes from. | A username and password your PBX sends when Signals challenges it. |
| Best for | A PBX or session border controller with a fixed public IP address. | A PBX behind a changing address, or several sites sharing one trunk. |
| What you manage | A list of addresses and ranges. | Generated credentials, rotated on a schedule. |
| Secret to protect | None. | The password, shown once. |

When you can, prefer an IP allow list with TLS. There is no password to leak, and a call from any
other address is refused.

## IP allow lists

An IP allow list is the set of public addresses Signals accepts calls from. Each entry is an IP
credential: a CIDR range, such as `203.0.113.10/32` for one address or `203.0.113.0/28` for sixteen.

### Set the list when you create the trunk

Pass the addresses in `sources` with `"auth_mode": "ip_allow_list"`. The list must not be empty.
Each entry must be a public IPv4 address or range. Private ranges such as `10.0.0.0/8` and
`192.168.0.0/16`, loopback, link-local, multicast and broadcast addresses are refused with
`422 invalid_trunk`.

```json
{
  "auth_mode": "ip_allow_list",
  "sources": ["203.0.113.10/32", "203.0.113.11/32"]
}
```

### Add, change and remove addresses

Manage the list afterwards with the IP credential endpoints:

| Action | Request |
| --- | --- |
| List | `GET /v1/trunks/{trunk_id}/ip-credentials` |
| Add | `POST /v1/trunks/{trunk_id}/ip-credentials` with `If-Match` and `Idempotency-Key` |
| Change | `PATCH /v1/trunks/{trunk_id}/ip-credentials/{credential_id}` with `If-Match` |
| Remove | `DELETE /v1/trunks/{trunk_id}/ip-credentials/{credential_id}` with `If-Match` |

An add or change takes a body like this:

```json
{
  "cidr": "203.0.113.16/29",
  "selector_precedence": 100,
  "enabled": true
}
```

- `cidr` must include the prefix length, for example `/32`.
- `selector_precedence` decides which entry wins when ranges overlap. It defaults to `100`.
- `enabled` lets you switch an entry off without deleting it. It defaults to `true`.

Two enabled entries on the same trunk cannot overlap at the same `selector_precedence`, and the same
range cannot appear twice. Either returns `409 trunk_component_conflict`.

:::caution
When you replace a PBX address, add the new address first and remove the old one a minute later.
Calls from an address that is not on the list are refused.
:::

## Digest credentials

Digest authentication is a challenge and response. Signals challenges a call from your PBX, and the
PBX answers with a username and a value derived from the password. The password itself never
crosses the network. Signals stores only that derived value (HA1), so it cannot show you the
password again later.

### Create a digest trunk

Create the trunk with `"auth_mode": "digest"`, an empty `sources` list and a `digest_realm`, the
name of the authentication domain your PBX answers for. Sending addresses with digest returns
`422 invalid_trunk`, and so does leaving out `digest_realm`.

```json
{
  "auth_mode": "digest",
  "sources": [],
  "digest_realm": "sip.trunk.pressone.co"
}
```

Signals creates the trunk's first credential in the same request. The `201 Created` body is
`{ "trunk": { ... }, "digest_password": "..." }`. The username is in the trunk's
`authentication.digest`. Copy the password now: this is the only response that contains it, and a
replay of the same request returns `"digest_password": null`.

### Create another credential

To add a credential to an existing trunk, for example for a second PBX, create one. Signals generates the username and password. You choose only the realm, the name of the
authentication domain your PBX answers for. The realm must be 1 to 255 characters with no spaces or
colons. The console uses the platform realm `sip.trunk.pressone.co` from `tls.realm` in
`GET /v1/platform/endpoints`.

```bash
curl -X POST "$SIGNALS/trunks/$TRUNK_ID/digest-credentials" \
  -H "Authorization: Bearer $SIGNALS_API_KEY" \
  -H "Content-Type: application/json" \
  -H 'If-Match: "1"' \
  -H "Idempotency-Key: 0d6b9e41-8c2f-4a57-b3e0-9f1a7c5d2e84" \
  -d '{ "realm": "sip.trunk.pressone.co" }'
```

```json
{
  "id": "01992f3a-91b0-7c55-8e21-4a6d0f3b9c18",
  "username": "sig_01992f3a91b07c558e214a6d0f3b9c18",
  "realm": "sip.trunk.pressone.co",
  "enabled": true,
  "valid_until": null,
  "rotated_from_id": null,
  "new_applied_at": null,
  "previous_retires_at": null,
  "created_at": "2026-10-01T10:02:11Z",
  "revoked_at": null,
  "password": "01992f3a91b27e04a5c8d1f60b3e7a2901992f3a91b27e04b9d2c4a81e6f0d35",
  "secret_unavailable": false
}
```

Copy the password into your PBX or a secrets manager now. This is the only response that contains
it.

### The password is shown once

- `GET /v1/trunks/{trunk_id}/digest-credentials` returns usernames and lifecycle dates, never the
  password or HA1.
- If your request times out and you retry it with the same `Idempotency-Key`, you get the same
  credential back with `"password": null` and `"secret_unavailable": true`. The password is never
  disclosed twice.
- If you lose a password, rotate the credential or revoke it and create a new one.

A digest trunk must keep at least one enabled credential. A change that would leave it with none
returns `422` with a detail that starts with `the trunk would not apply:`.

### Rotate with overlap

Rotation replaces a credential without dropping calls. Signals creates a new username and password
and keeps the old credential valid for 24 hours after the network has applied the new one. That
overlap gives you a day to update every PBX.

1. Rotate the credential. The response contains the new password, once:

   ```bash
   curl -X POST "$SIGNALS/trunks/$TRUNK_ID/digest-credentials/01992f3a-91b0-7c55-8e21-4a6d0f3b9c18/rotate" \
     -H "Authorization: Bearer $SIGNALS_API_KEY" \
     -H 'If-Match: "4"' \
     -H "Idempotency-Key: 7e2c0a95-1d4b-4f86-a9c3-5b8e2d6f0a17"
   ```

   ```json
   {
     "id": "01992f9c-2e70-7a13-9b54-8c1d3f6a0e29",
     "username": "sig_01992f9c2e707a139b548c1d3f6a0e29",
     "realm": "sip.trunk.pressone.co",
     "rotated_from_id": "01992f3a-91b0-7c55-8e21-4a6d0f3b9c18",
     "new_applied_at": null,
     "previous_retires_at": null,
     "password": "01992f9c2e72705d8a41c6e93f0b7d2401992f9c2e727e1fb6a0d39c4e85f172",
     "secret_unavailable": false
   }
   ```

2. Within a minute the network accepts the new credential. Its `new_applied_at` is then set, and
   `previous_retires_at` shows when the old credential stops working, 24 hours later.
3. Update each PBX with the new username and password before `previous_retires_at`.

### Revoke a credential

`DELETE /v1/trunks/{trunk_id}/digest-credentials/{credential_id}` with `If-Match` revokes the
credential and returns `204 No Content`. The network stops accepting it within a minute.

## TLS for signalling

SIP messages carry phone numbers, caller IDs and, with digest, authentication exchanges. TLS
encrypts them between your PBX and Signals. Signals accepts SIP over TLS on port 5061 at the
signalling address in `GET /v1/platform/endpoints`, and over UDP or TCP on port 5060.

Set `require_tls` to `true` to keep a trunk on TLS. With `require_tls` on, every termination target
must use `"transport": "tls"`, so Signals reaches your PBX over TLS too. A target with `udp` or
`tcp` returns `422 invalid_trunk`. A target written as a `sips:` URI must always use `tls`.

```json
{
  "require_tls": true,
  "targets": [
    { "sip_uri": "sip:203.0.113.10:5061", "priority": 10, "weight": 100, "transport": "tls" }
  ]
}
```

Point your PBX at port 5061 with TLS, and verify the Signals certificate. The `tls` object in the
platform endpoints response gives the TLS name (`realm`) and any certificate chain information
(`trust_chain`).

## Source validation

`source_validation` takes `relaxed` or `strict`. `strict` also checks the SIP `Via` header against
your allowed sources, so a request has to come from, and claim to come from, an address you listed.
Use `strict` with an IP allow list. Keep `relaxed` with digest, where your PBX address may change.

## Media security

Audio travels as RTP, separately from SIP. Two settings control it.

`media_security` decides whether audio is encrypted:

| Value | What happens |
| --- | --- |
| `rtp` | Audio is plain RTP. |
| `sdes_srtp_required` | Audio must be SRTP, encrypted with keys exchanged in the SDP (SDES). A call that does not negotiate SRTP is refused rather than downgraded to plain RTP. |

SDES keys travel inside the SIP messages, so SRTP only protects audio when signalling is also
encrypted. Pair `sdes_srtp_required` with `require_tls: true` and a TLS connection from your PBX.

`codecs` decides which audio codecs the trunk offers. G.711 (`PCMA` and `PCMU`) is always on; add
`G729`, `G722` or `OPUS` to turn those on too. A repeated codec is refused. Signals offers them to
your PBX in a fixed order, `OPUS`, `G722`, `G729`, `PCMA`, `PCMU`, whatever order you send.

Audio flows through the Signals media address in `media_cidrs`. Your PBX never exchanges audio
directly with the far end.

## Limits as a safety control

Limits stop a compromised PBX or a dialling loop from running up cost:

- On the trunk: `concurrency`, `cps` and `max_call_duration_seconds`.
- On the outbound policy: the same three, plus `max_destination_rate_minor` and
  `daily_spend_limit_minor`. See [Outbound policies](/sip-trunks/outbound-policies).

Signals applies every limit that covers a call, and the tightest wins.

## Checklist

- Use an IP allow list with `strict` source validation when your PBX has a fixed public address.
- Turn on `require_tls` and connect on port 5061.
- Use `sdes_srtp_required` when audio must be encrypted.
- Store digest passwords in a secrets manager, never in tickets or chat.
- Rotate digest credentials on a schedule.
- Set a daily spend cap on every outbound policy.

## Related

- [Connect your PBX](/sip-trunks/connect-your-pbx)
- [SIP Trunking API reference](/api/sip-trunking)
