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:
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
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!
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.
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.
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).
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 #.
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.
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/.
./easyrsa gen-crl
Enter CA passphrase. The new pki/crl.pem now lists the revoked serial.
./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.
./easyrsa sign-req server radius-1
Type yes to confirm, then enter the CA passphrase.
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.
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
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.
Windows clients need to trust the Radius server's certificate. The easiest way is to deploy the CA certificate via GPO.
ca.crt file.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 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
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
}
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 {
}
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 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
Make sure you correctly update the lines:
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 {
}
}
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 {
}
}
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.
# 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
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.
radius-2).freerad user added to winbindd_priv and ssl-cert groups.radius-2 via EasyRSA, copied to the right place in radius-2 server.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
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.
freeradius -XC systemctl enable freeradius systemctl start freeradius
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.
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.
This is a brief overview of the authentication flow when a Windows machine connects to the enterprise WiFi network.
ntlm_auth.host/PCNAME.domain) or user credentials.ntlm_auth via the mschap module.ntlm_auth authenticates 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 case MAD\wifi).ntlm_auth returns success (exit code 0), Freeradius sends an Access-Accept back to the access point, along with MPPE encryption keys for WPA2.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.
ntlm_auth, not in Freeradius policy. If you need to change the group, edit the ntlm_auth line in /etc/freeradius/3.0/mods-available/mschap.host/PCNAME) and user accounts can authenticate, as long as they are members of the wifi group.Caponato's Samba notebook. Start here or else Main menu.