Today I’m diving in to the world of network access control! Being able to authenticate network devies plugged in to your switches is a great way to improve network security without resorting to unplugging or disabling every unused port on yout equipment. Now every switch port is universal, and will enable on demand based on what is plugged in. While I couldn’t go through the complete authorization part of the setup (mapping devices to VLANs), I’m planning on making a future video for that step.

Contents

Video

Video Thumbnail

Certificates Overview

RADIUS and EAP love nesting, so we need a lot of keys here. In particular:

  • Server cert for RADsec
  • Client cert for RADsec (per-authenticator, which is an AP or Switch)
  • Server cert for EAP (may be the same as RADsec)
  • Client cert for each client

RADIUS is not a particularly secure protocol on its own. It operates over UDP 1812/1813 and has no encryption at all. Messages are authenticated using a ‘shared secret’ (which is often words instead of randomly generated), and relying on an MD5 hash. MD5 isn’t a great hash function in 2024 (it’s cryptographically considered insecure already, and so is its replacement SHA1). It’s also far from a password-quality hash for dealing with poor human-generated sources, so it’s basically entirely broken. Most of the RADIUS payloads do their own encryption all the way to the client, using TLS, but this still doesn’t encrypt our Accept decision which is sent over the clear to the authenticator.

Two industries took on the bad RADIUS security problem in different ways - the telecom industry decided to go off into the weeds and build something completely different and far more complex with far more ASN.1 and called it DIAMETER (ha ha). The normal people took the whole RADIUS protocol (MD5 and all) and dropped it inside a TLS session, where TLS provides security and RADIUS uses a hardcoded ‘secret’ (radsec is the literal secret).

So now we need to implement RADsec, which relies on mutual TLS (this should sound familiar if you follow me). So, for the TLS Sessions for RADsec, we need a server cert (for FreeRADIUS) and a client cert (for our authenticators). These can be from any CA, but since all of this equipment is our own it would make sense to use a private CA for all of these.

Then, separately, we are either going to do EAP-TLS (preferred) or EAP-PEAP (not preferred but commonly implemented) to the clients. Both of these encapsulate in TLS. EAP-TLS uses mutual TLS to authenticate the client, and EAP-PEAP uses TLS to authenticate the server and then MS-CHAPv2 to authenticate the client (probably using MD5 again).

I have three different methods for generating certs, which I have used in videos at this point:

  • Use OpenSSL and do it entirely manually
  • Use Smallstep and run a full private PKI setup
  • Use FreeRADIUS’s testing certs

Your choice depends on what you want to get out of the full setup. I’d also like to remind you that one cert per machine is very normal, you don’t need a different set of certs for mtls and also vpn and also 802.1x.

FreeRADIUS Test Certs

To generate FreeRADIUS test certs (after installing FreeRADIUS):

#Go to this directory, you must cd here
cd /etc/freeradius/3.0/certs
#Make the CA initially
make

#If you change configs and re-run make, it should regenerate
#In case it doesn't, you can run destroycerts
make destroycerts
make

My Easy OpenSSL Method

This method generates all of the certs we will need today by copying/pasting. Edit the commands before you run them to change the options. It’s easier to understand than the makefile, but also not very scalable. If you watched my mTLS video, it’s basically the same as that. You should probably use Smallstep for production-like things.

Certificate Authority

Same as mtls, simple 384-bit key

#Generate private key using scep384:
openssl ecparam -name secp384r1 -genkey -out root.key

#Sign the root certificate
#Pathlen:0 means there can be only one more cert below this CA (no more CAs)
#Make sure you update the subj name with your own names
#C=US is also the country, it's optional
#O= is the organization, also optional
#CN= is the Common Name and it's required
#I also set validity to 69 years, make sure you watch for expiration (manually)
openssl req -new -key root.key -x509 -nodes -days 25202 -out root.pem -subj "/C=US/O=apalrd.net/CN=radius" -addext "basicConstraints=critical,CA:TRUE,pathlen:0"

#CA cert must be readable by freerad
chown freerad:freerad root.pem

#Now you can view it (for fun)
openssl x509 -in root.pem -text -noout

Server

The same cert is used for both radsec and eap on the server side.

export sv=raddb.apalrd.net
#Generate scep256 key for this client
#If you want 192-bit security, use scep384r1 instead of prime256v1
openssl ecparam -name prime256v1 -genkey -out $sv.key

