Child pages
  • 802.1X secured wifi installation

Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

Overview

Introduction

This solution employs a router/server computer with two network interfaces, one is a network uplink, the other connects to the wifi routers with their respective supplicants.

Users authenticate using a username and password, and verify the identity of the RADIUS server using a certificate presented by the server (see more information under supplicant configuration).

As mentioned in '@@@', the NASes available do not support sending RADIUS accounting packets, they are only able to authenticate users against the response from a RADIUS server. This solution uses 802.1X to authenticate users and let them into the wireless network, and then Shorewall to perform the logging/accounting work. FreeRADIUS logs the MAC address of all devices that connect along with the username they authenticated as, this information can be used to match information in Shorewall's logs to a user session. For improved security the firewall policy will be to disallow all connections except those originating from known IP addresses of known wifi clients. A script, shwl_add.sh, gets run by FreeRADIUS upon successful authentication of a supplicant, which runs an ARP scan to find the IP address of the device with the MAC address specified by FreeRADIUS, it then adds the IP address to shorewall's "whitelist", logs the event along with some useful information to a MySQL database and writes the current timestamp to a text file. In Access-Accept packets the Session-Timeout and Termination-Action attributes are sent, informing the NAS that after the specified amount of time the supplicant needs to repeat the authentication process or be disconnected. When the supplicant repeats authentication the mentioned script detects that the supplicant is already known and simply logs the event to MySQL and updates the timestamp in the text file. Another script, shwl_del.sh, is run by crontab at a regular interval, which goes through all the IP addresses present in shorewall's "whitelist" and checks the timestamp of the corresponding supplicant's last authentication to FreeRADIUS in the corresponding text file. If more than the specified amount of time has passed, this script assumes that the supplicant has disconnected from the wifi network and removes its IP from shorewall's "whitelist", logs the event to MySQL, and deletes the related text files.

A python script, script_launcher.py, serves as an intermediary that is launched by FreeRADIUS using its Rlm_python module and in turn launches shwl_add.sh.

The database backend containing user credentials for FreeRADIUS is MySQL. The passwords are stored in MySQL in NT hashes. Adding/deleting users can be accomplished by SQL queries which can be included in site specific custom user management scripts or as hooks to the standard 'adduser' and 'deluser' utilities (out of the scope of this guide). Updating of passwords is accomplished by a script, pam_to_mysql_update.sh, that gets run by the libpam-script PAM module during PAM stack execution, and updates the password in the MySQL database according to the same password the user chose for their system user account. An entry with the username field already filled and matching the system user account username needs to be already present in the MySQL database. Commands that expire or disable a user's system user account (or password) without deleting it (such as passwd -l) will not cause the credentials in the MySQL database to be disabled, thus it is necessary to take care (perhaps with a site specific lock user script) to also invalidate the same.

Shorewall dynamic zones are used to achieve the dynamic change of firewall rules set for the IP addresses added/removed to the "whitelist". The rules for the normal zone concerning the network connecting to the NASes/supplicants disallows all connections, except those needed for the RADIUS conversation between the NASes and FreeRADIUS. A dynamic zone is declared under the normal zone, whose rules allow network access, with logging. The 'shorewall add <dynamic_zone_name> <ip_address>' and 'shorewall delete <dynamic_zone_name> <ip_address>' commands can then be used by the shwl_*.sh scripts to change the rules applicable for the specified source IP address.

Sudo is installed and configured as it is required for some of the mentioned scripts to run commands as root or as a different user.

A package containing the mentiond scripts, as well as the empty MySQL schema for the database used by some of them, is attached to this wiki page.

Scripts

Common MySQL database schema

A MySQL schema is contained in the shwl_add_shwl_del_pmu.sql in the attached archive. It is used by the shwl_add.sh, shwl_del.sh and pam_to_mysql_update.sh scripts to log events and errors. It contains a single table named 'event_log', it's format is as follows:

...

NOTE: The 'log_time' column indicates the time when the MySQL entry was logged (i.e. all the scripts use the MySQL NOW() function in the field for this column in all their queries), whereas the rad_attr_Event-Timestamp column contains the content of the Event-Timestamp RADIUS attribute, when applicable, which is expected to contain the timestamp of when the RADIUS server received the request ('@@@').

shwl_add.sh

This script is intended to be run by script_launcher.py (below mentioned).

It reads 10 lines on stdin, as follows:

...

RADIUS attribute: NAS-Port

...

The supplicant MAC address is used in the script, the remaining values are simply logged into the MySQL database. Some sanity check is performed on the username before the script continues. The supplicant MAC address is processed so as to obtain it in both of the following formats regardless of the format the NAS specified it in: filename friendly version: 0123456789ab, normal version: 01:23:45:67:89:ab. The script then checks if a file named as the filename friendly version of the supplicant MAC address already exists (referred to by the script as an IP file). If it does not, the script runs an ARP scan on the configured network interface and in the resulting table, looks for the IP address matching the specified supplicant MAC address. In case no such device is found, it repeats the scan a configurable amount of times at a configurable interval before giving up. Once a matching IP is found some amount of sanity check is performed on the scan results (e.g. to detect multiple MACs being used by the found IP), and then the event is logged into MySQL with "connect" mentioned in the 'event' column, the found IP is added to the configured shorewall dynamic zone, and a file named as the filename friendly version of the supplicant MAC address is created containing the device's IP, and a file named as the device's IP (referred to by the script as a timestamp file) is created containing the present timestamp. In case adding the IP address to the shorewall dynamic zone is not successful, the script waits a configurable amount of time, and then attempts a second time, after which it gives up (it was observed that it might happen that FreeRADIUS starts earlier in the boot process than Shorewall, and if this script is started during that timeframe the shorewall add command fails). In case the IP file is already present, the script simply logs the event to MySQL with "re-auth" mentioned in the 'event' column and re-writes the timestamp file with the present timestamp. In case an error is encountered, the script logs the error to MySQL mentioning "err-add-N" (where N is the error number) in the 'event' column, populating the remaining columns with their respective information as may be available at the time of the error, and exits immediately, returning the error number as exit code. In case access is available to the script's stdout and stderr, a description of the error message is also printed (and the script is quite verbose about what's happening), in case not, it is possible to look in the script's code for calls to the shwl_add_error_message_close() function, identify the call where the error number in question is passed to the function, and the error description can be found in the same function call. The configurable options can be found at the top of the script. The username is logged into the 'device_username' column, the IP address found during the scan is logged in the 'device_ip' column, the above mentioned normal version of the supplicant MAC address is logged in the 'device_mac' column, and the remaining RADIUS attributes are logged in the columns with the respective names.

