IPSK on Cisco without ISE but FreeRADIUS

What is IPSK?

The concept is not new, other wireless vendors had this or similar features for a while (often named PPSK, DPSK, or MPSK, all with a bit different functionality), but some time ago Cisco released “Identity PSK”, or short, “IPSK”. It has been available on AireOS since version 8.5 and on the 9800 controller since the beginning (16.10) - I did my first experiments with it on AireOS 8.5 and made it into a new service on our campus on 16.10 back then.

As the name suggests, it is a PSK authentication, but not every client on the SSID has to have the same PSK. You can group them by department, or type, or can even give every single device its own PSK. Additionally, with dynamic VLAN assignment (which has been possible forever), this leads to great grouping and security zones. This is why this solution is so great for IOT:

  • They can typically not use 802.1X
  • You want to separate them, e.g. cameras, sensors, displays, weird printers
  • You don’t want to use a separate SSID for every type, because of SSID overhead

IPSK makes it possible to have this on one SSID:

One SSID - 3 Clients in 3 VLANs with 3 different PSKs instead of 1 SSID per device type or vendor

The solution is typically to use a Cisco ISE to configure the auth side of the equation. But it does not have to be ISE - any RADIUS implementation that can send some attributes will work.

This is why I chose to show you how it is done in the great open-source RADIUS server FreeRADIUS.

But first, we will have a look at how this works in the background to understand what we will need from our RADIUS server.

How does IPSK work?

On the configuration side, IPSK is simply MAC auth additional to WPA2-PSK, which means that the wireless controller will send a request to a RADIUS server containing the MAC address of the client, and the RADIUS server will respond with ACCESS-ACCEPT or ACCESS-REJECT. This is not some fancy proprietary thing or even EAP, this will just be a plain RADIUS auth with the MAC address as username and password. The difference to simple MAC auth (which has been possible forever) is that the RADIUS server will send additional attributes, containing the desired PSK for that client. AAA override needs to be enabled on the SSID for that, and to separate clients into different networks you can additionally send attributes containing a VLAN ID.

The MAC auth will take place during association, so before the 4-way handshake - so that the PSK is known to the controller in time.

Flow of client connecting with MAC auth

To the client, this looks just like a plain WPA2-PSK SSID - so there is no worry about compatibility, even small microcontrollers in the most basic IOT devices can do that. The “magic” all happens in the controller and the RADIUS server.

To be clear, the usual way this works is that the MAC has to be known to the RADIUS server beforehand, so it has to be “pre-registred”. There are exceptions - you can instruct to send ACCESS-ACCEPT to unknown MACs if you like. If you send no PSK, the PSK defined in the WLAN will be the one used for the client, or you can send a generic one - different ones per controller, or even on a lower level, for example per building.

Getting ACCESS-ACCEPT from RADIUS does however not mean that the client can join - the PSK has to match of course. RADIUS just looks up if it knows about the MAC, and if it does, sends back ACCEPT and the PSK for it. If the client has the wrong PSK configured, the SSID join will fail, just as it would on any WPA2-PSK SSID with mismatched PSK.

Why would I want different PSKs?

A bit of a tangent, but to bring the point across, because not everybody seems to be aware of it:

If you know the PSK of a WLAN, you can decrypt the traffic. You need to have “seen” (meaning captured) the join of the client to the AP, but the PSK is the only thing that does not get sent as plain text over the air, meaning as long as the PSK is secure, the connection is. But as soon as the PSK is known, all connections can be decrypted. Each client connection will have its own encryption key - the PTK - but with the data from the 4-way handshake of the connection, you can calculate it.

The PSK is the only thing not sent across the air and the only thing keeping the PTK a secret.

To be clear, I am talking about WPA2-PSK here. This issue was addressed in WPA3 with WPA3-SAE - where the passphrase is your key to join the WLAN, but the encryption key is being negotiated via a secure Diffie-Hellman exchange and elliptic curve cryptography. Even with the known passphrase, it is (as of today, and when correctly implemented) not possible to calculate the encryption key between the client and AP as a third person.

As - especially in the IOT space - the vast majority of clients are WPA2 only for now, IPSK is a great solution to bring multiple applications together under one SSID, that you would not otherwise (for example irrigation systems, heating control, humidity sensors and printers).

Getting FreeRADIUS to accept the MAC and return PSK attributes