#Generate a CSR (certificate signing request) for my new key
#again, C and O are optional, CN is the Common Name of the cert
#Allowed only for server auth (in both radsec and eap roles, we are the server)
#Set for TOFU policy (network asks the user to trust on first use)
#Change certificatePolicy to 1.3.6.1.4.1.40808.1.3.1 for strict (client must be pre-configured to trust CA)
openssl req -new -key $sv.key -out $sv.csr -subj "/C=US/O=apalrd.net/CN=$sv" -addext "extendedKeyUsage = serverAuth" -addext "certificatePolicies = 1.3.6.1.4.1.40808.1.3.2"
#TODO crlDistributionPoints

#Sign the CSR using the root
#Shorter lived at only 365 days
openssl x509 -req -in $sv.csr -CA root.pem -CAkey root.key -CAcreateserial -out $sv.crt -days 365 -sha256 -copy_extensions=copyall

#Must add CA cert after server cert
cat root.pem > $sr.crt

#Server key must be readable by freerad
chown freerad:freerad $sv.key $sv.crt

#Now you can view it (for fun)
openssl x509 -in $sv.crt -text -noout

Client

You need one of these for each authenticator (wifi AP / switch), and also one for each EAP client. Change the name (export cl) each time. Common practice is to use a FQDN for device certs and user@domain for user certs

export cl=wap1.apalrd.net
#Generate scep256 key for this client
#If you want 192-bit security, use scep384r1 instead of prime256v1
openssl ecparam -name prime256v1 -genkey -out $cl.key

#Generate a CSR (certificate signing request) for my new key
#again, C and O are optional, CN is the Common Name of the cert
openssl req -new -key $cl.key -out $cl.csr -subj "/C=US/O=apalrd.net/CN=$cl" -addext "extendedKeyUsage = clientAuth"

#Sign the CSR using the root
#Sign it allowing for server and client auth as the key usage
openssl x509 -req -in $cl.csr -CA root.pem -CAkey root.key -CAcreateserial -out $cl.crt -days 365 -sha256 -copy_extensions=copyall

#Now you can view it (for fun)
openssl x509 -in $cl.crt -text -noout

#Now let's package it into a P12 archive so you can send it to your favorite client device
#You *must* enter a password here or some OSes will not accept the P12
#The password just encrypts the P12 file itself
openssl pkcs12 -export -out $cl.p12 -in $cl.crt -inkey $cl.key

Install FreeRADIUS

I’m running FreeRADIUS on a Debian 12 system (LXC continer, unprivilaged). Start by installing Debian 12 using your favorite method, and updating it fully (apt update && apt full-upgrade -y).

#Install from deb packages
apt install freeradius -y

Now we’re going to move over to /etc/freeradius/3.0 where we are going to hang out for a long time

The folder structure contains two directories with configs (mods-available and sites-available), which is what we edit, and when we want to enable them, we create a symlink in the corresponding *-enabled directory. This is similar to how a lot of distros package Apache and nginx, for example.

Systemd Override

To use client certificates (in both RADsec and in EAP-TLS), we need a temp directory for FreeRADIUS. Systemd is smart enough to create a per-service /tmp which we should just use, but FreeRADIUS is dumb enough to demand a directory which already exists, and then tries to chmod/chown it on startup (which Systemd has prevented). So, edit the systemd service (systemctl edit freeradius) and add this to the top:

[Service]
User=freerad
Group=freerad
RuntimeDirectory=freeradius freeradius/tmp
RuntimeDirectoryPreserve=yes

RADsec Configuration

This file goes in sites-available/radsec and configures the clients. Then, create a symlink to it (ln -s ../sites-available/radsec sites-enabled/radsec). I have cut a lot of the documentation out of this file, but if you want to read it, I started from the file sites-available/tls.

######################################################################
#
#  RADIUS over TLS (radsec)
#
######################################################################

