802.1X with FreeRADIUS on Cisco: The Full Lab, No ISE Required

Every 802.1X guide assumes you have Cisco ISE. You do not need it. Here is the whole chain built on FreeRADIUS and a Catalyst 9000v, with real Access-Accept output, a port flipping to Authorized, and the CML switch image that silently wastes your evening.

Switch CLI showing an 802.1X port in the Authorized state after a FreeRADIUS Access-Accept

Almost every 802.1X tutorial on the internet opens the same way: "first, log in to ISE." That is fine if you work somewhere that already owns a Cisco ISE deployment and can spare a policy set to experiment in. It is useless if you are studying at your kitchen table, because ISE is a licensed appliance with an appetite for RAM that no home lab is going to satisfy. The result is that the one security feature running on practically every enterprise access port stays theoretical for the people who most need to practise it.

You do not need ISE. 802.1X is an IEEE standard and RADIUS is an open protocol, and FreeRADIUS speaks both perfectly well. This article builds the whole chain from nothing: FreeRADIUS 3.2.7 on Debian 13, a Catalyst 9000v running IOS-XE 17.18.02 as the authenticator, and a real endpoint on a real access port. Every command works, and every block of output below was copied off that lab, including the moment the port flips to Authorized. If you want the theory of how port-based network access control fits together first, the pillar covers it; this piece is the build.

One warning before you spend an evening on it. If you lab this in CML, the switch image you pick decides whether any of it works, and two of the three obvious choices fail. That section comes first, deliberately.

Read this before you pick a switch image

802.1X is not a config you can practise on whatever L2 node you happen to have in your topology. I tested the same interface configuration on three CML images and got three different behaviours, only one of which is any use.

ioll2-xe, the everyday IOL-XE switch most people reach for, simply does not have the port authenticator. The global dot1x system-auth-control command is accepted, which is what makes this so misleading, but every interface-level command is thrown out:

SW(config-if)# authentication port-control auto
                              ^
% Invalid input detected at '^' marker.

SW(config-if)# dot1x pae authenticator
                    ^
% Invalid input detected at '^' marker.

SW(config-if)# mab
               ^
% Invalid input detected at '^' marker.

iosvl2 is worse, because it lies. The exact commands IOL-XE rejected are all accepted here without a murmur, they show up in show running-config, and nothing whatsoever happens. Wire a live endpoint to the port, bounce it, and you get this:

SW# show dot1x all summary
Interface       PAE     Client          Status
Gi0/1           AUTH    none            UNAUTHORIZED

SW# show authentication sessions interface Gi0/1
No sessions match supplied criteria.

! debug dot1x events during the interface bounce:
dot1x-ev:[Gi0/1] Interface state changed to UP
dot1x-ev:DOT1X Supplicant not enabled on GigabitEthernet0/1
dot1x-ev:[Gi0/1] No DOT1X subblock found for port down

The RADIUS server never receives a single Access-Request from that switch. The CLI parses into the running config but the authenticator state machine was never implemented, which is a silent no-op and considerably nastier than the honest rejection IOL-XE gives you. You will assume your shared secret is wrong, or your users file, or your supplicant, and you will be wrong about all three.

cat9000v-uadp is the one that works. It accepts the full stack, saves it, and the boot log confirms the machinery actually came up:

%AAA-6-METHOD_LIST_STATE: authen mlist ... of DOT1X service ... current state is : ALIVE
%AAA-6-RADIUS_SERVER_KEY_UPDATE: Radius server:PLZ key is updated
%RADIUS-4-NON_TLS_SERVER_CONFIGURED: RADIUS server PLZ is configured without tls/dtls
ioll2-xe
Interface CLIRejected
AuthenticatorNone
Access-Requests sentZero
VerdictFails loudly
iosvl2
Interface CLIAccepted
AuthenticatorNever starts
Access-Requests sentZero
VerdictFails silently
cat9000v-uadp
Interface CLIAccepted
AuthenticatorRuns
Access-Requests sentYes
VerdictUse this one