shwl_del.sh

This script is intended to be run by crontab at a regular interval.

This script loops through all the IP addresses present in the configured shorewall dynamic zone, reads their corresponding timestamp file (see shwl_add.sh description) and checks whether the configured amount of time has elapsed since. If it has, it logs the event to MySQL specifying "expire" in the 'event' column, removes the IP from the shorewall dynamic zone, deletes the timestamp file and searches for files (although there should be only one) containing the IP address (to find the IP file, see shwl_add.sh description) and deletes them. In the unexpected case that no timestamp file is found for a given IP or it does not contain a valid timestamp, the same actions are taken as when the configured amount of time has elapsed, but "untracked" is mentioned in the 'event' column instead of "expire". In case an error is encountered, the script logs the error to MySQL mentioning "err-del-N" (where N is the error number) in the 'event' column, populating the remaining columns with their respective information as may be available at the time of the error, but continues execution. At the end, it returns the error number as exit code, or, in case there were multiple errors, 127. In case access is available to the script's stdout and stderr, a description of the error message is also printed (and the script is quite verbose about what's happening), in case not, it is possible to look in the script's code for calls to the shwl_del_error_message() function, identify the call where the error number in question is passed to the function, and the error description can be found in the same function call. The configurable options can be found at the top of the script. The IP address being processed is logged in the 'device_ip' column, a comma separated list of the IP files is logged in the 'device_mac' column, or the text "/// no-ip-files ///" in case no IP files were present, or the text "/// err-del-N ///" (where N is the error number) in case an error occurred while searching for the file(s), and the rad_attr_* as well as the 'device_username' columns are left empty. The text "/// n/a ///" may be present in the 'device_ip' and 'device_mac' columns if this information is not applicable.

script_launcher.py

This script is intended to be called by the FreeRADIUS Rlm_python module, which, in the intended configuration, calls the post_auth(attr) function passing a tuple of tuples containing the relevant RADIUS attributes and their values as the 'attr' argument.

This script looks for the values of the following items in the tuple of tuples:

...

The script executes the configured command in the background and then writes to the new process's stdin the values of the above attributes, one attribute on each line, in the order shown in the table. It then exits, leaving the new process running. If one of the RADIUS attributes mentioned above as "Optional" is missing, the script writes "None" in its place. If one of the "Mandatory" ones is missing an error occurs. In case an error is encountered, the script exits immediately, returning radiusd.RLM_MODULE_FAIL. (This means if an error occurs after the new process has been launched, e.g. because a mandatory RADIUS attribute is missing, the script exits leaving the new process running, doing nothing, waiting for its stdin to be populated). If no error occurs, the script returns radiusd.RLM_MODULE_OK at the end. The configurable options can be found at the top of the script.

pam_to_mysql_update.sh

This script is intended to be run by the libpam-script PAM module during the PAM auth and PAM password stacks execution. In the intended configuration, the libpam-script module checks the script's exit code and reports failure back to the PAM stack, causing the PAM operation to fail, in case an error occurred in the script.

It reads the following environment variables:

...

This script checks, first, if it is running as root or not (the PAM stack does not necessary run as root, but as the user as which the service that invoked it is running). If it is, it proceeds to encrypt the user's password (${PAM_AUTHTOK}) as an NT hash and update it in the 'value' column of the 'radcheck' table in the 'radius' MySQL database for the entry where the value of the 'username' column matches the user's username (${PAM_USER}). Due to the nature of the SQL query used, there needs to already be an entry in the table containing the matching username in the 'username' column (for functionality it should actually also already contain the appropriate values for the remaining columns, except the 'value' column). If it is not running as root, it uses a workaround to escalate its privileges. It uses the available credentials to SSH into localhost as the user in question, causing the SSH daemon to run a new instance of the PAM stack to verify the user's credentials (and sshd runs the PAM stack as root) and thus a new instance of the script, as root. Before doing so the script makes sure that the PAM stack has not already now been invoked by sshd, in order to avoid, in case sshd should ever decide to run the PAM stack as a non-root user, that an infinite loop occurs spawning endless processes of this script, sshd, PAM, etc.

