Skip to main content

RADIUS Shared Secrets (per source-IP group)

Overview

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

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:

  1. OmniTWAG reads the source IP of the packet.
  2. 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).
  3. If no subnet matches, OmniTWAG drops the packet. The source is not authorized.
  4. 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.

FieldTypeDescription
idStringUnique identifier (UUID). The server sets this value.
nameStringHuman-readable label, for example Melbourne CBD APs.
secretStringRADIUS shared secret.
subnetsList of StringOne or more CIDR blocks (IPv4, IPv6, or a mix), for example ["10.10.0.0/24", "2001:db8:a::/48"].
breakout_typeStringData-plane handling for sessions from this access point: tunneled, nswo, or local. Default tunneled. See Per-AP breakout and charging.
address_sourceStringFor nswo breakout, how the UE IP is assigned: pool or dhcp. Default pool. See Per-AP breakout and charging.
charging_modeStringGy online-charging behaviour: charged, uncharged, or auth_only. Default charged. See Per-AP breakout and charging.
ip_pool_idStringOptional id of an IP pool that addresses nswo sessions from this access point. null means no pool.
inserted_atIntegerTime the credential was created, in milliseconds since the Unix epoch. The server sets this value.
updated_atIntegerTime 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.

ValueDescription
tunneledOmniTWAG 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.
nswoNon-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.
localThe 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.

ValueDescription
poolOmniTWAG pre-allocates a fixed address from the associated IP pool for each session. This is the default.
dhcpThe 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.

ValueDescription
chargedOmniTWAG opens a Gy online-charging session and reports usage to the OCS. This is the default.
unchargedOmniTWAG establishes the data session but does not open a Gy session. Use this value for flat-rate or non-charged access.
auth_onlyOmniTWAG 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.

MethodPathActionDescription
GET/radius_credentialsindexList all credentials.
GET/radius_credentials/{id}showGet one credential by id.
POST/radius_credentialscreateCreate a credential.
PUT/radius_credentials/{id}updateChange a credential.
DELETE/radius_credentials/{id}deleteRemove a credential.

Request body

POST and PUT accept a JSON body with these fields.

FieldTypeRequiredDescription
nameStringNoHuman-readable label.
secretStringYesRADIUS shared secret. Must not be empty.
subnetsList of StringYesOne or more valid CIDR blocks. Must not be empty.
breakout_typeStringNotunneled, nswo, or local. Default tunneled. See Per-AP breakout and charging.
address_sourceStringNopool or dhcp (for nswo). Default pool.
charging_modeStringNocharged, uncharged, or auth_only. Default charged.
ip_pool_idStringNoid 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

StatusMeaning
200 OKThe request was successful (index, show, update, delete).
201 CreatedThe credential was created.
404 Not FoundNo credential has the given id.
422 Unprocessable EntityThe 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"
}
ParameterTypeRequiredDefaultDescription
credentials_dets_pathStringNopriv/radius_credentials.detsPath to the DETS file that stores the credentials. Set this to a writable data directory in production.
secretStringNo123456Legacy single shared secret. Used only to seed the store on first start. See Migration from a single static secret.
allowed_source_subnetsList of StringNo[]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 secret is set and allowed_source_subnets is 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.

LabelDescription
sourceSource 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.

LabelDescription
sourceSource IP of the access point.
credential_idid 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:

  1. Find the source IP of the access point in the OmniTWAG logs.
  2. Confirm that a credential subnet contains that IP.
  3. 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:

  1. Read the credential_id label on the metric, or find the bad-secret message in the logs.
  2. Compare the secret on the access point with the secret in the credential.
  3. 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_path points to a directory that is not writable, or to a path that is cleared on restart.

Resolution:

  1. Set credentials_dets_path to a writable, persistent data directory.
  2. Confirm that the OmniTWAG process can write to that path.
  3. Add the credentials again through the API or the web interface.