Use cat9000v-uadp, or real hardware, or a 9200/9300 if you can borrow one. Nothing else in a typical CML refplat set will authenticate a port. And note the interface naming, because it matters later: the Catalyst 9000v has real switchports at GigabitEthernet1/0/1 through 1/0/24, not IOL's Ethernet0/x.

The three roles, and which box is which

802.1X only has three participants and every troubleshooting session goes faster if you keep them straight.

  • Supplicant: software on the endpoint that speaks EAP over LAN. In this lab that is eapol_test from wpa_supplicant 2.10 on the Debian VM. On your desk it is the native Windows or macOS 802.1X client.
  • Authenticator: the switch. It is a relay and a gate, nothing more. It wraps the endpoint's EAP frames into RADIUS packets, forwards them, and enforces whatever the server answers. It does not know or check any credentials itself.
  • Authentication server: RADIUS. Here, FreeRADIUS on Debian. This is where identity actually lives, and it is the box ISE would otherwise be.

Two protocols on two different wires. EAP over LAN (EAPOL) runs between the endpoint and the switch, and RADIUS runs between the switch and the server. That split is why a shared-secret problem and a credentials problem look completely different when you go looking for them. If the vocabulary here is new, the basic dot1x port configuration on a Cisco switch is the shorter concept primer to read alongside this.

Build the RADIUS server

Install freeradius and freeradius-utils on Debian, then stop the service so you can run the daemon in the foreground where it prints every packet and every policy decision. That debug output is the single most useful diagnostic tool in the whole exercise.

sudo systemctl stop freeradius     # release UDP 1812/1813
sudo freeradius -X                 # debug mode, full protocol decode
ss -lunp | grep 181                # expect 0.0.0.0:1812 and 0.0.0.0:1813

Note the binary name. On Debian 13 it is /usr/sbin/freeradius, and the radiusd -X command you will find in every older guide does not exist. Same daemon, different packaging.

Next, tell the server which NAS devices are allowed to talk to it. Anything not listed in clients.conf gets ignored outright, which is a failure mode that produces total silence rather than a reject. In /etc/freeradius/3.0/clients.conf:

client pinglabz-lab-subnet {
    ipaddr = 192.168.99.0/24
    secret = PingLabzRAD123
    nas_type = cisco
    shortname = pinglabz-lab
}

A subnet is convenient in a lab, but in production you list each switch by address. The secret has to match the switch's key character for character.

Then the identities, in /etc/freeradius/3.0/users. Two entries, because this lab proves two paths:

! 802.1X supplicant
alice   Cleartext-Password := "PingLabz1x"
        Reply-Message = "RES-0004 dot1x accept for alice"

! MAB: Cisco sends the MAC as BOTH username and password,
! lowercase, no separators (default IETF format on IOS)
525400031ace  Cleartext-Password := "525400031ace"
        Reply-Message = "RES-0015 MAB accept for H1 by MAC"

Flat-file identities are a lab shortcut, obviously. Point the files module at LDAP or SQL later; the switch side does not change at all when you do, which is rather the point of RADIUS.

Prove the server before you touch a switch

Do not wire anything up yet. radtest lets you generate Access-Requests locally and confirm the server's decisions in isolation, so that when the switch does start sending packets you already know the server half is sound. Four commands cover every decision the deployment needs to get right.

radtest alice PingLabz1x 127.0.0.1 0 testing123

Sent Access-Request Id 181 from 0.0.0.0:35149 to 127.0.0.1:1812 length 75
	User-Name = "alice"
	User-Password = "PingLabz1x"
Received Access-Accept Id 181 from 127.0.0.1:1812 to 127.0.0.1:35149 length 71
	Reply-Message = "RES-0004 dot1x accept for alice"

That is the packet a switch needs to see before it will open a port. Now break it, twice, in two different ways:

radtest alice WrongPassword 127.0.0.1 0 testing123
Received Access-Reject Id 48 from 127.0.0.1:1812 to 127.0.0.1:39339 length 71