listen {
    # FreeRADIUS is stupid sometimes / often
    # ipaddr = * means any *IPv4*
    # ipaddr = :: means any *IPv6*
	ipaddr = ::
	port = 2083

	# Allow both Auth and Accounting
	type = auth+acct

	# For now, only TCP transport is allowed.
	proto = tcp

	# Send packets to the default virtual server
	virtual_server = default

    # This is a client name, for the `radsec` section later
	clients = radsec

	# Connection limiting for sockets with "proto = tcp".
	limit {
	      #  Limit the number of simultaneous TCP connections to the socket
	      max_connections = 16

	      #  The lifetime, in seconds, of a TCP connection.  After
	      #  this lifetime, the connection will be closed.
	      lifetime = 0

	      #  The idle timeout, in seconds, of a TCP connection.
	      #  If no packets have been received over the connection for
	      #  this time, the connection will be closed.
	      idle_timeout = 30
	}

    # TLS Crypto section
	tls {
        # If your private key is encrypted
		# private_key_password = whatever
		private_key_file = /etc/freeradius/3.0/certs/raddb.apalrd.net.key

		# Certificate file
        # If key + cert are in the same file, use the same filename as above
		certificate_file = /etc/freeradius/3.0/certs/raddb.apalrd.net.crt

		# Trusted Root CA list
        # Combine all roots one after another into this file
		ca_file = /etc/freeradius/3.0/certs/root.pem

		# Fragment size (read the docs if you want to change)
		fragment_size = 8192


		# Set this option to specify the allowed
		# TLS cipher suites.  The format is listed
		# in "man 1 ciphers".
		cipher_list = "DEFAULT"

		# If enabled, OpenSSL will use server cipher list
		cipher_server_preference = no

		#  Older TLS versions are deprecated.
		tls_min_version = "1.2"
		tls_max_version = "1.3"


		#  Session resumption / fast reauthentication cache
		cache {
            enable = yes
            lifetime = 24 # hours
			name = "TLS RADSEC"
			persist_dir = "${logdir}/tlscache"
		}

		#  Require a client certificate.
		require_client_cert = yes

		# Verify client certs (i.e. using OCSP)
        # Configured to use the CA we setup earlier
		verify {
			# Due to some *quirks* in FreeRADIUS, it expects to be able to
            # manipulate the permissions of this directory
            # systemd would rather isolate it itself, so we need
            # a systemd override to use this feature
            tmpdir = /run/freeradius/tmp

			#  The command used to verify the client cert.
			#  We recommend using the OpenSSL command-line
			#  tool.
	        client = "/usr/bin/openssl verify -CAfile /etc/freeradius/3.0/certs/root.pem %{TLS-Client-Cert-Filename}"
		} #end verify
	} #end tls
} #end server

clients radsec {
    #Local host
    client localhost {
        ipaddr = ::1
        proto = tls
        #When using radsec, secret MUST be 'radsec'
        #this is literally written in the RFC
        secret = radsec
    }
    #Local network, in CIDR notation
    #Since we are using cert based auth, it would not be unreasonable
    #to allow all (::/0)
    client localnet {
        ipaddr = fd69:beef:cafe::/48
        proto = tls
        secret = radsec
    }
}#end radsec

EAP Module Configuration

Next, we need to setup the certs we will use for EAP. These are different than the ones we use for RADsec. This configuration goes in mods-available/eap, and I’ve written out the whole file so you can replace it if you want. Again, I trimmed comments for features I’m not using, so read the original for more context.