Now that we have established what we need to do and why, let’s get to configuring.

This configuration will be done on a freshly installed Ubuntu Server 22.04. It is running as VM on ESXi.

First, we install the FreeRADIUS package.

stefan@freerad:~$ sudo apt install freeradius
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following additional packages will be installed:
  freeradius-common freeradius-config freeradius-utils freetds-common libct4 libdbi-perl libfreeradius3
  libtalloc2 libtevent0 libwbclient0 make ssl-cert
Suggested packages:
  freeradius-krb5 freeradius-ldap freeradius-mysql freeradius-postgresql freeradius-python3 snmp libclone-perl
  libmldbm-perl libnet-daemon-perl libsql-statement-perl make-doc
The following NEW packages will be installed:
  freeradius freeradius-common freeradius-config freeradius-utils freetds-common libct4 libdbi-perl
  libfreeradius3 libtalloc2 libtevent0 libwbclient0 make ssl-cert
0 upgraded, 13 newly installed, 0 to remove and 0 not upgraded.
Need to get 2835 kB of archives.
After this operation, 10.0 MB of additional disk space will be used.
Do you want to continue? [Y/n] y

[installing progress cut]

With this, FreeRADIUS is already installed and running. Fortunately FreeRADIUS generally ships with a working configuration - which means that if it doesn’t run, you configured something wrong - go back a step.

Now we will insert our MAC address “user” - the simplest way is to just put it into the users file. Now I know that this is not really a scalable solution, but for our little lab demo, it will do. More about scaling this solution at the end of this article.

We open the “users” file… (if you are not comfortable with “vi”, any editor will do)

stefan@freerad:~$ sudo vi /etc/freeradius/3.0/users

and insert these lines (of course with the MAC address you want to use):

ea855b5bb670    Cleartext-Password := "ea855b5bb670"
                Tunnel-Type = "VLAN",
                Tunnel-Medium-Type = "IEEE-802",
                Tunnel-Private-Group-ID = 100,
                Cisco-AVPair = "psk-mode=ascii",
                Cisco-AVPair += "psk=mylonglongpsk"

74f9caa7a433    Cleartext-Password := "74f9caa7a433"
                Tunnel-Type = "VLAN",
                Tunnel-Medium-Type = "IEEE-802",
                Tunnel-Private-Group-ID = 200,
                Cisco-AVPair = "psk-mode=ascii",
                Cisco-AVPair += "psk=differentpsk"

A couple of things to note: MAC as user and MAC in the “Cleartext-Password” have to match of course. In the first line, there is a “:=” because this is what will get checked when this user tries to authenticate. This line is done, so no comma. All of the following lines are stuff that gets sent back - so they have “=”, and a comma after each other because they are multiple items. The last line is kind of special - we have 2 Cisco-AVPair attributes, so we have to extend the first with “+=” (otherwise, it will be overwritten).

The attributes in detail:

  • The “tunnel” attributes are all about the VLAN config, type and medium-type have to be this way, private-group-id is the VLAN ID you want to use.
  • Our two Cisco-AVPairs are about the PSK - one set’s the mode ASCII (as we send it readable in ASCII), and the second one is the PSK itself (in format: psk=the_psk_you_want_to_use)

So now we have 2 clients defined, with different PSK and for different VLANs.

For our wireless controller to use our RADIUS server, it needs to be in the client list. So we will add it:

stefan@freerad:~$ sudo vi /etc/freeradius/3.0/clients.conf

and add a few lines with the IP of our controller and the secret we choose:

client wlc {
        ipaddr          =
        secret          = wlc123

Also, it is a good idea to log auth requests. This is a config flag in the radiusd.conf file:

stefan@freerad:~$ sudo vi /etc/freeradius/3.0/radiusd.conf

where we change “auth = no” to “auth = yes” in the “log” section to log auth results.

        #  Log all (accept and reject) authentication results to the log file.
        #  This is the same as setting "auth_accept = yes" and
        #  "auth_reject = yes"
        #  allowed values: {no, yes}
        auth = yes

To instruct FreeRADIUS to read the changed config, we will restart it:

stefan@freerad:~$ sudo service freeradius restart

Now to test our MAC entry in the users file, FreeRADIUS comes with the handy tool “radtest”, and our server itself - localhost - is already in the allowed clients list with the secret “testing123”, so we don’t need to worry to add it. A quick line sends the request, the same way the controller will:

stefan@freerad:~$ radtest ea855b5bb670 ea855b5bb670 localhost 1 testing123
Sent Access-Request Id 52 from to length 82
	User-Name = "ea855b5bb670"
	User-Password = "ea855b5bb670"
	NAS-IP-Address =
	NAS-Port = 1
	Message-Authenticator = 0x00
	Cleartext-Password = "ea855b5bb670"
Received Access-Accept Id 52 from to length 84
	Tunnel-Type:0 = VLAN
	Tunnel-Medium-Type:0 = IEEE-802
	Tunnel-Private-Group-Id:0 = "100"
	Cisco-AVPair = "psk-mode=ascii"
	Cisco-AVPair = "psk=mylonglongpsk"

This is exactly what we want - we send the MAC as username and password in the Access-Request and get the Access-Accept with our VLAN- and PSK attributes back.

Setting our C9800 controller up for IPSK

This was again done on an ESXi VM, this time running the C9800CL (version 17.9.3) with a few settings to get it running with a few VLANs and have an AP join.

The controller has the default VLAN for AP join (and management), and tagged VLAN 100 and 200 on it as additional client VLANs (terminating on a local OPNsense VM).

The small lab network we use. Our test clients should land in VL100 and VL200

For our IPSK SSID, we will populate a policy tag with a new policy and a new SSID. So as a first step, we will generate a new policy. Using the CLI (via ssh), it will look like this:

WLC#conf t
Enter configuration commands, one per line.  End with CNTL/Z.
WLC(config)#wireless profile policy ipsk
WLC(config-wireless-policy)# aaa-override
WLC(config-wireless-policy)# description "for our ipsk ssid"
WLC(config-wireless-policy)# vlan default
WLC(config-wireless-policy)# no shut

The important - non-default step - here is the enabled “aaa-override”, so the controller will accept the RADIUS attributes for the VLAN and the PSK. The default VLAN is okay for our lab, in a production environment you would not pick mgmt VLAN. And we will override it anyway.

Next is the definition of our FreeRADIUS server:

WLC(config)#aaa new-model
WLC(config)#radius server freerad
WLC(config-radius-server)# address ipv4 auth-port 1812 acct-port 1813
WLC(config-radius-server)# key 0 wlc123

Here we define the IP address of our RADIUS server, with the default auth port, and the key we set in the “clients.conf” on our server.

WLC(config)#aaa group server radius freerad-group 
WLC(config-sg-radius)# server name freerad
WLC(config-sg-radius)# subscriber mac-filtering security-mode mac

Putting our previously defined “freerad” server into the aaa group “freerad-group”

WLC(config)#aaa authorization network ipsk group freerad-group

Finally, this will define our “ipsk” authorization method, using our freerad-group. With that in place, we can define our IPSK SSID.

WLC(config)#wlan myIPSK 2 myIPSK
WLC(config-wlan)# mac-filtering ipsk
WLC(config-wlan)# security wpa psk set-key ascii 0 abcabcabcabc
WLC(config-wlan)# no security wpa akm dot1x
WLC(config-wlan)# security wpa akm psk
WLC(config-wlan)# no shutdown

This will generate the SSID “myIPSK” (ID 2), using WPA2-PSK as security method. The here-defined PSK would get used, if no one is set by RADIUS, but the client will be accepted. Depending on your specific configuration, this might never be the case. The important, non-default config here is the “mac-filtering”, which refers to “ipsk” - our just-set authorization method (linking our RADIUS server to this SSID).

As a final step, we will add our SSID and policy to a policy tag. Here in the lab, the default policy tag will do - the test AP is configured for it:

WLC(config)#wireless tag policy default-policy-tag
WLC(config-policy-tag)#wlan myIPSK policy ipsk

This links everything together - policy tag is on AP, SSID with mac filtering to our FreeRADIUS server and policy with AAA override merged together to enable IPSK.

For reference, a few screenshots of how this would look on the Web GUI:

Configuration -> Tags & Profiles -> Policy - make new policy and enable "AAA override" under "Advanced"

Configuration -> Security -> AAA -> Servers / Groups - RADIUS -> Servers - add RADIUS server

Configuration -> Security -> AAA -> Servers / Groups - RADIUS -> Server groups - add new group and add server from above

Configuration -> Security -> AAA -> AAA Method List -> Authorization - add new Authorization List and assign server group from above

Configuration -> Tags & Profiles -> WLANs -> Add - add new WLAN, under Security -> Layer2 enable WPA+WPA2, MAC Filtering with Authorization List from above, enable PSK and set a default PSK

Testing it out

The AP is now broadcasting the new SSID:

SSID beacons picked up on my iPhone

Now, since I have already an iPhone here running, one important thing especially about phones:

Most modern mobile OS will use randomized MAC addresses. This will of course interfere with registered MACs. So you have to either turn it off (click on the little “i” on the iPhone will give you the option to turn it off for this SSID) so you can use the device MAC, or use the generated one for this SSID (also hidden behind the “i”). As of now, the MAC should stay the same for an SSID, but this could change with future versions. Android has a similar feature enabled by default. Windows has the ability, but it is turned off by default.

As the first test, we instruct the iPhone to join this SSID with our defined PSK. The RADIUS server logs the auth:

stefan@freerad:~$ tail /var/log/freeradius/radius.log
Sun Sep 10 15:12:48 2023 : Auth: (0) Login OK: [ea855b5bb670] (from client wlc port 7 cli ea-85-5b-5b-b6-70)

Log from the 9800 controller:

Sep 10 15:12:49.003: %CLIENT_ORCH_LOG-6-CLIENT_ADDED_TO_RUN_STATE: Chassis 1 R0/0: wncd: Username entry (ea855b5bb670) joined with ssid (myIPSK) for device with MAC: ea85.5b5b.b670

For our second client, I started FreeRADIUS in debug mode, to see it in action:

(0) Received Access-Request Id 3 from to length 372
(0)   User-Name = "74f9caa7a433"
(0)   User-Password = "74f9caa7a433"
(0) Login OK: [74f9caa7a433] (from client wlc port 7 cli 74-f9-ca-a7-a4-33)
(0) Sent Access-Accept Id 3 from to length 83
(0)   Tunnel-Type = VLAN
(0)   Tunnel-Medium-Type = IEEE-802
(0)   Tunnel-Private-Group-Id = "200"
(0)   Cisco-AVPair = "psk-mode=ascii"
(0)   Cisco-AVPair = "psk=differentpsk"
(0) Finished request

And ran a radioactive trace on the controller, with the interesting lines:

2023/09/10 16:25:32.981129344 {wncd_x_R0-0}{1}: [client-orch-sm] [14129]: (note): MAC: 74f9.caa7.a433  Association received. BSSID 10f9.20bd.da01, WLAN myIPSK, Slot 0 AP 10f9.2012.2334, AP10f9.2012.2334
2023/09/10 16:25:32.981345890 {wncd_x_R0-0}{1}: [client-orch-state] [14129]: (note): MAC: 74f9.caa7.a433  Client state transition: S_CO_INIT -> S_CO_ASSOCIATING
2023/09/10 16:25:32.981782853 {wncd_x_R0-0}{1}: [client-orch-state] [14129]: (note): MAC: 74f9.caa7.a433  Client state transition: S_CO_ASSOCIATING -> S_CO_MACAUTH_IN_PROGRESS
2023/09/10 16:25:32.981800098 {wncd_x_R0-0}{1}: [client-auth] [14129]: (note): MAC: 74f9.caa7.a433  MAB Authentication initiated. Policy VLAN 0, AAA override = 1, NAC = 0
2023/09/10 16:25:32.986471032 {wncd_x_R0-0}{1}: [client-auth] [14129]: (note): MAC: 74f9.caa7.a433  MAB Authentication success. AAA override = True, PPSK = True
2023/09/10 16:25:32.995506240 {wncd_x_R0-0}{1}: [client-orch-state] [14129]: (note): MAC: 74f9.caa7.a433  Client state transition: S_CO_MACAUTH_IN_PROGRESS -> S_CO_ASSOCIATING
2023/09/10 16:25:33.049359828 {wncd_x_R0-0}{1}: [client-auth] [14129]: (note): MAC: 74f9.caa7.a433  L2 PSK Authentication Success. EAP type: NA, Resolved VLAN: 200, Audit Session id: DE4711AC0000001046527677
2023/09/10 16:25:34.480733959 {wncd_x_R0-0}{1}: [client-iplearn] [14129]: (note): MAC: 74f9.caa7.a433  Client IP learn successful. Method: DHCP IP:
2023/09/10 16:25:34.481969822 {wncd_x_R0-0}{1}: [client-orch-state] [14129]: (note): MAC: 74f9.caa7.a433  Client state transition: S_CO_IP_LEARN_IN_PROGRESS -> S_CO_RUN

And the controller status of our two clients:

Both clients online, in different VLANs

or on the CLI:

WLC#sh wireless client summary 
Number of Clients: 2

MAC Address    AP Name                                        Type ID   State             Protocol Method     Role
74f9.caa7.a433 AP10F9.2012.2334                               WLAN 2    Run               11n(2.4) MAB        Local             
ea85.5b5b.b670 AP10F9.2012.2334                               WLAN 2    Run               11ax(5)  MAB        Local      
Leases on our OPNsense router, in different VLANs

What now?

Now that we have accomplished our goal of using IPSK on our C9800 Controller with FreeRADIUS - what would be next?

Well - if you wondered how this can possibly scale, you had the right thought. Adding MAC addresses to a text file, copying multiple lines of text, and restarting the server, does not scale.

What we can do, is use the versatility of FreeRADIUS - the “clients” text file is just one of many possible backends. You can use an SQL DB, Active Directory, LDAP, and run perl or python scripts - all you need to configure is how to get to the backend, and where to find the attributes we will need.

Most institutions already have some sort of auth backend and some management application. This could be some asset administration, user DB, you can probably get it also in some ERP applications. In my case, I already had an application for generating Wi-Fi guest accounts, that stores users in LDAP. Extending the LDAP scheme to accompany a VLAN ID and the PSK was easy, and with a few hundred lines of code, I got the application to be a multi-tenant solution, where the “administrative” user (for example, the person responsible for smart room signs) can add MAC addresses with PSKs to their assigned VLAN - but not to any other.

How this could look:

Viewing one CN in LDAP using an LDAP explorer

The LDAP scheme has been extended to include VLAN and PSK. The CN is the MAC address.

The application looks like this:

administration app

The administrative user logs in, picks the IPSK group he wants to add an entry (if he has more than one), and enters MAC address, desired PSK and some free text comment. The VLAN can not be changed and is fixed for the certain group. The PSK can be viewed by clicking on the little “eye”. Changes are instantly written to LDAP, new entries can be used seconds later.

The RADIUS config for this is of course highly dependent on your setup. But to give you an idea, this is how a basic FreeRADIUS IPSK in a separate virtual server can look.

For the LDAP part:

ldap ldap-ipsk {
        server = 'ldaps://ldap-server:636'
        identity = 'cn=myldapjoinuser,o=infrastructure'
        password = 'ldappw' 
        base_dn = 'ou=ipsk,o=acmecorp,c=at'

        update {
                reply:Tunnel-Private-Group-ID   := 'SysVlanID'
                reply:Cisco-avpair              += 'SysIpsk' 
        user {
                base_dn = "${..base_dn}"
                filter = "(cn=%{%{Stripped-User-Name}:-%{User-Name}})"

That is just an own configured instance of the LDAP module, telling the server how to log in and where to find the “user” (meaning MAC addresses). If it is found, the Tunnel-Private-Group-ID and the Cisco-avpair will be populated with VLAN ID and PSK.

The virtual server can look like this:

server ipsk {
        authorize {
                if (!updated) {
                else {
                        update control {
                                Auth-Type := Accept
         authenticate {
         post-auth {
                update {
                        reply:Tunnel-Type := "VLAN"
                        reply:Tunnel-Medium-Type := "IEEE-802"
                        reply:Cisco-avpair += "psk-mode=ascii"
                Post-Auth-Type REJECT {

In the authorize section after preprocessing, we call the previously defined ldap-ipsk module. If it finds the user it will reply with the VLAN and PSK attributes. If it does not, we will reject the request. Why is this like that? Because I removed the password check - it does not make sense here - the password is always the username MAC, and I do not store it as a password in LDAP. If it has been updated, we accept the request.

If I would want some other logic (accept everyone and set PSK based on some other logic then), here would be the place to accept anyway.

In the post-auth, I insert the other attributes that do not change into the reply.

FreeRADIUS is complex, but also very versatile. It is possible to write back to LDAP about the last successful login for example. The possibilities are endless.

I hope this article has been informative for you.