radtest ghost whatever 127.0.0.1 0 testing123
Received Access-Reject Id 213 from 127.0.0.1:1812 to 127.0.0.1:56137 length 38

Both are rejects, but look at the lengths. The wrong-password reject is 71 bytes and the unknown-user reject is 38. The difference is the Reply-Message: FreeRADIUS sets it during the authorize stage, which happens before the password is checked, so a user who exists but fails the password still gets their reply attribute echoed back on the reject. The ghost user matched no entry at all, so there is nothing to echo. Read the packet type, never the reply message. Plenty of people have stared at Reply-Message = "... accept for alice" inside an Access-Reject and concluded the server was broken.

Configure the switch

The authenticator config splits into three parts: AAA, the RADIUS server definition, and the port. Global first:

aaa new-model
aaa authentication login default local
aaa authorization exec default local          ! or your priv-15 user drops to priv-1
aaa authentication dot1x default group radius
aaa authorization network default group radius
!
dot1x system-auth-control                     ! the global on-switch for 802.1X
!
radius server PLZ
 address ipv4 192.168.99.100 auth-port 1812 acct-port 1813
 key PingLabzRAD123
!
ip radius source-interface Vlan1
radius-server attribute 6 on-for-login-auth
radius-server attribute 8 include-in-access-req

ip radius source-interface is not optional housekeeping. The source address of the Access-Request is what FreeRADIUS matches against clients.conf, and if the switch sources from an interface you did not authorise, the server drops the packet without answering. Pin it to the SVI you actually listed.

Then the port itself:

interface GigabitEthernet1/0/2
 switchport mode access
 authentication order dot1x mab
 authentication priority dot1x mab
 authentication port-control auto
 authentication violation restrict
 mab
 dot1x pae authenticator
 dot1x timeout tx-period 5
 spanning-tree portfast

authentication port-control auto is the line that arms the port. Without it you have a fully configured authenticator that never challenges anybody. authentication order dot1x mab says try 802.1X first and fall back to MAC Authentication Bypass, which is what handles printers and cameras that have no supplicant. dot1x timeout tx-period 5 shortens the EAPOL retry interval so the fallback happens in seconds instead of a minute and a half, which makes a lab far less tedious.

A real EAP exchange, end to end

802.1X is EAP, and it helps to watch a full handshake once before you start reading switch counters. The lab endpoint had no supplicant, so I drove a real PEAP/MSCHAPv2 conversation at FreeRADIUS with eapol_test instead, using the alice identity:

eapol_test -c eap.conf -a 127.0.0.1 -s testing123 -r0

CTRL-EVENT-EAP-STARTED EAP authentication started
CTRL-EVENT-EAP-PROPOSED-METHOD vendor=0 method=4 -> NAK
CTRL-EVENT-EAP-PROPOSED-METHOD vendor=0 method=25
CTRL-EVENT-EAP-METHOD EAP vendor 0 method 25 (PEAP) selected
CTRL-EVENT-EAP-PEER-CERT depth=0 subject='/CN=j-deb-vm'
EAP: Status notification: remote certificate verification (param=success)
CTRL-EVENT-EAP-SUCCESS EAP authentication completed successfully
MPPE keys OK: 1  mismatch: 0
SUCCESS

Read it as a negotiation. The server proposed method 4 (EAP-MD5), the supplicant NAKed it, and both sides settled on method 25 (PEAP). A TLS tunnel came up, the supplicant verified the server certificate, MSCHAPv2 ran inside the tunnel, and the exchange ended with EAP-SUCCESS and derived MPPE keys. On a wired port the switch relays exactly these EAP messages between EAPOL frames and RADIUS attributes without inspecting them. This is also where an EAP method mismatch bites in the real world: if the endpoint only offers EAP-TLS and the server has no certificate configuration for it, you get a failed negotiation, not a rejected password, and the two look nothing alike in the logs.

The port goes Authorized