In case an error is encountered, the script logs the error to the above mentioned '@@@' 'event_log' table in MySQL mentioning "err-pmu-N" (where N is the error number) in the 'event' column, populating the 'device_username' column with the user's username, leaving the other columns empty, and exits immediately, returning the error number as exit code. The error is logged to MySQL only if the script is running as root. In case access is available to the script's stdout and stderr, a description of the error message is also printed (and the script is quite verbose about what's happening if cfg_verbose is set to 1), in case not, it is possible to look in the script's code for calls to the pam_to_mysql_update_error_message_close() function, identify the call where the error number in question is passed to the function, and the error description can be found in the same function call. The configurable options can be found at the top of the script.

In the intended PAM/libpam-script configuration, in the case of the script running SSH to start a second instance as root, in the case the second instance (running as root) fails and returns an error exit code, libpam-script will report failure to the PAM stack, causing the authentication to fail, thus the SSH login to fail, and the ssh command that was launched in the first instance of the script, resulting finally in the first instance of the script to also fail, the first instance of libpam-script, and thus the first PAM stack. Setting cfg_verbose=1 will cause the script's verbose output to appear on screen in cases where a service invoking the PAM stack allows, e.g. when running the 'su' command.

Installation

Replication of production setup

Here, we replicate the relevant configuration already present on server.lastschl.av as a starting point. The test virtual machine will have two network interfaces, one serving as uplink on a 192.168.10.0/24 network (IP 192.168.10.52), and one to connect to the wifi routers/clients on a 192.168.9.0/24 network (IP 192.168.9.1). The FQDN will be server.test.av.

Base virtual machine preparation

Imported Last School's Debian9 VM template "Debian9-base.ova" into Virtual Box as Debian9-base_8021x, re-initializing all MAC addresses. The description for this virtual machine template is:

Code Block
Debian 9 amd64 installation
- Hostname:
debian9-base
- User accounts (username password):
ls last
root last
- Partitioning:
--- Physical:
------ 1GB RAID boot flag
------ 29GB RAID
--- RAID:
------ md0: ext3 /boot
------ md1: LVM - part of volume group debian9-base
--- LVM (VG/LV):
------ debian9-base/root: 18.6GB ext4 /
------ debian9-base/swap: 3.72GB swap area
- Up to date as of 2017-09-27
- sources.list includes:
Sections: main contrib non-free
Additional repository: backports
- Apt-cacher configured as per Last School site (Proxy credentials will need to be entered in /etc/apt/apt.conf.d/02proxy by user)
- SSH access installed and enabled

- Gnome and Firefox configured to auto-detect proxy settings
- Extra software installed:
vlc gimp emacs fonts-indic tcpdump iperf exfat-utils wireshark

- One network interface as bridged adapter, cable connected.

...

The host computer has two network interfaces, one connected to a network uplink and another connected to a couple of wifi routers. Each VirtualBox virtual interface is bridged to a different physical adapter. Network configuration is now as follows (interface name seen in guest OS - Adapter name in VirtualBox settings - Adapter "Attached to" setting in VirtualBox settings - Physical interface bridged to):

enp0s3 - Adapter 1 - Bridged adapter- physical interface connected to uplink

enp0s8 - Adapter 2 - Bridged adapter - physical interface connected to wifi routers

Booted the VM, logged in to the GUI, connected using DHCP with network manager

In terminal:

Code Block
rm /etc/apt/apt.conf.d/02proxy
apt-get update
apt-get upgrade

Rebooted the virtual machine

Set strong passwords for ls and root users

Installed my ssh public key in root's .ssh/authorized_keys file.

 

Installation of relevant services:

Shorewall (based on LASTSCHL-207):

 

Code Block
apt-get install shorewall
apt-get install ipset
mv /etc/shorewall{,-orig}
mkdir /etc/shorewall

Configuration:

Code Block
root@debian9-base:/etc/shorewall# for i in `ls`; do echo "========= $i ========="; cat $i | grep -v "^#" | grep -v "^$"; echo "========= $i ========="; echo ""; done
========= hosts =========
========= hosts =========

========= interfaces =========
net     enp0s3         detect      tcpflags,dhcp,nosmurfs,routefilter,logmartians
wifi	enp0s8		   detect	   tcpflags,nosmurfs,routefilter,logmartians
========= interfaces =========

========= masq =========
enp0s3					   192.168.9.0/24
========= masq =========

========= policy =========
$FW		net		REJECT		INFO(uid)
$FW		wifi	ACCEPT		INFO(uid)
wifi	all		REJECT
net		all		DROP		INFO
all		all		REJECT		info
========= policy =========

========= routestopped =========
========= routestopped =========

========= rules =========
Invalid(DROP)		 net			 all
ACCEPT:INFO(uid)     net             $FW             tcp     22
ACCEPT:INFO(uid)     net             $FW             udp     123
ACCEPT:INFO(uid)     net	     	 $FW	     	 icmp
ACCEPT:INFO(uid)     $FW             net             tcp     465,587,995,993
ACCEPT:INFO(uid)     $FW             net             udp     53,123
ACCEPT:INFO(uid)     $FW	     	 net	     	 icmp
ACCEPT:INFO(uid)     $FW             net             tcp     -            -               -               -               root
ACCEPT:INFO(uid)     $FW             net             udp     -            -               -               -               root
ACCEPT:INFO(uid)     $FW             net             icmp    -            -               -               -               root
ACCEPT:INFO(uid)     $FW             net             tcp     -            -               -               -               _apt
ACCEPT:INFO(uid)     $FW             net             udp     -            -               -               -               _apt
ACCEPT:INFO(uid)     $FW             net             icmp    -            -               -               -               _apt
========= rules =========

========= shorewall.conf =========
....
STARTUP_ENABLED=Yes
....
IP_FORWARDING=On
....
========= shorewall.conf =========

========= zones =========
fw			firewall
net			ipv4
wifi		ipv4
========= zones =========

In /etc/default/shorewall, set

Code Block
startup=1
Code Block
root@debian9-base:~# cat /etc/rsyslog.d/40-shorewall.conf 
:msg, contains, "Shorewall:" /var/log/shorewall
& stop

root@debian9-base:~# cat /etc/logrotate.d/shorewall 
/var/log/shorewall-init.log {
    weekly
    rotate 108
    compress
    nomissingok
    create 0640 root adm
}

/var/log/shorewall
{
	rotate 731
	daily
	nomissingok
	notifempty
	delaycompress
	compress
	dateext
	postrotate
		reload rsyslog >/dev/null 2>&1 || true
        endscript
}
root@debian9-base:~# cat /etc/logrotate.d/rsyslog 
/var/log/syslog
/var/log/auth.log
{
	rotate 731
	daily
	dateext
	nomissingok
	notifempty
	delaycompress
	compress
	postrotate
		invoke-rc.d rsyslog rotate > /dev/null
	endscript
}

/var/log/mail.info
/var/log/mail.warn
/var/log/mail.err
/var/log/mail.log
/var/log/daemon.log
/var/log/kern.log
/var/log/user.log
/var/log/lpr.log
/var/log/cron.log
/var/log/debug
/var/log/messages
{
	rotate 4
	weekly
	missingok
	notifempty
	compress
	delaycompress
	sharedscripts
	postrotate
		invoke-rc.d rsyslog rotate > /dev/null
	endscript
}

root@debian9-base:~# cat /etc/logrotate.conf 
# see "man logrotate" for details
# rotate log files weekly
weekly

# keep 4 weeks worth of backlogs
rotate 4

# create new (empty) log files after rotating old ones
create

# uncomment this if you want your log files compressed
#compress

# packages drop log rotation information into this directory
include /etc/logrotate.d

# no packages own wtmp, or btmp -- we'll rotate them here
/var/log/wtmp {
    nomissingok
    monthly
    create 0664 root utmp
    rotate 24
}

/var/log/btmp {
    nomissingok
    monthly
    create 0660 root utmp
    rotate 24
}

# system-specific logs may be configured here
Code Block
systemctl enable shorewall.service

 

Configure network and DHCP (based on LASTSCHL-212):

Code Block
systemctl disable network-manager.service
systemctl disable NetworkManager.service
unlink /etc/resolv.conf
echo nameserver 192.168.10.1 > /etc/resolv.conf
mkdir /etc/ltsp
Code Block
root@debian9-base:~# cat /etc/network/interfaces
# This file describes the network interfaces available on your system
# and how to activate them. For more information, see interfaces(5).

# The loopback network interface
auto lo
iface lo inet loopback

# The external interface
auto enp0s3
iface enp0s3 inet static
address 192.168.10.52
network 192.168.10.0
netmask 255.255.255.0
broadcast 192.168.10.255
gateway 192.168.10.1
  
# The wifi interface
auto enp0s8
iface enp0s8 inet static
address 192.168.9.1
netmask 255.255.255.0
broadcast 192.168.9.255
 
root@debian9-base:~# cat /etc/dhcp/dhcpd.conf | grep -v "^#" | grep -v "^$"
# Some of the following lines are there by default and are probably not required
ddns-update-style none;
option domain-name "example.org";
option domain-name-servers ns1.example.org, ns2.example.org;
default-lease-time 600;
max-lease-time 7200;
log-facility local7;
include "/etc/ltsp/dhcpd.conf";
 
root@debian9-base:~# cat /etc/ltsp/dhcpd.conf 
#
# Default LTSP dhcpd.conf config file.
#

authoritative;

subnet 192.168.9.0 netmask 255.255.255.0 {
    range 192.168.9.40 192.168.9.250;
    option domain-name "test.av";
    option domain-name-servers 192.168.9.1;
    option broadcast-address 192.168.9.255;
    option routers 192.168.9.1;
    option subnet-mask 255.255.255.0;
    option root-path "/opt/ltsp/amd64";
    if substring( option vendor-class-identifier, 0, 9 ) = "PXEClient" {
        filename "/ltsp/amd64/pxelinux.0";
    } else {
        filename "/ltsp/amd64/nbi.img";
    }

}
Code Block
apt-get install isc-dhcp-server

In /etc/default/isc-dhcp-server, set:

Code Block
INTERFACESv4="enp0s8"

 

 

Configure DNS (based on LASTSCHL-211):

Code Block
apt-get install dnsmasq
touch /var/log/dnsmasq
chmod 640 /var/log/dnsmasq
Code Block
root@debian9-base:~# cat /etc/dnsmasq.conf | grep -v "^#" | grep -v "^$"
strict-order
interface=enp0s8
expand-hosts
domain=test.av
log-queries
log-facility=/var/log/dnsmasq
 
root@debian9-base:~# cat /etc/logrotate.d/dnsmasq
/var/log/dnsmasq
{
	rotate 731
	daily
	nomissingok	
	notifempty
	delaycompress
	compress
	dateext
	postrotate
		reload rsyslog >/dev/null 2>&1 || true
	endscript
}
 
root@debian9-base:~# cat /etc/hostname
server.test.av
 
root@debian9-base:~# cat /etc/hosts
127.0.0.1	localhost

192.168.9.1	test.av
192.168.9.1	server.test.av	server

# The following lines are desirable for IPv6 capable hosts
::1     localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters

 

New stuff

Now that we have a working setup similar to the production one, we will modify it to implement the new solution.

Download the latest version of the attached shwl_add_shwl_del_sl_pmu archive and extract it somewhere convenient.

Shorewall

Add to /etc/shorewall/hosts:

Code Block
wifi1 enp0s8:dynamic

...

Code Block
# Just after: wifi		all		REJECT
# Added:
wifi1		net		ACCEPT		INFO
wifi1		$FW		ACCEPT		INFO(uid)
$FW		wifi1		ACCEPT		INFO(uid)
# Before: net		all		DROP		INFO

...

Code Block
....
wifi1:wifi ipv4 dynamic_shared

In /etc/shorewall/shorewall.conf set:

Code Block
SAVE_IPSETS=Yes

Add to /etc/shorewall/rules (replace IP addresses with actual IP address of wifi routers):

Code Block
 
# At the top of the file:
?SECTION ALL
# Allow the server and NASes to talk RADIUS and HTTP (web interface)
ACCEPT          wifi:192.168.9.2,192.168.9.3,192.168.9.4                $FW                                             tcp     -       80
ACCEPT          $FW                                                     wifi:192.168.9.2,192.168.9.3,192.168.9.4        tcp     80      -
ACCEPT          wifi:192.168.9.2,192.168.9.3,192.168.9.4                $FW                                             udp     1812    -
ACCEPT          $FW                                                     wifi:192.168.9.2,192.168.9.3,192.168.9.4        udp     -       1812
# But, reject anything else to and from any other device part of the 192.168.9.0/24 network that is not part of any dynamic zone
REJECT          wifi                                                    all                                             -       -       -
REJECT          all                                                     wifi                                            -       -       -

?SECTION NEW
 
# At the end of the file:
ACCEPT:INFO(uid)        wifi:192.168.9.2,192.168.9.3,192.168.9.4        $FW             udp    1812

 

FreeRADIUS

Code Block
apt-get install freeradius
systemctl enable freeradius.service

 

Modify /etc/freeradius/3.0/mods-available/eap:

comment the following:

Code Block
....
#       md5 {
#       }
....
#       leap {
#       }
....
#       gtc {
#               #  The default challenge, which many clients
#               #  ignore..
#               #challenge = "Password: "
#
#               #  The plain-text response which comes back
#               #  is put into a User-Password attribute,
#               #  and passed to another module for
#       	#  authentication.  This allows the EAP-GTC
#               #  response to be checked against plain-text,
#               #  or crypt'd passwords.
#               #
#               #  If you say "Local" instead of "PAP", then
#       	#  the module will look for a User-Password
#               #  configured for the request, and do the
#               #  authentication itself.
#               #
#               auth_type = PAP
#       }
....
#       tls {
#               # Point to the common TLS configuration
#               tls = tls-common
#
#       	#
#               # As part of checking a client certificate, the EAP-TLS
#               # sets some attributes such as TLS-Client-Cert-CN. This
#               # virtual server has access to these attributes, and can
#               # be used to accept or reject the request.
#       	#
#       #       virtual_server = check-eap-tls
#       }
....

modify the 'default_eap_type' directive under section 'eap' to be:

Code Block
default_eap_type = peap

and the 'default_eap_type' directive under section 'ttls' to be:

Code Block
default_eap_type = mschapv2

Modify /etc/freeradius/3.0/sites-available/default, comment the following lines (see comments included in the code block):

Code Block
# All the listen sections except the IPv4 version with "type = auth"
listen {
	ipaddr = *
	port = 0
	type = acct
	limit {
	}
}
listen {
	type = auth
	ipv6addr = ::	# any.  ::1 == localhost
	port = 0
	limit {
	      max_connections = 16
	      lifetime = 0
	      idle_timeout = 30
	}
}
listen {
	ipv6addr = ::
	port = 0
	type = acct
	limit {
	}
}
# In the authorize section:
chap
mschap
digest
files
-ldap
pap
# In the authenticate section:
Auth-Type PAP {
		pap
}
Auth-Type CHAP {
		chap
}
Auth-Type MS-CHAP {
		mschap
}
mschap
digest

Uncomment the following line in the 'authorize' section:

Code Block
auth_log

Add the following line at the end of the 'post-auth' section and at the beginning of the Post-Auth-Type REJECT section:

Code Block
reply_log

 

 

Add the following in the post-auth section, just before the Post-Auth-Type REJECT section:

Code Block
update reply {
             Session-Timeout := 3600
             Termination-Action := 1
}

 

Modify /etc/freeradius/3.0/sites-available/inner-tunnel, comment the following lines:

Code Block
# The whole listen section
listen {
       ipaddr = 127.0.0.1
       port = 18120
       type = auth
}
# In the authorize section:
chap
mschap
files
-ldap
# In the authenticate section:
Auth-Type PAP {
		pap
}
Auth-Type CHAP {
		chap
}
Auth-Type MS-CHAP {
		mschap
}

Add the following line after 'filter_username' and before 'suffix' in the 'authorize' section:

Code Block
auth_log

Add the following line at the end of the 'post-auth' section and at the beginning of the Post-Auth-Type REJECT section:

Code Block
reply_log

 

Modify /etc/freeradius/3.0/radiusd.conf, set (in the 'log' section):

Code Block
auth = yes

 

Modify /etc/freeradius/3.0/clients.conf, comment the 'client localhost' and 'client localhost_ipv6' section and add (replace with actual IP addresses of wifi routers):

Code Block
client wifi-ap1 {
       ipaddr = 192.168.9.2
       secret = password # Replace with an actual password
}
 
client wifi-ap2 {
       ipaddr = 192.168.9.3
       secret = password # Replace with an actual password
}

client wifi-ap3 {
       ipaddr = 192.168.9.4
       secret = password # Replace with an actual password
}

Modify /etc/logrotate.d/freeradius, modify the following options as follows ('dateext' option needs to be added):

Code Block
rotate 732
nomissingok
dateext
Code Block
rm /var/log/freeradius/radius.log
rm /var/log/freeradius/radwtmp
chmod o-rwx /var/log/freeradius
chown freerad:freerad /var/log/freeradius
chmod o-rwx /etc/freeradius

It has been observed that radius.log comes with world-readable permissions upon installation of the package, deleting it causes FreeRADIUS to re-create it, and it gets re-created with more secure permissions. /etc/freeradius also comes with the executable bit set for all users, which makes it easier for sensitive information contained within to be world-readable in case the permissions of an individual file are not set restrictive enough (as was, by default, the case with the file containing the encryption passwords for the SSL certificates). Could not find any information on the net on whether there is a good reason for the executable bit being set, so, decided it is safer to remove it.

...

Modify /etc/freeradius/3.0/certs/server.cnf, set the following settings:

Code Block
...
[ CA_default ]
...
default_days            = 732
...
[ req ]
...
input_password	= password # Replace with an actual password
output_password	= password # Replace with an actual password, should be same as input_password
...

[certificate_authority]
countryName	= IN
stateOrProvinceName	= Tamil Nadu
localityName	= Auroville
organizationName	= Test
emailAddress	= admin@test.av
commonName	= "Test Certificate Authority"
...

Modify /etc/freeradius/3.0/certs/ca.cnf, set the following settings:

Code Block
...
[ CA_default ]
...
default_days            = 732
...
crlDistributionPoints	= URI:http://server.test.av/test_ca.crl

[ req ]
...
input_password	= password # Replace with an actual password
output_password	= password # Replace with an actual password, should be same as input_password

[server]
countryName	= IN
stateOrProvinceName	= Tamil Nadu
localityName	= Auroville
organizationName	= Test
emailAddress	= admin@test.av
commonName	= "Test Server Certificate"
 
[v3_ca]
...
crlDistributionPoints	= URI:http://server.test.av/test_ca.crl
...
Code Block
cd /etc/freeradius/3.0/certs
rm -f *.pem *.der *.csr *.crt *.key *.p12 serial* index.txt* # This step is probably not needed, 
make ca.pem
make ca.der
make server.pem
make server.csr
chown freerad:freerad *
chmod o-rwx *
rm bootstrap
rm passwords.mk
# Delete all other files in the folder except: server.cnf, ca.cnf, xpextensions, Makefile, README, dh, ca.pem, server.pem, server.key

Modify /etc/freeradius/3.0/mods-available/eap, modify the following directives under section 'tls-config tls-common' to be:

Code Block
private_key_password = password # Replace password with the password chosen previously '@@@' same or different?
private_key_file = /etc/freeradius/3.0/certs/server.pem
....
certificate_file = /etc/freeradius/3.0/certs/server.pem
....
ca_file = /etc/freeradius/3.0/certs/ca.pem

 

MySQL

Code Block
apt-get install mysql-server freeradius-mysql
mysql -uroot
  CREATE DATABASE radius;
  exit
mysql -uroot radius < /etc/freeradius/3.0/mods-config/sql/main/mysql/schema.sql

Edit /etc/freeradius/3.0/mods-config/sql/main/mysql/setup.sql. Modify the following lines:

Code Block
CREATE USER 'radius'@'localhost';
SET PASSWORD FOR 'radius'@'localhost' = PASSWORD('radpass');

to

Code Block
CREATE USER 'freerad'@'localhost' IDENTIFIED VIA unix_socket;

and update the username 'radius' to be 'freerad' wherever else it is mentioned in the file.

Code Block
mysql -uroot radius < /etc/freeradius/3.0/mods-config/sql/main/mysql/setup.sql
Code Block
cd /etc/freeradius/3.0/mods-enabled
ln -s ../mods-available/sql sql

In /etc/freeradius/3.0/mods-enabled/sql, set the following options:

Code Block
driver = "rlm_sql_mysql"
dialect = "mysql"
server = "localhost"
port = 3306
login = "freerad"
password = ""
radius_db = "radius"
logfile = ${logdir}/sqllog.sql

Modify /etc/freeradius/3.0/sites-enabled/inner-tunnel, find the following line under authorize, post-auth and Post-Auth-Type REJECT sections

Code Block
-sql

modify it to

Code Block
sql

Modify /etc/freeradius/3.0/sites-enabled/default, find the following line under authorize, post-auth and Post-Auth-Type REJECT sections

Code Block
-sql

In the post-auth and Post-Auth-Type REJECT sections, modify it to

Code Block
sql

In the authorize section, comment it out.

Python module / script_launcher.py script

Code Block
apt-get install libpython2.7-dev # It is not fully sure whether this package is needed
cd /etc/freeradius/3.0/mods-enabled
ln -s ../mods-available/python python

Put the following in it:

Code Block
title/etc/freeradius/3.0/mods-enabled/python
#
# Make sure the PYTHONPATH environmental variable contains the
# directory(s) for the modules listed below.
#
# Uncomment any func_* which are included in your module. If
# rlm_python is called for a section which does not have
# a function defined, it will return NOOP.
#
python {
	module = script_launcher

	python_path = ${modconfdir}/${.:name}:/usr/lib/python2.7
	
	mod_post_auth = ${.module}
	func_post_auth = post_auth
}

Modify /etc/freeradius/3.0/sites-enabled/inner-tunnel:

Code Block
title/etc/freeradius/3.0/sites-enabled/inner-tunnel
...
# Add this line just after 'sql' in the 'post-auth' section
python
...

Modify /etc/freeradius/3.0/mods-available/eap, modify the 'copy_request_to_tunnel' directive under both sections 'peap' and 'ttls' to be:

 

Code Block
copy_request_to_tunnel = yes

Place the script_launcher.py script from the shwl_add_shwl_del_sl_pmu archive at /etc/freeradius/3.0/mods-config/python/script_launcher.py

Code Block
chown freerad:freerad /etc/freeradius/3.0/mods-config/python/script_launcher.py
chmod 640 /etc/freeradius/3.0/mods-config/python/script_launcher.py

 

sudo

 

Code Block
apt-get install sudo

Create /etc/sudoers.d/shwl_add_shwl_del_pmu, permissions 640 root:root, with:

Code Block
freerad ALL=(root:root) NOPASSWD:/sbin/shorewall,/usr/bin/arp-scan

 

shwl_add / shwl_del scripts

 Prerequisites from above steps: sudo, FreeRADIUS python module / script_launcher.py script, shorewall, FreeRADIUS MySQL

Code Block
apt-get install arp-scan
# Install the shwl_*.sh scripts from the shwl_add_shwl_del_sl_pmu archive in /usr/local/sbin/
chown root:freerad /usr/local/sbin/shwl_*
chmod 750 /usr/local/sbin/shwl_*
mkdir /var/local/shwl_add
chown freerad:freerad /var/local/shwl_add
chmod 700 /var/local/shwl_add
chmod a-s /var/local/shwl_add

Add the following line to freerad's crontab

Code Block
*/1 * * * * /usr/local/sbin/shwl_del.sh

Settings in /usr/local/sbin/shwl_add.sh (at top of file):

Code Block
....
# Settings
cfg_shwl_zone="wifi1" # Shorewall dynamic zone to which client devices' IP addresses need to be added
cfg_shwl_retry_delay=2 # Number of seconds to wait, in case of failure in adding IP to shorewall dynamic zone, before attempting second time
cfg_file_location="/var/local/shwl_add" # Folder where runtime information will be stored
cfg_file_location_owner_user="freerad" # User by which above folder should be owned
cfg_file_location_owner_group="freerad" # Group by which above folder should be owned
cfg_ip_srch_iface="enp0s8" # Network interface on which to scan for devices
cfg_ip_srch_initial_delay=1 # How many seconds to wait before first attempt at scanning
cfg_ip_srch_retry_delay=2 # How many seconds to wait in between further attempts at scanning
cfg_ip_srch_max_attempts=15 # Maximum number of attempts at scanning before giving up
cfg_mysql_user="freerad" # MySQL username
cfg_mysql_db="shwl_add_shwl_del_pmu" # MySQL database name where to log events
cfg_mysql_log_table="event_log" # Table in MySQL database where to log events
....

Settings in /usr/local/sbin/shwl_del.sh (at top of file):

Code Block
....
# Settings
cfg_ip_match_pattern="192.168." # Pattern to match all IP addresses that might be in the shorewall dynamic zone
cfg_session_expiry_timeout=3660 # Session duration (should be slightly longer than Session-Timeout attribute specified in FreeRADIUS)
cfg_shwl_zone="wifi1" # Shorewall dynamic zone containing clients' IP addresses
cfg_file_location="/var/local/shwl_add" # Folder where runtime information is stored
cfg_file_location_owner_user="freerad" # User by which above folder should be owned
cfg_file_location_owner_group="freerad" # Group by which above folder should be owned
cfg_mysql_user="freerad" # MySQL username
cfg_mysql_db="shwl_add_shwl_del_pmu" # MySQL database name where to log events
cfg_mysql_log_table="event_log" # Table in MySQL database where to log events
....

 

MySQL

 

Code Block
mysql -uroot
  CREATE DATABASE shwl_add_shwl_del_pmu;
  GRANT ALL on shwl_add_shwl_del_pmu.event_log TO 'freerad'@'localhost';
  exit
mysql -uroot shwl_add_shwl_del_pmu < shwl_add_shwl_del_pmu.sql # Updating shwl_add_shwl_del_pmu.sql to the full path of the shwl_add_shwl_del_pmu.sql file extracted from the shwl_add_shwl_del_sl_pmu archive

 

 

pam_to_mysql_update.sh script

Prerequisities from above: sudo, FreeRADIUS MySQL, shwl_add / shwl_del scripts MySQL

Code Block
apt-get install libpam-script sshpass
mkdir /usr/share/libpam-script/pam-script.d/pam_to_mysql_update
cd /usr/share/libpam-script/pam-script.d/pam_to_mysql_update
# Install the pam_to_mysql_update.sh script from the shwl_add_shwl_del_sl_pmu archive in here
ln -s pam_to_mysql_update.sh pam_script_auth
ln -s pam_to_mysql_update.sh pam_script_passwd
mysql -uroot
	GRANT ALL on radius.radcheck TO 'freerad'@'localhost';
exit
pam-auth-update # And, uncheck the box for "Support for authentication by external scripts"

Add the following line at the end of /etc/pam.d/common-auth:

Code Block
title/etc/pam.d/common-auth
....
auth	required                        pam_script.so onerr=fail dir=/usr/share/libpam-script/pam-script.d/pam_to_mysql_update/

Add the following line at the end of /etc/pam.d/common-password:

Code Block
title/etc/pam.d/common-password
....
password	required                        pam_script.so onerr=fail dir=/usr/share/libpam-script/pam-script.d/pam_to_mysql_update/

Settings in /usr/share/libpam-script/pam-script.d/pam_to_mysql_update/pam_to_mysql_update.sh (at top of file):

Code Block
....
# Settings
cfg_mysql_user="freerad" # MySQL username
cfg_mysql_user_db="radius" # MySQL database name where to update passwords
cfg_mysql_log_db="shwl_add_shwl_del_pmu" # MySQL database name where to log events
cfg_mysql_user_table="radcheck" # Table in MySQL database where to update passwords
cfg_mysql_log_table="event_log" # Table in MySQL database where to log events
cfg_verbose=0 # Print verbose messages on stdout
....

 

Managing users

The below procedures also create/change password for/delete system users as well as users for FreeRADIUS.

Adding users

Replace 'user' with the desired username.

Code Block
mysql -uroot
	use radius;
	INSERT INTO radcheck VALUES ('','user','NT-Password',':=','');
exit
adduser user # When prompted for "Current password:", ignore and press enter

Changing password

It is sufficient to use standard utilities such as 'passwd', the password will be updated in the MySQL database as well. Tested with 'passwd' and User Accounts applet in GNOME. In case prompted with "Current password:" (exactly as written here) it is sufficient to ignore and press enter. Commands that expire or disable a user's system user account (or password) without deleting it (such as passwd -l) will not cause the credentials in the MySQL database to be disabled, thus it is necessary to take care (perhaps with a site specific lock user script) to also invalidate the same.

Deleting users

Replace 'user' with the username to be deleted.

Code Block
mysql -uroot
	use radius;
	DELETE FROM radcheck WHERE username='user';
exit
deluser user

WiFi router (NAS) configuration

Settings to be configured

These are the settings that usually need to be configured, on dual-band routers it might be necessary to configure some of the settings twice, once under the settings for the 2.4GHz SSID and once for the 5GHz SSID:

  • SSID - SSID of choice
  • Network security type: WPA2 Enterprise
  • WPA type: Set to either Auto or WPA2
  • WPA encryption: Set to either Auto or AES
  • RADIUS server IP - 192.168.9.1
  • RADIUS server port - 1812
  • RADIUS server secret/password - Password chosen in clients.conf for this particular NAS
  • Secure password - Choose a secure password for accessing the NAS web (or other) interface. It is important as it controls access to the wireless security settings, and the web (or other) interface is reachable by supplicants connected to the network.
  • Clients isolation - If enabled, prevents connected supplicants from talking to each other/seeing each other's traffic. Can improve security if there is either only one NAS installed or each NAS is in a separate broadcast domain (and there is no other device connected, e.g. through wired network, in the same broadcast domain), at the expense of not allowing connected supplicants to communicate directly with each other (e.g. SSH into each other, etc.)
  • IP address - IP address needs to match IP mentioned in clients.conf
  • Disable DHCP server
  • Some models: Reauthentication period - Specify to something equal to or greater than the Session-Timeout specified in /etc/freeradius/3.0/sites-available/default. Some NASes interpret 0 as disabling re-authentication, and might then also ignore any value mentioned by the FreeRADIUS Session-Timeout / Termination-Action attributes.
  • Some models: WPA2/RSN preauthentication '@@@'
  • Some models: Operation mode - Some NASes have an Operation mode setting, which pre-sets/locks some settings to defaults that are appropriate for different kinds of uses, e.g. "DSL Router", "Wireless Router", "Wireless Access Point". This varies by model, but usually something like "Wireless Access Point" is a good first choice, if available, alternatively "Wireless Router"

TP-Link Archer C20 v4 00000004

In this model, the "Reauthentication period" setting is not available, but the router does honor the timeout specified by the RADIUS server. Operation mode can be set to "Access Point". All other settings should be set as mentioned above. This is a dual band router and some settings need to be set in two places, once for each SSID.

TP-Link TD-W8968 V4 0x00000001

In this model, the "Reauthentication period" setting is available as "Network Re-auth Interval", and the router also does honor the timeout specified by the RADIUS server, and follows whichever is smaller. Setting the setting to zero causes the feature to be disabled and the timeout specified by the RADIUS server to also be ignored. Operation mode can be set to "Wireless Router Mode". All other settings should be set as mentioned above.

TP-Link TL-WR740N v4 00000000

In this model, the "Reauthentication period" setting is not available, and the router does not honor the timeout specified by the RADIUS server. The hostapd daemon running on this router supports the feature and works in a similar way as in the above TP-Link TD-W8968 V4, but no option to configure it is available in the web interface, and hostapd is run with this setting set to 0. Judging by the source code of this (very old) version of hostapd, it is believed (but not tested) that this means, once authenticated, the router might allow the supplicant to continue being part of the network for up to twelve hours without querying the RADIUS server again. A fix is possible to make hostapd run with its default setting of 3600 seconds, but is out of the scope of this wiki page. No operation mode setting is available. All other settings should be set as mentioned above.

Supplicant configuration

Linux

  1. Copy the .ca file generated during certificate generation onto the computer.
  2. Select the network's SSID from the list in Network Manager.
  3. When asked, enter the following information, then press connect:
    CA certificate: Browse and select the .ca file
    Identity: the username 
    Password: the password
    Leave all other fields as they are

Android

  1. Copy the .ca file generated during certificate generation onto the phone.
  2. Open the “Settings” app, go to “Wi-Fi” → “Advanced settings” → “Install certificates”.
  3. Select the .ca file.
  4. Assign it a name of choice
  5. Under “Certificate use” select “WiFi”
  6. Once again, open the “Settings” app, go to “Wi-Fi”, and select the network's SSID from the list.
  7. When asked, enter the following information, then press connect:
    CA certificate: Select the earlier chosen name when installing the .ca file
    Identity: the username 
    Password: the password
    Leave all other fields as they are

Windows 10

  1. Select the network's SSID from the list of wireless networks
  2. Enter username and password
  3. When prompted whether to trust the server, confirm

Mac OS

  1. Select the network's SSID from the list of wireless networks
  2. Enter username and password
  3. When prompted whether to trust the server, confirm

iPhone

  1. Select the network's SSID from the list of wireless networks
  2. Enter username and password
  3. When prompted whether to trust the server, confirm

Security observations

On Linux and Android supplicants it is required to install the .ca file generated during certificate generation in order to verify the RADIUS server's identity. In case the identity presented by the RADIUS server changes at any point, the supplicant fails to connect, and re-presents the user with the prompt for network credentials. It is possible to connect without installing the .ca file, but one needs to specify "No CA certificate required" or "Do not validate". In this case the supplicant will send credentials to any RADIUS server for that SSID without verifying its identity. It is possible to avoid sending the real user name in the unencrypted outer tunnel, by specifying a different value (normally 'anonymous') in the "Anonymous identity" field.

On Mac OS and iPhone supplicants, when connecting to the SSID for the first time, the server certificate's details are presented to the user and the user is asked if they want to trust the server. In case the identity presented by the RADIUS server changes at any point, the user will be prompted with a message, not containing any warning, sadly, that looks identical to the one displayed when connecting for the first time, where a user is extremely likely to press Trust once again. On Mac OS, it is also possible to copy the .ca file and install it, avoiding the prompt on first connect, on iPhone, on the iPhone this was tested on, this did not have any effect. It seems to be possible, but greatly complicated (involving installing a software from the App Store, and using it to create a configuration profile which then needs to be saved to a file, copied and imported onto the supplicant device) to configure the supplicant to not send the real user name in the unencrypted outer tunnel.

Windows 10 '@@@'

Sources

https://wiki.freeradius.org/guide/Basic-configuration-HOWTO

https://wiki.freeradius.org/guide/SQL-HOWTO-for-freeradius-3.x-on-Debian-Ubuntu

https://wiki.freeradius.org/modules/Rlm_python

https://wiki.freeradius.org/config/Certificates

http://deployingradius.com/documents/configuration/certificates.html

http://deployingradius.com/documents/protocols/compatibility.htmlChild page description:

Overview - Description of the solution and documentation of custom scripts

Installation - Instructions on installing, and attachment containing scripts