Stand up 802.1X in monitor mode
When you need this
Section titled “When you need this”- “Which devices on the fabric could NOT pass 802.1X — before we ever turn enforcement on?”
- “Show me every authentication attempt, on which switch port, with the decision we would have made.”
- “I want a paper trail of network access, revisable as beliefs change, without risking a single patient-care port.”
l2trace already knows every legitimate MAC and the port/VLAN it belongs
on, bitemporally. The RADIUS server turns that knowledge into an
access-control decision: a switch relays each device’s authentication to
l2trace, l2trace decides MAC Authentication Bypass (MAB) against what it
has actually observed, and records the attempt in the bitemporal
auth_event log. In monitor mode (the default) the reply is always
Accept with no VLAN, so the network is left exactly as it is and the value
is the recorded discrepancy flag: an unknown MAC, or a device on a VLAN
its port doesn’t allow.
The safety model (read this first)
Section titled “The safety model (read this first)”This is designed for a network where cutting the wrong port is a real harm. Three things enforce that:
- Monitor mode is the default and makes zero changes. Every reply is
Access-Accept with no VLAN attribute; the switch runs
authentication openso the port forwards regardless. l2trace only observes and records. - Enforcement takes three independent switches to arm. Real
Accept/Reject and dynamic-VLAN assignment happen only when
RADIUS_MONITOR_MODE=falseandRADIUS_ENFORCEMENT_PERMITTED=trueand that one switch’sdevice_collector.extras.radius_enforce=true. A stray flag on a production row, by itself, can never start cutting ports — and every enforce-flagged switch is named loudly in the logs on each refresh. - Enforcement fails open. Even when armed, it rejects only a genuinely-unknown MAC; a topology gap (a port l2trace hasn’t mapped) or a decision-time DB error results in Accept, never a denial.
Prove enforcement on lab switches before arming a single production port.
1. Configure the switch (monitor mode)
Section titled “1. Configure the switch (monitor mode)”On each switch, run 802.1X/MAB with authentication open so the port
never blocks, point RADIUS at l2trace, and enable Change-of-Authorization
so l2trace can push a re-auth later. Representative Cisco IOS-XE (exact
syntax varies by train):
aaa new-modelradius server L2TRACE address ipv4 <l2trace-ip> auth-port 1812 acct-port 1813 key <shared-secret>aaa group server radius L2TRACE server name L2TRACEaaa authentication dot1x default group L2TRACEaaa authorization network default group L2TRACEdot1x system-auth-control
! CoA (l2trace pushes re-auth / VLAN change here)aaa server radius dynamic-author client <l2trace-ip> server-key <shared-secret>
interface GigabitEthernet1/0/5 authentication open ! monitor: the port NEVER blocks authentication port-control auto mab ! MAC Authentication Bypass dot1x pae authenticatorauthentication open is the load-bearing line: it makes the switch honor
the auth exchange for logging while leaving the port up no matter what
l2trace replies. Keep it on every port until enforcement is validated.
Modern IOS-XE sends a RADIUS Message-Authenticator by default (the BlastRADIUS mitigation); l2trace requires it. If you run older gear that can’t, see the security note below.
2. Register each switch as a RADIUS client
Section titled “2. Register each switch as a RADIUS client”l2trace answers only switches it knows, resolved by source IP. Add a
device_collector row with source=radius for each switch, carrying that
switch’s shared secret as a secrets URI (never a literal in the DB — see
secrets management):
{ "source": "radius", "device_id": <the switch's device id>, "enabled": true, "auth": { "radius_secret": "env://L2TRACE_SW5_RADIUS_SECRET" }, "extras": { "radius_enforce": false }}The secret is resolved from the server’s environment at packet time and
re-read on each refresh, so rotations land without a restart. A switch
with no radius_secret falls back to RADIUS_SHARED_SECRET.
3. Enable + deploy the server
Section titled “3. Enable + deploy the server”The RADIUS server runs as its own service so switch authentication is independent of the reconciler. Settings (all default-safe):
| Env var | Default | Meaning |
|---|---|---|
RADIUS_ENABLED | false | Turn the server on |
RADIUS_MONITOR_MODE | true | Master safety: never change the network |
RADIUS_REQUIRE_MESSAGE_AUTHENTICATOR | true | Drop packets lacking a valid Message-Authenticator |
RADIUS_ENFORCEMENT_PERMITTED | false | Global arm for enforcement (second gate) |
RADIUS_AUTH_PORT / RADIUS_COA_PORT | 1812 / 3799 | UDP ports |
Single host: add the docker-compose.radius.yml overlay (publishes UDP
1812/1813/3799) or co-run it in reconcile with RADIUS_ENABLED=true.
Swarm: it’s a standalone radius service (host-mode UDP, pinned to a node
so switches have a stable target). Run it directly with:
l2trace radius4. Verify end to end with the mock switch
Section titled “4. Verify end to end with the mock switch”You don’t need a real switch to prove the pipeline. The bundled mock switch (a software NAS) sends a signed Access-Request and listens for CoA, exactly like a real one. Point it at the running server:
# Send a MAB Access-Request for a MAC and print the decision.l2trace radius-mock-switch --server <l2trace-ip> --secret <shared-secret> \ --mac 00:11:22:33:44:55 --no-listen# 00:11:22:33:44:55: ACCEPTThe mock’s source IP must be a registered source=radius switch. A known
MAC comes back ACCEPT with no VLAN (monitor mode); the attempt lands in
the auth_event log with its discrepancy verdict.
To watch the Change-of-Authorization path, keep the mock listening and push a CoA to it from another terminal:
# Terminal 1: authenticate, then listen for CoA on udp/3799.l2trace radius-mock-switch --server <l2trace-ip> --secret <s> --mac 00:11:22:33:44:55
# Terminal 2: send a re-auth onto VLAN 20 (what Phase 4's approval loop will do).l2trace radius-coa --host <mock-switch-ip> --secret <s> \ --mac 00:11:22:33:44:55 --vlan 20# CoA <- coa mac=00-11-22-33-44-55 vlan=20 (acked=True)5. Read the decisions
Section titled “5. Read the decisions”Every attempt is in the Auth view of the web UI, and at the API:
curl -s "https://l2trace.example.net/api/auth-events" | jq '.[] | {mac, switch, port, result, monitor_mode, discrepancy, reason}'The signal in monitor mode is discrepancy: true: an unknown MAC, or a
device observed on a VLAN its NAS port doesn’t allow. These are your
before-you-enforce worklist — the devices that would be rejected or
moved once enforcement is armed, surfaced with zero risk. Because the log
is bitemporal, “what did we believe about this device’s access last
Tuesday?” stays answerable.
Security note: the shared secret
Section titled “Security note: the shared secret”RADIUS runs over UDP and an Access-Request Authenticator is a nonce, not
keyed by the secret — so a datagram spoofing a registered switch’s source
IP would otherwise be trusted. l2trace requires the RFC 2869
Message-Authenticator (HMAC keyed by the shared secret) on every
request and drops anything lacking or failing it before any decision,
log write, or port lookup. Leave RADIUS_REQUIRE_MESSAGE_AUTHENTICATOR
on. Only relax it for legacy switches that genuinely can’t send one, and
know that doing so leaves that intake source-IP-spoofable.
Lab: PEAP (EAP-MSCHAPv2)
Section titled “Lab: PEAP (EAP-MSCHAPv2)”Beyond MAB, l2trace speaks PEAPv0 / EAP-MSCHAPv2 (username + password inside a TLS tunnel) for lab validation. Generate a CA + server cert and point the server at them:
uv run python scripts/gen_lab_ca.py # writes lab-certs/ (gitignored)# then set RADIUS_TLS_CERT / RADIUS_TLS_KEY to lab-certs/server.{crt,key}l2trace radius-mock-switch --server <l2trace-ip> --secret <s> \ --peap --username labuser --password labpass --ca lab-certs/ca.crt# labuser: ACCEPT (eap=success)What the RADIUS server does and doesn’t do
Section titled “What the RADIUS server does and doesn’t do”- Does: MAB decisioning, the bitemporal auth log, discrepancy flagging,
dynamic-VLAN + CoA primitives, monitor mode, triple-gated enforcement,
PEAP-MSCHAPv2 (validated against real
wpa_supplicanton a lab switch), EAP-TLS with client certificates (see Authenticate with client certificates (EAP-TLS)), the provisioning / quarantine approval workflow (an unknown device lands on a provisioning VLAN, an operator approves it, and a CoA re-auth moves it onto its real VLAN), and MPPE session-key derivation so a wireless AP can complete WPA-Enterprise (see Stand up wireless 802.1X). - Doesn’t (yet): RADIUS accounting. That is later work.
See also
Section titled “See also”- Authorize with Nautobot policies — hand the authorization policy to Nautobot once the standalone base here is proven
- 802.1X with Nautobot, and what happens when Nautobot is down — the optional Nautobot authorization source and its outage behavior
- The server + mock switch:
src/l2trace/radius/ - Manage secrets — the
env://URI aradius_secretuses - Why bitemporal? — why an access-control log wants two time axes