Here is the moment the whole exercise exists for. A net-tools endpoint (MAC 52:54:00:f0:93:48) plugged into Gi1/0/2 on the Catalyst 9000v, with FreeRADIUS running in debug on the Debian VM:

SW9K# show authentication sessions interface GigabitEthernet1/0/2 details

            Interface:  GigabitEthernet1/0/2
          MAC Address:  5254.00f0.9348
            User-Name:  525400f09348
               Status:  Authorized
               Domain:  DATA
       Oper host mode:  single-host
    Common Session ID:  0C63A8C00000000E969AF179
Method status list:
       Method           State
        dot1x           Stopped          <-- 802.1X tried first, nobody answered
          mab           Authc Success    <-- fell back to MAB, RADIUS said yes

That method list is the entire story of a mixed access port. 802.1X was attempted, the endpoint had no supplicant to answer with, the authenticator gave up after its retries, and MAB took over. This is authentication order dot1x mab behaving exactly as designed, and it is why the fallback exists at all: what happens when the supplicant never answers is a whole topic of its own.

On the server side, this is the switch's own Access-Request, not a radtest simulation:

(2) Received Access-Request Id 3 from 192.168.99.12:61802 to 192.168.99.100:1812 length 291
(2)   User-Name = "525400f09348"
(2)   User-Password = "525400f09348"
(2)   Service-Type = Call-Check
(2)   Cisco-AVPair = "service-type=Call Check"
(2)   Cisco-AVPair = "method=mab"
(2)   Cisco-AVPair = "audit-session-id=0C63A8C00000000E969AF179"
(2)   NAS-IP-Address = 192.168.99.12
(2)   NAS-Port-Id = "GigabitEthernet1/0/2"
(2)   Calling-Station-Id = "52-54-00-F0-93-48"
(2)   Called-Station-Id  = "52-54-00-13-E9-16"
(2) eap: No EAP-Message, not doing EAP
(2) files: users: Matched entry 525400f09348 at line 226
(2) Found Auth-Type = PAP
(2) pap: User authenticated successfully
(2) Sent Access-Accept Id 3 from 192.168.99.100:1812 to 192.168.99.12:61802 length 83
(2)   Reply-Message = "RES-0015 live MAB accept for H9 on cat9000v"

Everything you need for troubleshooting is in that request. NAS-IP-Address tells you which switch asked, NAS-Port-Id names the exact interface, Calling-Station-Id is the endpoint MAC and Called-Station-Id is the switch port's own. And one line separates the two authentication paths cleanly: eap: No EAP-Message, not doing EAP, followed by Found Auth-Type = PAP. MAB is a bare PAP request the switch fabricates from the source MAC, with the MAC as both username and password. A genuine 802.1X request carries an EAP-Message attribute and Service-Type = Framed instead. Same server, same users file, two entirely different conversations, and knowing which one you are looking at saves an hour.

Once authorized, the switch pins the learned address:

SW9K# show mac address-table interface GigabitEthernet1/0/2
   1    5254.00f0.9348    STATIC      Gi1/0/2

And traffic passes, which it did not before. A ping from the endpoint to the switch SVI at 192.168.99.12 returned 3 packets transmitted, 3 received, 0% packet loss. Before authorization the port was unauthorized in single-host mode and the endpoint was cut off completely. Worth remembering that MAC-based authorization is only as trustworthy as the MAC, so pair it with stopping spoofed ARP and source addresses on an authorized port before you rely on it for anything sensitive.

What this was captured on

Authentication serverFreeRADIUS 3.2.7 on Debian 13 (trixie), 192.168.99.100
AuthenticatorCatalyst 9000v (cat9000v-uadp), IOS-XE 17.18.02, SVI Vlan1 192.168.99.12
Supplicanteapol_test (wpa_supplicant 2.10) for EAP; a net-tools endpoint for the port
Wiringbridge1 to Gi1/0/1 (RADIUS path), endpoint to Gi1/0/2 (dot1x + MAB)
Also testedioll2-xe and iosvl2, both unusable for 802.1X

The three ways this breaks

