Introduction to FreeRADIUS: LDAP Authentication and Authorization
In this article, we will create a scenario in which there will be two user groups who have different privileges for managing the network. When one of the users login to the router (NAS), a RADIUS request will be created and sent to the FreeRADIUS server. Then the RADIUS server will query the LDAP (Lightweight Directory Access Protocol) server if this user is existed and has the right credentials.
These are the summarized steps. I will cover the details later in this article with packet capture by Wireshark. Following shows the network topology.
I assume you have already installed FreeRADIUS server with the default configuration and its utility packages. If not please visit here (FreeRADIUS PART 1). Since I configured NAS for authentication and authorization before in this article (https://www.golinuxcloud.com/aaa-network-policy-server/), I will not include the configuration here.
Configure FreeRADIUS for LDAP
Step-1: FreeRADIUS comes with a lot of modules and configuration files. We will not use all the files and modules. Fore sake of learning, I will use couple of them. Let’s continue with virtual server files which are stored under "/etc/freeradius/3.0/sites-enabled
" directory. Two virtual servers are configured in default when you install a FreeRADIUS server. Following shows the path and the list of the servers.
root@freeradius:/home/celal# ls -l /etc/freeradius/3.0/sites-enabled/ total 0 lrwxrwxrwx 1 freerad freerad 26 Şub 16 14:36 default -> ../sites-available/default lrwxrwxrwx 1 freerad freerad 31 Şub 16 14:36 inner-tunnel -> ../sites-available/inner-tunnel root@freeradius:/home/celal#
Notice that when I list the files under the directory, it points to another directory. It is called a symbolic link. "default
" and "inner-tunnel
" servers are not present in "/etc/freeradius/3.0/sites-enabled
" directory. Instead, they are stored in "/etc/freeradius/3.0/sites-available
". It is a concept borrowed from Apache server. It will make no difference if you store them in "sites-enabled" directory or create a symbolic link to another folder. I will go for creating the server under "sites-enabled
".
Step-2: Since the default servers contain a tone of configuration, I will back them up, then remove them from the folder.
root@freeradius:/home/celal# mkdir backup root@freeradius:/home/celal# cp /etc/freeradius/3.0/sites-enabled/default /home/celal/backup/ root@freeradius:/home/celal# cp /etc/freeradius/3.0/sites-enabled/inner-tunnel /home/celal/backup/
Step-3: Remove the default virtual servers.
root@freeradius:/home/celal# rm /etc/freeradius/3.0/sites-enabled/default root@freeradius:/home/celal# rm /etc/freeradius/3.0/sites-enabled/inner-tunnel
Step-4: Create a new virtual server with your preferred text editor. I will use nano.
root@freeradius:/home/celal# nano /etc/freeradius/3.0/sites-enabled/my_server
Then add the lines below.
server my_server { listen { type = auth ipaddr = * port = 1812 } authorize { ldap if (ok || updated) { update control { Auth-Type := ldap } } } authenticate { Auth-Type LDAP { ldap } } }
I will use ldap module in the the "Authorization" block to search for users and groups in the LDAP database. It is similar to "files" module which uses the local "users" file as source of the credentials. Since there is no authentication type set in the lines below "ldap" module, we need to set the "Auth-Type" in the "if" block. When a module is executed (in this case "ldap"), it returns a code. I will use that code as condition for setting the authentication type. Following shows the list of available return codes.
Module return code | Description |
---|---|
notfound | Information was not found |
noop | The module did nothing |
ok | The module succeeded |
updated | The module updated the request |
fail | The module failed |
reject | The module rejected the request |
userlock | The user was locked out |
With "Authentication" module, LDAP database will be used for confirming the user’s credentials.
Step-5: FreeRADIUS runs the modules in "/etc/freeradius/3.0/mods-enabled/
" directory. We need to copy ldap module to that directory. Do it as below.
root@freeradius:/home/celal# cp /etc/freeradius/3.0/mods-available/ldap /etc/freeradius/3.0/mods-enabled/
Step-6: Open ldap module with your preferred text editor an modify it as it suits you.
ldap { server = 'mydomain.com' base_dn = 'CN=Users,DC=mydomain,DC=com' identity = 'CN=celal,CN=Users,DC=mydomain,DC=com' password = ' PassWord123' user { filter = "(sAMAccountName=%{%{Stripped-User-Name}:-%{User-Name}})" } }
Before a client sends a ldap search request to the ldap server, it needs to bind, which is almost equivalent of authentication. Because of that, I need to define a ldap sever, a user for binding and a base_dn
where to search users. Since I will use a Windows LDAP, I will specify "sAMAccountName
" in the user section which is a user object attribute that holds username.
Step-7: In this step, I will define a RADIUS client with IP address and shared secret. Open "client
" file.
root@freeradius:/home/celal# nano /etc/freeradius/3.0/clients.conf
And add the lines below, modifying for yourself.
client cisco_router { ipaddr = 192.168.1.15 secret = celaldogan }
Step-8: Run FreeRADIUS in debug mode with "freeradius -X
"
As you see I got an error about eap module, because I deleted default virtual servers in the beginning. I do not need eap module, thus I will delete that module and launch it again.
root@freeradius:/home/celal# rm /etc/freeradius/3.0/mods-enabled/eap
Testing FreeRADIUS for LDAP Authentication and Authorization
There are two ways to test the FreeRADIUS server, with ssh loging to the router or using "radtest" tool. I prefer the second one this time.
Step-1: Apply the command below for testing.
root@freeradius:/home/celal# radtest jane PassWord123 127.0.0.1 1812 testing123 Sent Access-Request Id 240 from 0.0.0.0:53470 to 127.0.0.1:1812 length 74 User-Name = "jane" User-Password = "PassWord123" NAS-IP-Address = 127.0.1.1 NAS-Port = 1812 Message-Authenticator = 0x00 Cleartext-Password = "PassWord123" Received Access-Accept Id 240 from 127.0.0.1:1812 to 127.0.0.1:53470 length 20 root@freeradius:/home/celal#
Following screenshot shows debug output from the FreeRADIUS server when it receives the request.
The authentication for jane is working, but notice that there is not anything related to authorization in the screenshot above. We need to add some configuration for that.
Step-2: First we need to pull the group information from the ldap server.
Open "dictionary" file and define an attribute for ldap group.
root@freeradius:/home/celal# nano /etc/freeradius/3.0/dictionary
And add "ATTRIBUTE My_Group 3000 string
" line to the file as below.
Open ldap module.
root@freeradius:/home/celal# nano /etc/freeradius/3.0/mods-enabled/ldap
and add the line below and save the module.
control:My_Group ='memberOf'
Step-3: Configure the virtual server to check the group information and send authorization information accordingly.
post-auth { if (&control:My_Group =~ /Network Admin/) { update reply { Cisco-AVPair = "shell:priv-lvl=15" } } if (&control:My_Group =~ /Network Operator/) { update reply { Cisco-AVPair = "shell:priv-lvl=7" } } }
"post-auth" module is executed after a successful authentication. I am setting the privilege level based on the user’s group. I am checking the groups in the "if" condition with a regex.
Step-4: Capture the packets while testing with "radtest jane PassWord123 127.0.0.1 1812 testing123
" command.
The debug output from FreeRADIUS is below.
Analyzing the packets between the FreeRADIUS and the LDAP server
Packet Number 1: Before we query the ldap server, we need to bind (authenticate).
We specify that we would like to use "simple binding", which carry the credentials in clear text.
Packet Number 2: It is a "bindResponse" packet, which informs that the binding has been successful.
Packet Number 3: It is a "searchRequest" packet. We are asking ldap server to send us the user information in the attributes. Now we only need "memberOf" attribute. The other ones are the default attributes FreeRADIUS adds.
Packet Number 4: The ldap server sends the user information to the radius server in this packet.
Packet Number 5: After gathering the user’s information, we bind (authenticate) with the user (jane) in this packet.
Packet Number 6: The provided credentials are verified with this packet.
Final thoughts
LDAP database can be used for authentication and authorization. We kept the configuration files as simple as we can for learning purpose in this article.
Hello,
I can’t retrieve the LDAP user group. Can I have a sample of LDAP data with you?
Thanks in advance.
Hello who can help me? i have a problem
Did you ever get a solution to this? I have the same problem with my configuration files that lead to the same error.
Hello Celal,
Great article.
I am using Active Directory as my LDAP database, and I am getting an error message in the post-auth. I know that you have an Active Directory article but that one doesn’t address the problem I want to solve. This article does, basically I want more granularity in the user groups and I want to manipulate the response the radius server gives depending on the type of device the user is accessing and their respective AD group membership. Bellow the error I am getting:
I’m doing exactly the same, with AD and LDAP and I also have the same problem, any solution?
Hello, I followed the guide but used pfsense as freeRadius server and asking it to fetch ldap from mail server. Everything is fine but only error I’m getting is mschapv2 FAILED no NT password cannot perform authentication . Please help
Please help me,
Bind success but notfound, i use previlage admin
You probably messup with openldap config.
You need to search for user in AD in specified OU/CN, and after that, check user group.
Look so that you search user, and targeting group instead of some OU with users
How can I assign specific role for LDAP’s user every login. The default is network-operator if you don’t define any role (I need network-admin). I’ve done any research but no proper solution yet. The official document is kind of hard to understand and apply in real cases.
Thanks for your help, sir.
The role is derived from the LDAP group of which the user is a member. If the user is member of “Network Admin”, then “priv-lvl=15” attribute is sent to the switch/router.
You can see the configuration for authorization here (Testing FreeRADIUS for LDAP Authentication and Authorization in the Step 3 (https://www.golinuxcloud.com/freeradius-ldap-authentication-authorization/)).
A very useful blog! I would have 2 concerns here.
1. memberOf is rarely a single entry in AD Ldap so MyGroup would be populated by the 1st group found which might not be the one you were looking for.
2. If memberOf matches neither ‘Network Admin’ or ‘Network Operator’ it would give a user level=1 access (read-only on most switches).
Still thank you for explaining radius sites and modules in an easy-to-understand format.
To address #1 I sugest Changing
Not to familiar with post-auth so #2 would still a concern for me.
1)
memberOf
attribute can contain comma separated multiple groups.Please look at the packet coming from the server here (https://www.golinuxcloud.com/wp-content/uploads/wireshark-265.png)
we used “
=~
” operator, which matches if the request contains an attribute which matches the given regular expression.2) When memberOf matches neither of the group, FreeRadius will not add
"Cisco-AVPair = "shell:priv-lvl=X"
attribute.However, I did not test the authorization without sending the attribute.
I went ahead I tested ldap module in my lab with both
~=
,+=
, and checked debug output withradiusd -X
~=
only returns the 1st memberOf result+=
returns all memberOf resultsI confirmed that when neither group matches it logs me into my dell switch so I added a
!=
to the end of the controls to enforcepriv-lvl=0
like soIn the above example when multiple My_Group’s are returned, post-auth will only accept/trigger off the 1st result. If I set (
&control:My_Group =~ /Admin User/
) it works as expected but if I tell it to trigger off (&control:My_Group =~ /Network Admin/
) it will not work..Thank You for sharing your observation in detail.
Hi Michael,
I have found a solutions to the My_Group matching against just the 1st result. The code I wrote below will loop through the groups till it finds a match with the regular expression. Then sets a flag if a match is found. Followed by breaking out the loop. If not found then the flag is set to 0. Then runs through a typical If.. else.. statement.