Configure Freeradius
Table of contents
Configure Clients
/etc/freeradius/clients.conf
The only really required settings (in a standard configuration) are the ip address(es) for your NAS (Network Access Server, for example it could be your access point) and the secret.
client <name> {
ipaddr = <CIDR for your subnet or specific IP>
secret = <a strong password>
}
Configure radiusd
/etc/freeradius/radiusd.conf
The only change that we’ll do to this file, is to disable proxying requests, since we don’t need that.
#
# To disable proxying, change the "yes" to "no", and comment the
# $INCLUDE line.
#
# allowed values: {no, yes}
#
proxy_requests = no
#$INCLUDE proxy.conf
Configure Modules and Site
Modules and Sites to enable
For our use case, this is the list of modules and sites needed.
/etc/freeradius/
|-- mods-enabled
| |-- always -> ../mods-available/always
| |-- attr_filter -> ../mods-available/attr_filter
| |-- cache_auth -> ../mods-available/cache_auth
| |-- date -> ../mods-available/date
| |-- detail -> ../mods-available/detail
| |-- detail.log -> ../mods-available/detail.log
| |-- eap -> ../mods-available/eap
| |-- expr -> ../mods-available/expr
| |-- ldap_google -> ../mods-available/ldap_google
| |-- linelog -> ../mods-available/linelog
| |-- preprocess -> ../mods-available/preprocess
| |-- unpack -> ../mods-available/unpack
| `-- utf8 -> ../mods-available/utf8
|-- sites-enabled
| `-- default -> ../sites-available/default
|
[...]
To enable a module or a site, you simply create a symbolic link in either the mods-enabled/ or sites-enabled/ directory.
For example, to enable the eap module, run this from within the mods-enabled/ directory:
ln -s ../mods-available/eap eap
Configure eap module
/etc/freeradius/mods-available/eap
We only need eap-tls, so the first thing we do is:
set default_eap_type = tls
You can delete (or comment out) all other eap types (md5, pwd, gtc, ecc). Just leave tls.
The next thing we have to do is to configure some tls details.
We need to tell the module where our server certificate and key are located and what is the password to decrypt the key.
By default, freeradius uses the same file (server.pem) to store both the certificate and the key.
In our case, the key is in a separate file (server.key)
private_key_password = mystrongkeypassword
private_key_file = ${certdir}/server.key
certificate_file = ${certdir}/server.pem
Comment out the ca_file variable, we’ll have openssl look for all certificates stored in ca_path (by default is /etc/freeradius/certs for debian).
#ca_file = ${cadir}/ca.pem
ca_path = ${cadir}
In case you have properly set up Active Revocation with CRL, you can set freeradius to check CRL for expired certificates by setting
check_crl = yes
check_all_crl = yes
Be careful with this, if radius cannot reach your CRL or if your CRL expires, it will reject all your clients.
Configure ldap_google module
/etc/freeradius/mods-available/ldap_google
We will use this module only to authorize our users, not to authenticate them.
There’s a subtle difference.
Within the authorize section, we will only check if the user exists in our Google Directory and retrieve some information.
The ldap authentication process instead requires the user to provide their password and use the provided credentials to bind as the user.
We don’t need to authenticate the users using ldap, we already know that we can trust the username stored in the certificate that they offer. The only way they can obtain a certificate is to connect to our SCEP server and the only way to connect is to have a configuration profile that we have pushed which includes their username.
So the only thing we need is to check that the user still exists in our directory and retrieve the information we need (for example, the VLAN id).
To do this, we have to set up an ldap client from our Google Admin console, save our credentials and download the certificate-key pair, which freeradius will use to authenticate and bind as admin to Google’s ldap instance.
We then go to set up the connection:
# The server URL is standard, no need to chance it
server = 'ldaps://ldap.google.com:636/'
# Google LDAP client username and password as generated during
# client creation.
identity = 'Username'
password = 'Password'
# The primary domain of your organization (let's say is domain.com)
base_dn = 'dc=domain,dc=com'
Let’s go down to the tls section now to tell it where to find our certificate-key pair
tls {
# By default ${certdir} is raddb/certs/. You can
# please these files anywhere you want. The only
# requirement is that they are readable by
# FreeRADIUS, and NOT readable by anyone else on the
# system!
#
certificate_file = ${certdir}/google/certificate.crt
private_key_file = ${certdir}/google/key.key
require_cert = 'allow'
}
Now we can configure any attributes that we need to retrieve from ldap.
In my case, I set a custom attribute for each user where I store the VLAN id. I called this attribute vlan-id.
This is how you retrieve this attribute. I added this to the control list, because I want to cache this attribute, so that after we can retrieve it and add it to the reply list. I strongly recommend to cache ldap responses, because Google’s ldap is not fast (see below).
update {
control:Tunnel-Private-Group-ID := 'vlan-id'
control: += 'radiusControlAttribute'
request: += 'radiusRequestAttribute'
reply: += 'radiusReplyAttribute'
}
Configure cache_auth module
/etc/freeradius/mods-available/cache_auth
So, this module will help us to cache ldap responses.
This way, after the first bind, freeradius won’t need to contact Google again to authorize the user and retrieve any attributes and this will make the whole process A LOT faster.
Look for the cache_auth_accept section and set the attributes that you want to be cached after a successful bind in the update section.
cache cache_auth_accept {
driver = "rlm_cache_rbtree"
key = "%{md5:%{%{Stripped-User-Name}:-%{User-Name}}%{User-Password}}"
ttl = 7200
update {
&control:Tunnel-Private-Group-Id := &control:Tunnel-Private-Group-Id
}
}
Configure default site
/etc/freeradius/sites-available/default
It’s now time to configure our site. You can use the default site and edit it.
There are 4 sections we care about for our use case:
- listen
- authorize
- authenticate
- post-auth
The listen section tells the server what to listen and where to listen for it.
You can keep the settings as in the default site.
The authorize section is where we check if we want to keep going with the client and we set the authentication method.
autorize {
# Sanitize username and request content
filter_username
preprocess
# Split up user names in the form user@domain
split_username_nai
#
# Check the authentication cache to see if this user
# recently sucessfully authenticated
#
update control {
&Cache-Status-Only := 'yes'
}
cache_auth_accept
#
# If there's a cached User-Name, we can skip ldap
# Otherwise, we go through it (and cache the result)
#
if (notfound) {
ldap_google
if (updated) {
cache_auth_accept
}
}
# Reject if user doesn't exist
if (notfound) {
reject
}
# We can do additional checks on the user domain
if (&Stripped-User-Domain != 'domain.com') {
reject
}
#
# If we got here, it means that the user exists and
# is authorized. Let's go on with eap.
#
eap {
ok = return
updated = return
}
}
Next is the authentication section. It’s very easy, we just need to enable eap authentication and the server will know what to do
authenticate {
# Allow EAP authentication.
eap
}
In post-auth we do some final checks, cache (or retrieve) user information and update the reply to set the VLAN id.
Anything after the update reply block is the existing post-auth section content for the default site.
post-auth {
#
# Reject packets where User-Name != TLS-Client-Cert-Common-Name
# There is no reason for users to lie about their names.
#
verify_tls_client_common_name
#
# Retrieve/Cache info
#
cache_auth_accept
#
# Update reply with VLAN information
#
update reply {
Tunnel-Type := 13
Tunnel-Medium-Type := 6
# In case no VLAN is assigned from ldap or from cache, we assign a guest VLAN
&reply:Tunnel-Private-Group-ID = "%{%{control:Tunnel-Private-Group-Id}:-100}"
}
#
# The session-state attributes are automatically deleted after
# an Access-Reject or Access-Accept is sent.
#
if (session-state:User-Name && reply:User-Name && request:User-Name && (reply:User-Name == request:User-Name)) {
update reply {
&User-Name !* ANY
}
}
update {
&reply: += &session-state:
}
# Remove reply message if the response contains an EAP-Message
remove_reply_message_if_eap
Post-Auth-Type REJECT {
attr_filter.access_reject
eap
remove_reply_message_if_eap
}
Post-Auth-Type Challenge {
# remove_reply_message_if_eap
# attr_filter.access_challenge.post-auth
}
# If the client sends EAP-Key-Name in the request,
# then echo the real value back in the reply.
#
if (EAP-Key-Name && &reply:EAP-Session-Id) {
update reply {
&EAP-Key-Name := &reply:EAP-Session-Id
}
}
}
There are other sections which are a bit less critical for our use case, leave them there like this.
#
# Pre-accounting. Decide which accounting type to use.
#
preacct {
preprocess
#
# Ensure that we have a semi-unique identifier for every
# request, and many NAS boxes are broken.
acct_unique
}
#
# Accounting. Log the accounting data.
#
accounting {
detail
# Filter attributes from the accounting response.
attr_filter.accounting_response
}
session {
}