######################################################################
#
#  EAP-TLS and no other modes
#
######################################################################
eap {
	#  Invoke the default supported EAP type when
	#  EAP-Identity response is received.
	default_eap_type = tls

	# EAP-Request/Response cache timeout
	timer_expire = 60

	# Reject unknown types
	ignore_unknown_eap_types = no

	#  Help prevent DoS attacks by limiting the number of sessions we track
	max_sessions = ${max_requests}


	#  Common TLS configuration for TLS-based EAP types
	tls-config tls-common {
		#private_key_password = whatever
		private_key_file = /etc/freeradius/3.0/certs/radius.apalrd.net.key
		certificate_file = /etc/freeradius/3.0/certs/radius.apalrd.net.crt


		#  Trusted Root CA list *for clients*
		ca_file = /etc/freeradius/3.0/certs/root.pem

		#  Set this option to specify the allowed TLS cipher suites
		cipher_list = "DEFAULT"

		#  If enabled, OpenSSL will use server cipher list
        #  In 2024 leave this disabld
		cipher_server_preference = no

		#  Set min / max TLS version.
		#
		#  While the server will accept "1.3" as a value,
		#  most EAP supplicants WILL NOT DO TLS 1.3 PROPERLY.
		#
		#  i.e. they WILL NOT WORK, SO DO NOT ASK QUESTIONS ON
		#  THE LIST ABOUT WHY IT DOES NOT WORK.
		#
		#  The TLS 1.3 support is here for future
		#  compatibility, as clients get upgraded, and people
		#  don't upgrade their copies of FreeRADIUS.
		#
		#  Also note that we only support TLS 1.3 for EAP-TLS.
		#  Other versions of EAP (PEAP, TTLS, FAST) DO NOT
		#  SUPPORT TLS 1.3.
		#
		tls_min_version = "1.2"
		tls_max_version = "1.2"

		#  Client certificates can be validated via an
		#  external command.  This allows dynamic CRLs or OCSP
		#  to be used.
		verify {
		    tmpdir = /run/freeradius/tmp

            #  The command used to verify the client cert.
			#  We recommend using the OpenSSL command-line
			#  tool.
            client = "/usr/bin/openssl verify -CAfile /etc/freeradius/3.0/certs/root.pem %{TLS-Client-Cert-Filename}"
		}
	}


	#  EAP-TLS
	tls {
		# Point to the common TLS configuration
		tls = tls-common
	}

    # EAP PEAP
	peap {
		# Point to the common TLS configuration
		tls = tls-common

        # Name of virtual server to handle the inner tunnel
		virtual_server = "inner-tunnel"

		#  The tunneled EAP session needs a default
		#  EAP type which is separate from the one for
		#  the non-tunneled EAP module.
		default_eap_type = mschapv2

        # Other things you don't need to change
        # I cut out paragraphs of docs here
		copy_request_to_tunnel = no
		use_tunneled_reply = no
	} #end eap peap

	#  EAP-MSCHAPv2
	#  This will be used *inside* PEAP
	mschapv2 {
        #We actually need no config here
	} # end eap mschapv2

} # end eap

Inner Tunnel Configuration (Optional)

If you need to support clients which only speap EAP-PEAP, then we can configure the ‘inner’ tunnel. This is basically an EAP session (EAP-MSCHAPv2) inside of another EAP session, and how anyone thought this was intelligent is beyond me.

If you are NOT using inner tunnel, you can remove the symlink to disable the virtual server: rm sites-enabled/inner-tunnel

If you ARE using inner tunnel, you can leave the symlink alone. The default file is fine.

Default Server Configuration (Optional)

Since the default server listens on the (insecure) RADIUS ports 1812 and 1813, I removed those lines from the default file (sites-available/default). In my setup, this is from line 48 (the line immediately after server default {) to line 272 (right before the comment block for authorize {).

This is purely optional. The current attacks on non-radsec RADIUS all involve attacking the authenticator side and allowing access, so having the RADIUS server exposed in this way is not believed to be a security risk. FreeRADIUS is also protected by not having any clients defined by default, so connections would be rejected anyway. Regardless, I don’t want that protocol in use, so I disabled it.

If you like using the radtest tool, you could change it to bind to ::1 instead of ::, so radtest still works unencrypted on localhost.

Users File (Optional)

The Users file (users, right in the freeradius directory) stores user info when logging in with a username+password. It’s not used in EAP-TLS, since we instead trace the cert and don’t actually look at the identity at all, but it’s used in PEAP-MSCHAPv2. If you are enabling that one, then here’s an example users file:

#
# 	Configuration file for the rlm_files module.
# 	Please see rlm_files(5) manpage for more information.
#
#   All lines start with the username, followed by the conditions to accept
#   Remaining lines are responses. Read the original users file for more help.
#

# My stupid camera that won't do TLS, only PEAP
camera3 Cleartext-Password := "camera3"

## Same, but only on port 3 of the switch with the right hostname
camera5 Cleartext-Password := "camera5", NAS-Port-Id == "ether3", NAS-Identifier == "sw5.palnet.net"

## Same, but with a specific MAC address
camera7 Cleartext-Password := "camera7", Calling-Station-Id == "08-ED-ED-F1-1B-32"
    Tunnel-Type = VLAN,
    Tunnel-Medium-Type = 802,
    Tunnel-Private-Group-ID = "10"
    #Add it to the camera VLAN

## Same, but stuck on a VLAN 9
camera11 Cleartext-Password := "camera11"
    Tunnel-Type = VLAN,
    Tunnel-Medium-Type = 802,
    Tunnel-Private-Group-ID = "9"
    #Add it to the camera VLAN


# DEFAULT reject them
# You could also := Accept them
# and give them attributes like default vlan
DEFAULT Auth-Type := Reject