• FreeRADIUS
  • Linux

Using FreeRADIUS to perform EAP-TLS and PAP

In my journey to make CavanaSystems more open I wanted to replace my existing Microsoft NPS farm with a piece of Free Software… and found an amazing one!

Make your requirements

As with any IT project, you have to start with your requirements.
Mine were:
1. High Availability
2. PAP will be used for authenticating and authorizing SSH sessions to network devices
3. EAP-TLS will be used to provide network-level authentication and dynamic VLAN assignment for cable and wireless access
4. Authentication and Authorization decisions must be taken accordingly to a centralized user store (Active Directory Domain Services)
5. Authentication events must be logged on the RADIUS Server locally (for now)

The existing system

These are the policies that where implemented on the outgoing system.
For cable access:

The Microsoft NPS Server showing a policy.

For wireless access:

The Microsoft NPS Server showing a policy.

And at last, SSH CLI Authentication and Authorization for Cisco devices:

The Microsoft NPS Server showing a policy.

The infrastructure was made of two servers, failover managed by the NAS devices (as it’s typical in a RADIUS deployment).

FreeRADIUS basics

This post is not a tutorial, it’s of no use that I go about and rewrite what is already explained very well by FreeRADIUS creators and book publishers; instead, here’s a list of things you have to read to understand how this thing works:

1. FreeRADIUS man pages:

2. FreeRADIUS technical guide:

3. The comments in the configuration files (if you have read the manual pages above you already know this)

While I think that FreeRADIUS itself is a very good piece of software, its documentation is quite good but not awesome. It lacks something that ties everything together, which for me was a book: “FreeRADIUS Beginner’s Guide” of Dirk van der Walt, published by Packt Publishing. I highly recommend buying a hard copy of it (you are going to take notes on it).
It’s based on FR 2 but the server hasn’t changed in its core design, just keep handy the /etc/raddb/README.rst file which documents what’s changed from version 2 to 3.

Designing the solution

By now you already know (because you read the goddamned man, didn’t you?) that every RADIUS packet that comes into FreeRADIUS goes through a very specific pipeline; the exact shape of it depends on which type of packet the server is processing (Authentication or Accounting) and the decisions you are exerting over that packet (Are you proxying it or authenticating/accounting it locally? Will it go through all the processing sections applicable to that packet or will it be discarded early in the process? …).
In this phase we are designing how our packets will be processed.

PAP Requests:
1. When the server receives a PAP request it should check the Directory via LDAP to gather the user’s group information
2. Then it must set the Auth-Type to pam (the servers are joined to the AD Domain and as such, capable of Kerberos authentication via the system’s PAMs)
3. Authentication is performed via the appropriate PAM module
4. Additional RADIUS AVPs and VSAs are added via unlang policies, when there is a match from an AD group gathered from the authorize phase

EAP-TLS Requests:
1. When the server receives an EAP-TLS request it should check the Directory via LDAP to gather the computer’s group information
2. Authentication is performed via FreeRADIUS EAP module, which will check the client certificate revocation information against the CA OCSP responder
3. Additional RADIUS AVPs and VSAs are added via unlang policies, when there is a match from an AD group from the authorize phase

Making things work

Configuring the LDAP module instances

FreeRADIUS modules support instantiation; this is achieved by putting an arbitrary name after the module name. For the sake of consistency it’s recommended to keep the original module name followed by the instance name of your choice.

The first instance takes care of retrieving group information for a user account:

The second instance does the same for a computer account, but the LDAP query is different so it’s the only piece of information reported here.
Be aware that computers account User-Name attribute is not immediately usable as received by the NAS, so it needs some unlang transformation that will be explained shortly.

Configuring the EAP module

It’s quite a standard EAP module configuration with EAP-TLS and OCSP checking with soft-fail enabled.
TLS cache is cleaned by a cron job not shown here.

Unlang policies

