9 July 2026 · 9 min read · recloud engineering
Running FreeRADIUS on Kubernetes: subscriber AAA at scale
RADIUS is the quiet dependency of every ISP: if it stops answering, your BNG stops bringing subscribers online. Moving it onto Kubernetes gives you rollbacks, canaries and real monitoring — but RADIUS predates the cloud-native world by two decades, and it shows. Here is what actually matters when you containerise subscriber AAA.
The UDP problem
RADIUS runs over UDP, and NAS devices identify clients by source IP and shared secret. Two consequences:
- The NAS must see a stable server address. Expose FreeRADIUS through a
LoadBalancerservice with a pinned address (MetalLB pool or equivalent) rather than a NodePort that moves. - Replies must leave from the address the request arrived on. Set
externalTrafficPolicy: Localso kube-proxy doesn't SNAT the request and break the return path — it also preserves the NAS source address for yourclients.confmatching.
apiVersion: v1
kind: Service
metadata:
name: radius
annotations:
metallb.universe.tf/address-pool: radius-vips
spec:
type: LoadBalancer
externalTrafficPolicy: Local
ports:
- { name: auth, port: 1812, protocol: UDP }
- { name: acct, port: 1813, protocol: UDP }
selector: { app: freeradius }
Where the BNG lives on a directly attached VLAN, a Multus secondary interface (covered in the bare-metal article) is simpler still: the pod owns a real address on the AAA VLAN and Kubernetes handles everything else.
Authentication is stateless. Accounting is not.
Access-Request handling scales horizontally without drama: three replicas behind the same VIP, each hitting the same subscriber database, is a fine design. Accounting is the part that punishes casual scaling — Interim-Updates arrive continuously for every online session, and you must not lose Stop records if you bill on usage.
The pattern we deploy: FreeRADIUS writes accounting to a queue instead of straight into SQL.
# sites-enabled/default (accounting section)
accounting {
# fire-and-forget into a local buffer, workers drain to Kafka/SQL
detail
sql_kafka
}
A consumer service then owns the database write, dedupes on Acct-Unique-Session-Id, and can be redeployed freely without dropping packets — the queue absorbs the gap. It also gives your monitoring a beautiful signal: consumer lag tells you instantly when accounting is falling behind.
Health checks that mean something
A TCP connect check tells you nothing about a UDP daemon. Probe RADIUS with RADIUS — radclient sending a Status-Server request:
livenessProbe:
exec:
command: ["/usr/bin/radclient-probe.sh"] # Status-Server to 127.0.0.1:18121
periodSeconds: 10
failureThreshold: 3
Pair it with a readiness probe that also checks the subscriber database connection: a FreeRADIUS pod that can't reach its datastore should stop receiving traffic before it starts rejecting subscribers.
Monitoring the thing that matters
Export per-pod counters (accepts, rejects, latency histograms) via the FreeRADIUS status server into Prometheus, then alert on the ratios, not the absolutes: a spike in Access-Reject with steady Access-Request volume is a config or datastore problem; both dropping together means the network in front of you is the problem. Graph online session count from your accounting pipeline next to BNG session counts from SNMP — when those diverge, you have silent accounting loss.
Rollouts without dropped subscribers
With externalTrafficPolicy: Local, a terminating pod means its node stops being a valid backend — so use maxUnavailable: 0, maxSurge: 1 rolling updates and a preStop sleep long enough for the LB withdrawal to propagate. NAS retransmission covers the last in-flight packets. Subscribers already online are untouched throughout: sessions live on the BNG, not in RADIUS.