RADIUS Shared Secrets (per source-IP group)
OmniTWAG authenticates each RADIUS client (an access point, or NAS) with a shared secret. This feature lets an operator manage many shared secrets, each tied to one or more source-IP subnets. A group of access points that share a subnet also shares one secret. The operator adds, changes, and removes these credentials on demand through the REST API or the control-panel web interface. The store is persistent, so credentials survive a restart.
Table of Contents
- Concepts
- How authentication uses a credential
- Credential model
- Per-AP breakout and charging
- REST API
- Web interface
- Configuration
- Migration from a single static secret
- Metrics
- Troubleshooting
Concepts
A credential ties one shared secret to one or more source subnets. A source subnet is a CIDR block. The subnet can be IPv4, IPv6, or a mix of both.
The source IP is the IP address of the access point that sends the RADIUS
packet. OmniTWAG reads the source IP from the received UDP packet. OmniTWAG does
not use the NAS-IP-Address attribute for secret selection, because an attacker
can set that attribute to any value.
A credential subnet also authorizes the access point. If the source IP matches a credential subnet, the access point is authorized. If the source IP matches no credential subnet, OmniTWAG drops the packet. This means that when an operator adds a credential for a new access point, the access point is authorized at the same time.
How authentication uses a credential
OmniTWAG resolves the shared secret for every incoming RADIUS packet. The steps are:
- OmniTWAG reads the source IP of the packet.
- OmniTWAG finds the credential whose subnet matches the source IP. If more than one subnet matches, OmniTWAG uses the subnet with the longest prefix (the most specific match).
- If no subnet matches, OmniTWAG drops the packet. The source is not authorized.
- If a subnet matches, OmniTWAG uses that credential's secret to decode the
packet, to check the
Message-Authenticator, to encrypt the MPPE keys, and to sign the reply.
If a subnet matches but the Message-Authenticator does not verify, the access
point holds a different secret than the one on file. OmniTWAG logs this event and
increases a metric. OmniTWAG still processes the packet. The reply is signed with
the on-file secret, so the access point rejects the reply and authentication fails.
This behaviour makes a misconfigured secret easy to find. It does not drop the
packet, so a valid edge case is never dropped by mistake.
Credential model
Each credential has these fields.
| Field | Type | Description |
|---|---|---|
id | String | Unique identifier (UUID). The server sets this value. |
name | String | Human-readable label, for example Melbourne CBD APs. |
secret | String | RADIUS shared secret. |
subnets | List of String | One or more CIDR blocks (IPv4, IPv6, or a mix), for example ["10.10.0.0/24", "2001:db8:a::/48"]. |
breakout_type | String | Data-plane handling for sessions from this access point: tunneled, nswo, or local. Default tunneled. See Per-AP breakout and charging. |
address_source | String | For nswo breakout, how the UE IP is assigned: pool or dhcp. Default pool. See Per-AP breakout and charging. |
charging_mode | String | Gy online-charging behaviour: charged, uncharged, or auth_only. Default charged. See Per-AP breakout and charging. |
ip_pool_id | String | Optional id of an IP pool that addresses nswo sessions from this access point. null means no pool. |
inserted_at | Integer | Time the credential was created, in milliseconds since the Unix epoch. The server sets this value. |
updated_at | Integer | Time the credential was last changed, in milliseconds since the Unix epoch. The server sets this value. |
The operator sets name, secret, subnets, breakout_type, address_source,
charging_mode, and ip_pool_id. The server sets id, inserted_at, and
updated_at.
Per-AP breakout and charging
Each credential also controls the data plane and charging for the access points in the group. The defaults preserve the standard behaviour: a GTP tunnel to the PGW with online charging.
breakout_type
breakout_type sets how subscriber traffic leaves the TWAG.
| Value | Description |
|---|---|
tunneled | OmniTWAG establishes a GTP-C/GTP-U session to the PGW and forwards the subscriber traffic over the tunnel. This is the default. See S2a GTP-C Interface. |
nswo | Non-Seamless WLAN Offload. OmniTWAG anchors the session locally and does not tunnel to the PGW. OmniTWAG assigns the UE IP from an IP pool. The subscriber traffic transits the TWAG and breaks out to the local network. |
local | The access point owns the data plane. OmniTWAG authenticates the subscriber and assigns no IP. The UE gets its address from the local network. |
address_source
address_source applies only when breakout_type is nswo. It sets how the UE
IP is assigned. See IP Pools.
| Value | Description |
|---|---|
pool | OmniTWAG pre-allocates a fixed address from the associated IP pool for each session. This is the default. |
dhcp | The OmniTWAG DHCP server dishes out an address from the pool range when the UE sends a DHCP DISCOVER. |
charging_mode
charging_mode sets the Gy online-charging behaviour. See Online Charging.
| Value | Description |
|---|---|
charged | OmniTWAG opens a Gy online-charging session and reports usage to the OCS. This is the default. |
uncharged | OmniTWAG establishes the data session but does not open a Gy session. Use this value for flat-rate or non-charged access. |
auth_only | OmniTWAG authenticates the subscriber but does not establish a data session. Use this value when another system provides connectivity. |
How the fields interact
charging_mode: auth_only takes priority. When a credential uses auth_only,
OmniTWAG does not establish a data session, so breakout_type and
address_source have no effect.
REST API
The OmniTWAG REST API serves these endpoints over HTTPS on the configured api_ex
port (8444 by default). The base path is /radius_credentials.
| Method | Path | Action | Description |
|---|---|---|---|
| GET | /radius_credentials | index | List all credentials. |
| GET | /radius_credentials/{id} | show | Get one credential by id. |
| POST | /radius_credentials | create | Create a credential. |
| PUT | /radius_credentials/{id} | update | Change a credential. |
| DELETE | /radius_credentials/{id} | delete | Remove a credential. |
Request body
POST and PUT accept a JSON body with these fields.
| Field | Type | Required | Description |
|---|---|---|---|
name | String | No | Human-readable label. |
secret | String | Yes | RADIUS shared secret. Must not be empty. |
subnets | List of String | Yes | One or more valid CIDR blocks. Must not be empty. |
breakout_type | String | No | tunneled, nswo, or local. Default tunneled. See Per-AP breakout and charging. |
address_source | String | No | pool or dhcp (for nswo). Default pool. |
charging_mode | String | No | charged, uncharged, or auth_only. Default charged. |
ip_pool_id | String | No | id of an IP pool for nswo addressing. An empty string means no pool. |
PUT changes only the fields in the body. Fields that are not in the body keep
their current value.
Responses
| Status | Meaning |
|---|---|
200 OK | The request was successful (index, show, update, delete). |
201 Created | The credential was created. |
404 Not Found | No credential has the given id. |
422 Unprocessable Entity | The secret is empty, a subnet is not a valid CIDR block, or breakout_type / address_source / charging_mode is not a valid value. |
Example: list credentials
Request:
GET /radius_credentials
Response:
{
"count": 1,
"radius_credentials": [
{
"id": "0d1e2f3a-4b5c-6d7e-8f90-1a2b3c4d5e6f",
"name": "Melbourne CBD APs",
"secret": "s3cr3t",
"subnets": ["10.10.0.0/24", "2001:db8:a::/48"],
"inserted_at": 1734512400000,
"updated_at": 1734512400000
}
]
}
Example: create a credential
Request:
POST /radius_credentials
{
"name": "Melbourne CBD APs",
"secret": "s3cr3t",
"subnets": ["10.10.0.0/24", "2001:db8:a::/48"]
}
A successful request returns 201 Created with the new credential, including its
id.
Example: an invalid subnet
Request:
{
"secret": "s3cr3t",
"subnets": ["10.10.0.0/24", "not-a-subnet"]
}
Response (422 Unprocessable Entity):
{
"error": "invalid CIDR subnet(s): not-a-subnet"
}
Web interface
The control panel has a RADIUS Secrets page at /radius_credentials. The page
lists every credential with its name, subnets, a masked secret, and the last-changed
time. The page has a form to add a credential and controls to edit or delete a
credential. Enter one or more subnets in the subnets field. Separate the subnets
with a comma or a new line.
Configuration
The RADIUS credential store keeps its data in a DETS file. Set the file path in the
radius_config block.
config :omnitwag,
radius_config: %{
# Path to the persistent RADIUS credential store.
credentials_dets_path: "priv/radius_credentials.dets",
# Legacy single-secret fields (see "Migration" below).
allowed_source_subnets: [],
secret: "123456"
}
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
credentials_dets_path | String | No | priv/radius_credentials.dets | Path to the DETS file that stores the credentials. Set this to a writable data directory in production. |
secret | String | No | 123456 | Legacy single shared secret. Used only to seed the store on first start. See Migration from a single static secret. |
allowed_source_subnets | List of String | No | [] | Legacy source-IP allow-list. Used only to seed the store on first start. See Migration from a single static secret. |
Migration from a single static secret
Earlier versions used one static secret and an optional allowed_source_subnets
allow-list. On first start, if the credential store is empty, OmniTWAG seeds one
credential from these legacy fields. The seed runs only when both of these are true:
- The store has no credentials.
- The
secretis set andallowed_source_subnetsis not empty.
The seeded credential uses the legacy secret and the legacy subnets. This keeps an
existing deployment working after an upgrade. OmniTWAG logs a message when it seeds a
credential.
If allowed_source_subnets is empty, OmniTWAG does not seed a credential. In that
case OmniTWAG rejects all RADIUS requests until the operator adds a credential. Add
credentials through the REST API or the web interface.
Per-packet authentication does not read secret or allowed_source_subnets. These
fields serve only as the first-start seed. Manage live credentials through the API
or the web interface.
Metrics
OmniTWAG exposes these counters on the Prometheus endpoint. See the Metrics Reference for the endpoint and query examples.
radius_auth_unauthorized_source_count
Type: Counter Description: Number of RADIUS packets dropped because the source IP matched no credential subnet.
| Label | Description |
|---|---|
source | Source IP of the dropped packet. |
radius_auth_bad_secret_count
Type: Counter
Description: Number of RADIUS packets whose source IP matched a credential, but
whose Message-Authenticator did not verify. The access point holds a different
secret than the one on file.
| Label | Description |
|---|---|
source | Source IP of the access point. |
credential_id | id of the matched credential. |
Troubleshooting
An access point gets no response
Symptoms: The access point sends RADIUS requests but receives no reply.
The radius_auth_unauthorized_source_count metric increases.
Possible causes:
- No credential subnet contains the source IP of the access point.
- The access point sends from a different IP than expected, for example a NAT address.
Resolution:
- Find the source IP of the access point in the OmniTWAG logs.
- Confirm that a credential subnet contains that IP.
- If no credential covers the IP, add a credential for the correct subnet.
An access point is authorized but authentication fails
Symptoms: The radius_auth_bad_secret_count metric increases for the access
point. Clients cannot authenticate.
Possible causes:
- The access point holds a different shared secret than the credential on file.
Resolution:
- Read the
credential_idlabel on the metric, or find the bad-secret message in the logs. - Compare the secret on the access point with the secret in the credential.
- Correct the secret on the access point, or update the credential to match.
Credentials are lost after a restart
Symptoms: The credential list is empty after a restart. OmniTWAG rejects all requests.
Possible causes:
- The
credentials_dets_pathpoints to a directory that is not writable, or to a path that is cleared on restart.
Resolution:
- Set
credentials_dets_pathto a writable, persistent data directory. - Confirm that the OmniTWAG process can write to that path.
- Add the credentials again through the API or the web interface.