It’s quite simple. It goes like this:
– IF you come from a specific subnet (the one where network devices have their management interfaces) AND you are SSH-ing into one of such devices AND you are part of a LDAP group, then you get some VSAs and AVPs (good for you!).
– IF you don’t match the preceding criteria, continue with the packet processing (we need this as we can process some other requests differently).
– ELSE we reject you.
Be mindful that since we have used LDAP module instantiation here, you have to check against the ldap_users-LDAP-Group AVP (you’ll see that in radiusd output when run in debug mode with -X).

We have two kinds of policies here; the first two just perform dynamic VLAN assignment: they put some more AVPs in the reply based on the presence of other AVPs in the request.
Be mindful that because we have used LDAP module instantiation here, you have to check against the ldap_computers-LDAP-Group AVP.

The second kind, it performs a check the EAP module doesn’t do.
This check is already present into the default FreeRADIUS configuration, but it’s disabled as it requires some tweaking for your local site, and specifically if you have to deal with Windows clients.

The Windows 802.1x supplicant will send it’s User-Name like this: host/UPPERCASE_HOSTNAME.lowercase.domain.

Who doesn’t love this?
We have to strip the host/ part then catch the rest through a regular expression, then validate it against the hostname in the certificate (which Windows requests in lowercase, just to piss you off. Yes, I checked the .X500 RFC 1617 which is what standardizes the components you see in LDAP based directories and .X509 style certificates such as TLS ones. It’s case insensitive so if Windows wanted it could have requested the certificate with the same uppercase hostname).
This policy works for both the Windows supplicant (and its habits) and also with “sane” supplicants which will send the User-Name attribute equal to its certificate subject.

Don’t forget to declare Sanitized-Host-Name in FreeRADIUS dictionary as type string.

This thing is so flexible that you will never stop tinkering.

The site definition

I will summarize things a little for the sake of being on spot, it’s not how it actually looks and it probably won’t work if you paste it verbatim. Or it may. Anyhow, the parts concerning this post are correct.
We make a trick similar to what you just saw to get a usable Stripped-User-Name to use in the LDAP query to gather computer’s group membership. If the User-Name does not match our regex then in the LDAP query the User-Name attribute will be used instead.

Note that User-Name!= Sanitized-Host-Name as the latter is just the host name, while the former is what the supplicant provides (which can be horrible in the case of Windows).

If you are wondering why I checked if the packet was EAP before testing for EAP-TLS, it’s because FR will throw a warning in the debug output when doing PAP: if the message being processed is not EAP, all EAP-related AVPs are not present. It will work the same, but it was bothering me.

Testing it

Does it work? Yes it does.
Here’s my debug output when FreeRADIUS processes a PAP message.

From this output you can see that the request took the correct path; I left a debug_all open so you could see the 3 main attribute lists populated (request, control and reply).

And here is the server debug output when processing an EAP-TLS request; it will make different round-trips so I took only the most important exchanges between the NAS and FreeRADIUS.

Here something very interesting happened. As soon as the eap module in the authorize section was hit, it exited with return which means “stop processing this section, as I have passed and there is nothing more interesting to be done here” and it goes straight to the authenticate section where the eap module send an Access-Challenge.

The following message reaches the authorize section where we can finally query our LDAP Directory because the client has presented itself in response to the earlier Access-Challenge:

In this exchange we queried the LDAP directory and obtained the groups the PC is a member of.

There are a few more exchanges where the client negotiates the EAP method and a few others which we omit. Finally, the EAP-TLS submodule is called to perform the validation of the client certificate:

The certificate is validated and the authenticate section is handled. The only missing step is post-auth were we process the policies defined earlier:

I left the debugging on so you could see what was the state of the AVPs the server used to make its decisions.

Changes into radiusd.conf

Don’t forget to disable proxying and other components you are not using.
Enable auth logging.

That’s it. The only two very minor things I couldn’t wrap my head around were why radiusd sees the client certificate as an untrusted intermediate CA when it actually is the client certificate itself and why it fails (crashes during startup) the StartTLS method for binding via LDAP.

I worked around the LDAP situation by just using explicit LDAPS on port 636, and decided to leave the certificate warning alone (after testing it is actually validating the certificate by testing a mixture of untrusted CAs and trusted ones but with wrong key usage).

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.