Setting up Freeradius and Samba AD for WPA2 WiFi Auth
Warning: This configuration works on a Debian 12 setup, has not been tested on Debian 13.
There are several steps to make this work. This is not a pure Samba article, but I think it's good to have it here in my notebook for my reference. Radius is quite a complex system; it took me a while to make things work. Most of this has been possible with the help of Kees Van Vloten and his Freeradius Enterprise WiFi notes. They are presented here in a different fashion.
Steps to follow:
- You already have a Samba member server joined to the domain. See Setting up a Member Server.
- You have created an AD permission group (named 'MAD\wifi' in our example) that contains all Windows machines allowed to connect to WiFi.
- Configure Samba to allow ntlm_auth to use mschapv2 and ntlmv2.
- Create a PKI infrastructure to generate and maintain certificates.
- Install and configure Freeradius on a member server.
- Install the CA certificate on Windows clients.
Configure Samba to allow ntlm_auth to use mschapv2 and ntlmv2
This one is pretty straightforward. Add the following line to `/etc/samba/smb.conf` in *all* your DCs:
ntlm auth = mschapv2-and-ntlmv2-only
Save and restart Samba AD DC:
/etc/init.d/samba-ad-dc restart
Install EasyRSA to manage your PKI
EasyRSA is part of the OpenVPN group and allows you to very easily manage a simple (internal only) Public Key Infrastructure. See EasyRSA.
You do not need root privileges to run EasyRSA. This is a very simple collection of programs; it can be run on any machine or server, and it has no dependencies on Samba or Freeradius.
git clone https://github.com/OpenVPN/easy-rsa cd ~/easy-rsa/easyrsa3 cp vars.example vars
Edit `./vars` file and have:
set_var EASYRSA_REQ_COUNTRY "ES" set_var EASYRSA_REQ_PROVINCE "Madrid" set_var EASYRSA_REQ_CITY "Madrid" set_var EASYRSA_REQ_ORG "Caponato Enterprises" set_var EASYRSA_REQ_EMAIL "capo@caponato.es" set_var EASYRSA_REQ_OU "ITS"
In order to have a location for a CDP — Certificate Revocation List Distribution Point — for your internal Certification Authority, edit `./x509-types/COMMON` and:
authorityInfoAccess = caIssuers;URI:http://pki.caponato.es/pki/ca.crt crlDistributionPoints = URI:http://pki.caponato.es/cdp/crl.pem
You need to set up a basic webserver, create a virtual host (in our example `pki`), and upload those files (`ca.crt` and `crl.pem`) when they are issued. Setting up the webserver is beyond the scope of this article.
See for better and longer explanations.
I named my Radius server `radius-1`:
cd ~/easy-rsa/easyrsa3 ./easyrsa init-pki ./easyrsa build-ca ./easyrsa gen-req radius-1 nopass ./easyrsa sign-req server radius-1 ./easyrsa gen-crl
Upload your CA and CRL to your PKI webserver. Please note your destination directories in the webserver will be different.
Please note, if you revoke any certificate, you must generate a new CRL (Certificate Revocation List) file and redistribute it to your PKI webserver. Create your CA / CRL combo file and redistribute.
cd ~/easy-rsa/easyrsa3 rsync ./pki/ca.crt pki:/var/www/html/pki/ rsync ./pki/crl.pem pki:/var/www/html/cdp/
Copy your newly generated certificates to your Radius server: private key, public key, CA cert, and CRL certificates:
cd ~/easy-rsa/easyrsa3 rsync -av ./pki/private/radius-1.key radius-1:/etc/ssl/private/ rsync -av ./pki/issued/radius-1.crt radius-1:/etc/ssl/certs/ rsync -av ./pki/ca.crt radius-1:/etc/ssl/certs/ca-caponato.pem rsync -av ./pki/crl.pem radius-1:/etc/ssl/certs/crl-caponato.pem # Please note CA and CRL certificates have been renamed for clarity. # Please note CA cert extension has been changed to pem — equivalent to crt.
Log on to your `radius-1` server and set up permissions for certificates correctly:
chmod 640 /etc/ssl/private/radius-1.key chgrp ssl-cert /etc/ssl/private/radius-1.key chmod 644 /etc/ssl/certs/radius-1.crt chmod 644 /etc/ssl/certs/ca-caponato.pem chmod 644 /etc/ssl/certs/crl-caponato.pem
Your certificates are all set and ready for Freeradius!
Renewing an Expired RADIUS Server Certificate
This note covers renewing a RADIUS server certificate that has expired or is about to expire. The procedure renews the server certificate while keeping the same CA, so Windows clients require no changes — they keep trusting whatever new cert the CA signs.
If the CA itself has expired, this guide does not apply — you would need to issue a new CA and redistribute it via GPO to every domain client.
Symptoms
In /var/log/freeradius/radius.log:
ERROR: (TLS) Alert read:fatal:certificate expired Auth: Login incorrect (eap_peap: (TLS) Alert read:fatal:certificate expired): [user] (from client unifi-ap ...)
Also large numbers of Cannot continue, as the peer is misbehaving errors from Windows clients — same root cause, Windows just tears down the TLS session rather than sending a clean alert.
Diagnosis
On the RADIUS server, check the server cert, the CA cert, and the CRL:
openssl x509 -in /etc/ssl/certs/radius-1.crt -noout -subject -issuer -dates openssl x509 -in /etc/freeradius/3.0/ca_and_crl.pem -noout -subject -issuer -dates openssl crl -in /etc/freeradius/3.0/ca_and_crl.pem -noout -lastupdate -nextupdate
If the server cert is expired but the CA is still valid → continue. If the CRL is also near expiry, regenerate it as part of this work (check_crl = yes makes an expired CRL break PEAP the same way).
Bump validity in vars (one-off, recommended)
EasyRSA defaults are 825 days for certs and 180 days for the CRL. Increase them before reissuing:
cd ~/easy-rsa/easyrsa3 sed -i 's/^#set_var EASYRSA_CERT_EXPIRE.*/set_var EASYRSA_CERT_EXPIRE 3650/' vars sed -i 's/^#set_var EASYRSA_CRL_DAYS.*/set_var EASYRSA_CRL_DAYS 3650/' vars grep -E 'EASYRSA_CERT_EXPIRE|EASYRSA_CRL_DAYS' vars
Both lines should appear without a leading #.
Revoke and reissue
Run commands one at a time. Each command that touches the CA prompts for the CA passphrase — if you paste a block, the next command becomes the (wrong) passphrase and revoke/gen-crl fail with bad decrypt errors.
1. Revoke the old cert
cd ~/easy-rsa/easyrsa3 ./easyrsa revoke radius-1
Type yes at confirmation, then enter the CA passphrase. The old cert/key/req are moved to pki/revoked/.
2. Generate a fresh CRL
./easyrsa gen-crl
Enter CA passphrase. The new pki/crl.pem now lists the revoked serial.
3. Generate a new request
./easyrsa gen-req radius-1 nopass
Press Enter at every DN prompt to accept the defaults. When reissuing radius-2, type the correct hostname at the Common Name prompt — the default in brackets may show the previous CN.
4. Sign the request
./easyrsa sign-req server radius-1
Type yes to confirm, then enter the CA passphrase.
5. Verify
openssl x509 -in pki/issued/radius-1.crt -noout -subject -dates openssl crl -in pki/crl.pem -noout -lastupdate -nextupdate
Cert notAfter and CRL nextUpdate should both be ~10 years in the future.
Distribute the new files
To radius-1
From the PKI server:
cd ~/easy-rsa/easyrsa3 rsync -av ./pki/private/radius-1.key radius-1:/etc/ssl/private/ rsync -av ./pki/issued/radius-1.crt radius-1:/etc/ssl/certs/ rsync -av ./pki/crl.pem radius-1:/etc/ssl/certs/crl-caponato.pem
On radius-1:
chmod 640 /etc/ssl/private/radius-1.key chgrp ssl-cert /etc/ssl/private/radius-1.key chmod 644 /etc/ssl/certs/radius-1.crt chmod 644 /etc/ssl/certs/crl-caponato.pem cat /etc/ssl/certs/ca-caponato.pem /etc/ssl/certs/crl-caponato.pem > /etc/freeradius/3.0/ca_and_crl.pem
Restart and verify
systemctl restart freeradius systemctl status freeradius --no-pager
For deeper verification, stop the service and run in debug mode to watch a real client authenticate cleanly:
systemctl stop freeradius freeradius -X
Look for Login OK and Sent Access-Accept lines, then Ctrl-C and start the service.
Install the CA certificate on Windows clients
Windows clients need to trust the Radius server's certificate. The easiest way is to deploy the CA certificate via GPO.
- Open Group Policy Management on your DC.
- Create or edit a GPO linked to the OU containing your computers.
- Navigate to: Computer Configuration → Policies → Windows Settings → Security Settings → Public Key Policies → Trusted Root Certification Authorities.
- Right-click → Import → select your
ca.crtfile.
The CA certificate will be pushed to all domain-joined machines on the next Group Policy refresh.
You can force an immediate refresh on a client with:
gpupdate /force
To verify, open certmgr.msc on a client machine and check that your CA appears under Trusted Root Certification Authorities.
Install and configure Freeradius on a member server
Install Freeradius, and add the `freerad` user to Winbind and SSL groups:
apt-get install freeradius freeradius-common freeradius-utils makepasswd usermod -a -G winbindd_priv,ssl-cert freerad
Configuration steps:
Create a combined CA and CRL certificate:
cat /etc/ssl/certs/ca-caponato.pem /etc/ssl/certs/crl-caponato.pem > /etc/freeradius/3.0/ca_and_crl.pem
Configure Freeradius
Configure clients.conf
This allows your APs to talk to the Radius service.
For a single AP, add this to `/etc/freeradius/3.0/clients.conf`:
# For each access-point add:
client <AP_HOSTNAME> {
ipaddr = <AP_IPADDRESS>
netmask = 32
secret = <AP_SECRET>
shortname = <AP_HOSTNAME>
}
Or, if you have a subnet with all APs in it — like my setup — you can do:
# For all access-points add:
client unifi-ap {
ipaddr = 192.168.254.0/24
secret = radius-24
shortname = unifi-ap
}
Update proxy.conf:
Pay attention to the line, at the end of the file:
realm mad.caponato.es
And adjust to your setup.
/etc/freeradius/3.0/proxy.conf
proxy server {
default_fallback = no
}
home_server localhost {
type = auth
ipaddr = 127.0.0.1
port = 1812
secret = testing123
response_window = 20
zombie_period = 40
revive_interval = 120
status_check = status-server
check_interval = 30
check_timeout = 4
num_answers_to_alive = 3
max_outstanding = 65536
coa {
irt = 2
mrt = 16
mrc = 5
mrd = 30
}
limit {
max_connections = 16
max_requests = 0
lifetime = 0
idle_timeout = 0
}
}
home_server_pool samba_auth_failover {
type = fail-over
home_server = localhost
}
realm mad.caponato.es {
auth_pool = samba_auth_failover
}
realm LOCAL {
}
Create the sites samba_default and samba_inner-tunnel
nano /etc/freeradius/3.0/sites-available/samba_default
######################################################################
#
# As of 2.0.0, FreeRADIUS supports virtual hosts using the
# "server" section, and configuration directives.
#
# Virtual hosts should be put into the "sites-available"
# directory. Soft links should be created in the "sites-enabled"
# directory to these files. This is done in a normal installation.
#
# If you are using 802.1X (EAP) authentication, please see also
# the "inner-tunnel" virtual server. You will likely have to edit
# that, too, for authentication to work.
#
######################################################################
server default {
listen {
type = auth
ipaddr = *
port = 1812
limit {
max_connections = 16
lifetime = 0
idle_timeout = 30
}
}
listen {
ipaddr = *
port = 1813
type = acct
limit {
max_connections = 16
lifetime = 0
idle_timeout = 30
}
}
# IPv6 versions of the above - read their full config to understand options
authorize {
filter_username
preprocess
eap {
ok = return
#updated = return
}
}
authenticate {
eap
}
preacct {
preprocess
}
accounting {
}
post-auth {
update {
&reply: += &session-state:
}
# For Exec-Program and Exec-Program-Wait
exec
# Remove reply message if the response contains an EAP-Message
remove_reply_message_if_eap
Post-Auth-Type REJECT {
attr_filter.access_reject
# Insert EAP-Failure message if the request was rejected by policy instead of because of an authentication failure
eap
# Remove reply message if the response contains an EAP-Message
remove_reply_message_if_eap
}
}
post-proxy {
# You MUST also use the 'nostrip' option in the 'realm' configuration. Otherwise, the User-Name attribute
# in the proxied request will not match the user name hidden inside of the EAP packet, and the end server will
# reject the EAP request.
eap
}
}
nano /etc/freeradius/3.0/sites-available/samba_inner-tunnel
######################################################################
#
# This is a virtual server that handles *only* inner tunnel
# requests for EAP-TTLS and PEAP types.
#
######################################################################
server inner-tunnel {
listen {
ipaddr = 127.0.0.1
port = 18120
type = auth
}
authorize {
filter_username
mschap
update control {
&Proxy-To-Realm := LOCAL
}
eap {
ok = return
}
}
authenticate {
Auth-Type MS-CHAP {
mschap
}
eap
}
post-auth {
Post-Auth-Type REJECT {
attr_filter.access_reject
update outer.session-state {
&Module-Failure-Message := &request:Module-Failure-Message
}
}
}
post-proxy {
eap
}
}
Remove default sites, and create the symlinks for the Samba sites
# Remove default sites rm /etc/freeradius/3.0/sites-enabled/default rm /etc/freeradius/3.0/sites-enabled/inner-tunnel # Enable Samba sites ln -s /etc/freeradius/3.0/sites-available/samba_default /etc/freeradius/3.0/sites-enabled/samba_default ln -s /etc/freeradius/3.0/sites-available/samba_inner-tunnel /etc/freeradius/3.0/sites-enabled/samba_inner-tunnel
Configure mschap
Make sure you correctly update the lines:
- -domain=MAD \
- -require-membership-of=MAD\wifi \
to your setup.
nano /etc/freeradius/3.0/mods-available/mschap
# Microsoft CHAP authentication
#
# This module supports MS-CHAP and MS-CHAPv2 authentication.
# It also enforces the SMB-Account-Ctrl attribute.
#
mschap {
use_mppe = yes
with_ntdomain_hack = yes
require_encryption = yes
require_strong = yes
ntlm_auth = "/usr/bin/ntlm_auth --request-nt-key \
--allow-mschapv2 \
--domain=MAD \
--require-membership-of=MAD\wifi \
--username=%{%{mschap:User-Name}:-00} \
--challenge=%{%{mschap:Challenge}:-00} \
--nt-response=%{%{mschap:NT-Response}:-00}"
pool {
start = ${thread[pool].start_servers}
min = ${thread[pool].min_spare_servers}
max = ${thread[pool].max_servers}
spare = ${thread[pool].max_spare_servers}
uses = 0
retry_delay = 30
lifetime = 86400
cleanup_interval = 300
idle_timeout = 600
}
passchange {
}
}
Configure eap
nano /etc/freeradius/3.0/mods-available/eap
eap {
default_eap_type = mschapv2
timer_expire = 60
ignore_unknown_eap_types = no
cisco_accounting_username_bug = no
max_sessions = ${max_requests}
tls-config tls-common {
#private_key_password = whatever
private_key_file = /etc/ssl/private/radius-1.key
certificate_file = /etc/ssl/certs/radius-1.crt
ca_file = /etc/freeradius/3.0/ca_and_crl.pem
ca_path = ${cadir}
check_crl = yes
cipher_list = "DEFAULT"
cipher_server_preference = no
tls_min_version = "1.2"
# FreeRADIUS only supports TLS 1.3 for special builds of wpa_supplicant and Windows
tls_max_version = "1.2"
ecdh_curve = "prime256v1"
cache {
enable = no
store {
Tunnel-Private-Group-Id
}
}
verify {
}
}
tls {
tls = tls-common
}
peap {
tls = tls-common
default_eap_type = mschapv2
copy_request_to_tunnel = no
use_tunneled_reply = no
virtual_server = "inner-tunnel"
}
mschapv2 {
}
}
Enable authentication logging
Edit /etc/freeradius/3.0/radiusd.conf and set auth = yes inside the log {} section. This ensures that authentication accept/reject events are logged to /var/log/freeradius/radius.log when running as a daemon.
Find the log section and change:
log {
...
auth = yes
...
}
By default auth is set to no, which means the daemon logs nothing about authentication events.
Enable required modules only
# Remove all default modules rm /etc/freeradius/3.0/mods-enabled/* # Enable only what's needed cd /etc/freeradius/3.0/mods-enabled for mod in always attr_filter eap exec expr files linelog mschap preprocess realm utf8; do ln -s ../mods-available/$mod .; done
Enable and start Freeradius:
systemctl enable freeradius systemctl start freeradius
Setting up a second Radius server for resilience
A second Radius server provides failover in case the primary becomes unavailable. The UniFi controller allows you to configure a primary and secondary Radius server per SSID.
Prerequisites
- A second Samba member server joined to the domain (e.g.
radius-2). - The
freeraduser added towinbindd_privandssl-certgroups. - A separate certificate issued for
radius-2via EasyRSA, copied to the right place in radius-2 server. - The CA certificate copied on to
radius-2
Set permissions on radius-2:
chmod 640 /etc/ssl/private/radius-2.key chgrp ssl-cert /etc/ssl/private/radius-2.key chmod 644 /etc/ssl/certs/radius-2.crt
Create the combined CA and CRL file:
cat /etc/ssl/certs/ca-caponato.pem /etc/ssl/certs/crl-caponato.pem > /etc/freeradius/3.0/ca_and_crl.pem
Sync configuration from radius-1
All Freeradius configuration is identical between nodes except for the certificate paths in the eap module. Sync everything except that file:
rsync -av --delete radius-1:/etc/freeradius/ /etc/freeradius/
Then edit /etc/freeradius/3.0/mods-available/eap on radius-2 and update the certificate paths:
private_key_file = /etc/ssl/private/radius-2.key certificate_file = /etc/ssl/certs/radius-2.crt
The ca_file remains the same on both nodes as they share the same CA.
Validate and start
freeradius -XC systemctl enable freeradius systemctl start freeradius
Configure UniFi for failover
In the UniFi controller, go to Settings → Profiles → RADIUS and add both servers. The controller will use the primary and fall back to the secondary if the primary is unreachable.
Preparing for client onboarding to the enterprise network
We are now ready to onboard our first client to the enterprise network. As per the configuration established earlier, the authorized group for network access is identified as `wifi`. Every user who needs access to the enterprise Wi-Fi network, as well as every workstation that requires joining the network, must be a member of the Active Directory group named `wifi`.
To ensure smooth and secure access, it is essential that this group remains up-to-date with the correct membership. Please implement regular audits and updates of this group to ensure that only authorized users and devices are granted access. This will help maintain the security and integrity of the network while minimizing unauthorized access.
If you require every computer to be able to join the network, the easiest way is to add the `Domain Computers` group as a member of the `wifi` group.
Moving forward, please make sure that all new employees or devices requiring network access are promptly added to the `wifi` group, and that any users or devices that no longer need access are removed in a timely manner.
If any automation is in place to manage this group, it should also be reviewed and updated regularly to reflect the most current user and device access needs.
How it works
This is a brief overview of the authentication flow when a Windows machine connects to the enterprise WiFi network.
The players
- Supplicant: the Windows machine connecting to WiFi.
- Authenticator: the UniFi access point. It does not make authentication decisions — it forwards everything to Radius.
- Authentication server: Freeradius, which validates credentials against Samba AD via
ntlm_auth.
The flow
- The Windows machine associates with the SSID (configured as WPA2-Enterprise) and sends its machine credentials (
host/PCNAME.domain) or user credentials. - The access point forwards the request as a RADIUS Access-Request to Freeradius.
- Freeradius initiates a PEAP session — a TLS tunnel is established between the client and Freeradius. This is where the CA certificate is validated by the client.
- Inside the TLS tunnel, MSCHAPv2 authentication takes place. Freeradius calls
ntlm_authvia themschapmodule. ntlm_authauthenticates against Samba AD (via winbind) and checks that the machine or user is a member of the AD group specified by--require-membership-of(in our caseMAD\wifi).- If
ntlm_authreturns success (exit code 0), Freeradius sends an Access-Accept back to the access point, along with MPPE encryption keys for WPA2. - If authentication fails or the machine/user is not in the group, Freeradius sends an Access-Reject and the client is denied access.
- The access point grants or denies network access based on the Radius response.
Why so many round-trips?
A single authentication generates approximately 12 RADIUS Access-Request / Access-Challenge exchanges. This is normal — the PEAP/TLS handshake requires multiple round-trips to establish the encrypted tunnel before the actual MSCHAPv2 authentication can take place inside it.
Key points
- The AD group check happens inside
ntlm_auth, not in Freeradius policy. If you need to change the group, edit thentlm_authline in/etc/freeradius/3.0/mods-available/mschap. - Both machine accounts (
host/PCNAME) and user accounts can authenticate, as long as they are members of thewifigroup. - The access point never sees the credentials — it only relays encrypted EAP packets between the client and Freeradius.
Caponato's Samba notebook. Start here or else Main menu.