The shared secret does not match. This is the most common 802.1X-to-RADIUS failure, and it is invisible from the switch: you get no reply, the port times out, and nothing suggests a key problem. On the server, running in debug, a mismatched key shows up as a complaint about an invalid Message-Authenticator on the received packet. Diagnose it from the server side, always. And check the switch's source address against clients.conf at the same time, because an unlisted NAS produces the same symptom of total silence for a completely different reason.

The supplicant never answers. Very common and usually not a fault. The endpoint has no 802.1X client, or the client is disabled, or a user never entered credentials. The authenticator retries EAPOL-Identity-Request on its tx-period, gives up, and you see dot1x Stopped in the method list. What happens next depends entirely on your config: with MAB configured, the port falls back and authorizes by MAC; without it, the port stays shut. That is precisely why MAB exists, and if you run ISE rather than FreeRADIUS the MAB config for IOS-XE against ISE covers the same fallback with a different policy engine.

The port stays unauthorized. Work outward from the switch. show authentication sessions interface X details tells you whether a session exists at all; no session means the authenticator never started, which on a lab image usually means you picked the wrong platform. A session that reaches the server and comes back rejected is a credentials or EAP-method problem, and the FreeRADIUS debug trail names it exactly. A session where the switch sends and nothing comes back is a transport or shared-secret problem. Three symptoms, three separate places to look, and the debug output distinguishes them instantly.

Gotchas that cost me time

  • The aaa new-model privilege trap. After aaa new-model, a local user with privilege 15 gets dropped to privilege 1 on VTY unless you also configure aaa authorization exec default local. Symptom: SSH lands you at SW> instead of SW#, and automation dies with "Failed to enter enable mode". It looks exactly like a credentials problem and is not. This bit both lab switches.
  • The dot1x port boots err-disabled. The default violation action is shutdown, so Gi1/0/2 came up with %PM-4-ERR_DISABLE: security-violation error detected on Gi1/0/2 before anything was even plugged in. authentication violation restrict plus errdisable recovery cause security-violation fixes it, then shut/no-shut to recover the port.
  • Cable a switchport, not the management port. The Catalyst 9000v's first data interface is Gi0/0, a dedicated management port in Mgmt-vrf. Wire your external connector there (the obvious first-interface choice) and the bridge lands on the management VRF, so nothing reaches your SVI or your access port. Use a Gi1/0/x switchport instead. This wasted two lab rebuilds.
  • Skip enable secret on cat9000v in CML. Setting one makes PyATS-driven console access fail with "Bad Password". With no enable secret and aaa authorization exec default local in place, admin lands straight at a priv-15 prompt.
  • It is freeradius -X, not radiusd -X, on Debian 13. Ten minutes lost to a command that every tutorial written before 2020 still tells you to run.

Key takeaways

  • 802.1X does not require ISE. FreeRADIUS plus a Catalyst switch gives you a complete, working authenticator chain you can build at home in an evening.
  • In CML, only cat9000v-uadp implements the port authenticator. ioll2-xe rejects the CLI outright and iosvl2 accepts it and silently does nothing, which is the trap that wastes people's time.
  • Prove the server with radtest before you configure a single switch. It cleanly separates "my identities are wrong" from "my switch cannot reach RADIUS".
  • Read the RADIUS packet type, not the reply message: a Reply-Message set during authorize is echoed on an Access-Reject too.
  • One debug line separates the two paths. eap: No EAP-Message, not doing EAP plus Auth-Type = PAP means MAB; an EAP-Message attribute means real 802.1X.
  • dot1x Stopped followed by mab Authc Success in the method list is a silent supplicant handled correctly, not a failure.

The rest of the picture (host modes, guest and critical VLANs, dynamic VLAN assignment, RADIUS redundancy and phased rollout) builds on exactly this chain, just with more policy at the server end. Work through the full 802.1X cluster once you have this lab authorizing a port, because every one of those features is easier to reason about after you have watched an Access-Accept arrive and a port flip state.

Read next