I was studying PKI for my SSCP and got bored enough to actually use it for something. Two new UniFi APs were sitting in a box waiting to be deployed, and I had a working private CA I'd already built for other projects. Seemed like a fun weekend project.
Starting With PEAP
The plan going in was PEAP-MSCHAPv2, mostly because I didn't want to install client certificates on every device my family owns. Usernames and passwords, while slightly less secure seemed reasonable enough.
I installed FreeRADIUS on a small Debian LXC:
apt install freeradius
Verified the base install first, before touching anything EAP-specific:
radtest testuser testpass123 localhost 0 testing123
Then moved to eapol_test, a tool that simulates a full wireless client's EAP exchange without needing an actual AP:
apt install eapoltest
network={
ssid="test"
key_mgmt=WPA-EAP
eap=PEAP
identity="testuser"
password="testpass123"
phase2="auth=MSCHAPV2"
}
The debug output was pretty satisfying to watch. Full TLS handshake, server certificate exchange, encrypted inner MSCHAPv2, and finally MPPE keys OK: 1 mismatch: 0. Hell yeah. Auth works, before I'd touched a single piece of hardware.
Along the way I found a warning buried in FreeRADIUS's own config comments, in block capitals, about Windows clients silently failing if the server certificate doesn't have the right Extended Key Usage:
extendedKeyUsage = serverAuth
Since I wanted to make sure this worked, before moving on to the Unifi APs, I tested using an old Netgear AP I had laying around. I connected my Android phone, then my sister's Windows 11 laptop. Both worked cleanly :)
On the OpenBSD router, RADIUS needed its own narrow allowance, UDP, specific AP, specific server, nothing more:
pass in quick on $int_if inet proto udp from <ap-ip> to <radius-server-ip> port { 1812 1813 }
Realizing PEAP Wouldn't Cut It
Once I had real devices connected, potential issues became obvious: my printer doesn't support 802.1X at all. Neither do some of the older smart TVs. Every phone and laptop in the house does. That's not really a PEAP problem or an EAP-TLS problem, it's a hardware limitation, so I had to figure out a workaround.
Given that split, PEAP stopped making sense as the primary method. If every capable device could just use a certificate instead of a password, there was no reason to keep a password-based fallback in the mix. The printer and anything else without 802.1X support would get its own WPA3-Personal network instead. Which although I wanted to avoid password authentication entirely, this was one sacrifice I had to make. I ensured the network is isolated, with a strong random passphrase; a completely reasonable security posture on its own merits, just not one I originally planned.
I generated client certs for the devices that support them, using a template I'd already built for other projects:
openssl genrsa -out devicename.key 4096
openssl req -new -key devicename.key -out devicename.csr -config client-cert-template.cnf
openssl x509 -req -days 730 -in devicename.csr -CA ca.crt -CAkey ca.key \
-CAcreateserial -out devicename.crt -extensions v3_req -extfile client-cert-template.cnf
Something I Actually Learned
Disabling PEAP should have been simple. Comment out the block, restart, done. I tested it, and PEAP still authenticated, it just failed afterward because the password entries were gone.
Turns out commenting out just the opening line of a config block doesn't reliably disable it, the closing brace and everything else was still sitting there, structurally intact from FreeRADIUS's parser's point of view. The actual fix was going through and commenting out every EAP sub-module individually, not just the one I wanted gone:
default_eap_type = tls
with peap { }, ttls { }, mschapv2 { } and the rest commented out in full, not just their opening lines. Restarted, checked the debug output on startup, and saw exactly one line: Linked to sub-module rlm_eap_tls. Nothing else. Perfect. Misconfiguration fixed :)
Certificates, Identity, and a Small Mystery
Generating client certificates was the easy part, I already had templates from other projects. Bundling them for Android/iOS specifically required learning something new, a single .p12 file containing both the cert and its private key together.
openssl pkcs12 -export -out devicename.p12 -inkey devicename.key -in devicename.crt -certfile ca.crt
I got a little confused during testing. My phone kept failing with a vague "authentication problem," despite a correctly signed, correctly bundled certificate. The fix turned out to be the Identity field in Android's WiFi setup, it needs to match the certificate's CN exactly. Once I entered the right value, the connection succeeded immediately.
That led to one more thing worth doing properly. Buried in FreeRADIUS's config, commented out by default:
check_cert_cn = %{User-Name}
A setting that explicitly enforces the claimed identity actually matches the certificate presented, rather than relying on incidental behavior elsewhere in the config. I tested it deliberately both ways, wrong identity, confirmed rejection, correct identity, confirmed success. Small thing, but if I'm deploying strict identity management, I may as well do it right.
Pretty Happy with the Result
I have three networks now, each matched to what the device can actually do. Phones and laptops authenticate with individual certificates. The printer and a couple of older smart TVs sit on a separate WPA3-Personal network with a long, random passphrase. Actual guests use the ISP's own equipment entirely, a physically separate connection with its own public IP, nowhere near anything I run.
Nobody in my house can hand someone the WiFi password anymore, because there isn't one. If I ever need to cut a device off, I pull its entry from the server and it's gone. It feels strangely satisfying doing something most small businesses don't even bother with, and it started because I was bored studying for an exam.