Setup FreeRADIUS: LDAP Authentication and Authorization


Nurten Dogan

Wireshark

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.

Setup FreeRADIUS: LDAP Authentication and Authorization

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"

Setup FreeRADIUS: LDAP Authentication and Authorization

 

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.

Setup FreeRADIUS: LDAP Authentication and Authorization

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.

Setup FreeRADIUS: LDAP Authentication and Authorization

 

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.

Setup FreeRADIUS: LDAP Authentication and Authorization

 

The debug output from FreeRADIUS is below.

Setup FreeRADIUS: LDAP Authentication and Authorization

 

Analyzing the packets between the FreeRADIUS and the LDAP server

Packet Number 1: Before we query the ldap server, we need to bind (authenticate).

Setup FreeRADIUS: LDAP Authentication and Authorization

 

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.

Setup FreeRADIUS: LDAP Authentication and Authorization

 

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.

Setup FreeRADIUS: LDAP Authentication and Authorization

 

Packet Number 4: The ldap server sends the user information to the radius server in this packet.

Setup FreeRADIUS: LDAP Authentication and Authorization

 

Packet Number 5: After gathering the user’s information, we bind (authenticate) with the user (jane) in this packet.

Setup FreeRADIUS: LDAP Authentication and Authorization

 

Packet Number 6: The provided credentials are verified with this packet.

Setup FreeRADIUS: LDAP Authentication and Authorization

 

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.

 

Views: 855

Nurten Dogan

He is proficient in System Administration, Python, Computer Network, Network Engineering, PHP, Web Testing, Penetration Testing, Wireshark, RADIUS, Cisco Router, TCP/IP, Kali Linux, OSPF, NPS, and Multiprotocol BGP. You can connect with him on LinkedIn .

Can't find what you're searching for? Let us assist you.

Enter your query below, and we'll provide instant results tailored to your needs.

If my articles on GoLinuxCloud has helped you, kindly consider buying me a coffee as a token of appreciation.

Buy GoLinuxCloud a Coffee

For any other feedbacks or questions you can send mail to admin@golinuxcloud.com

Thank You for your support!!

14 thoughts on “Setup FreeRADIUS: LDAP Authentication and Authorization”

  1. Hello who can help me? i have a problem

    including configuration file /etc/raddb/policy.d/eap
    including configuration file /etc/raddb/policy.d/filter
    including configuration file /etc/raddb/policy.d/operator-name
    including configuration file /etc/raddb/policy.d/rfc7542
    including files in directory /etc/raddb/sites-enabled/
    including configuration file /etc/raddb/sites-enabled/my_server
    /etc/raddb/sites-enabled/my_server[22]: Invalid location for 'if'
    [root@ipa-server sites-enabled]# service radiusd start
    Redirecting to /bin/systemctl start radiusd.service
    Job for radiusd.service failed because the control process exited with error code.
    See "systemctl status radiusd.service" and "journalctl -xe" for details.
    Reply
    • Did you ever get a solution to this? I have the same problem with my configuration files that lead to the same error.

      Reply
  2. 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:

    (0) # Executing group from file /etc/freeradius/3.0/sites-enabled/my_svc
    (0)   Auth-Type LDAP {
    rlm_ldap (ldap): Reserved connection (1)
    (0) ldap: Login attempt by "jdoe"
    (0) ldap: Using user DN from request "CN=john doe,OU=MySite,OU=users,OU=My-City,DC=MyDomain,DC=com"
    (0) ldap: Waiting for bind result...
    (0) ldap: Bind successful
    (0) ldap: Bind as user "CN=john doe,OU=MySite,OU=users,OU=My-City,DC=MyDomain,DC=com" was successful
    rlm_ldap (ldap): Released connection (1)
    (0)     [ldap] = ok
    (0)   } # Auth-Type LDAP = ok
    (0) # Executing section post-auth from file /etc/freeradius/3.0/sites-enabled/my_svc
    (0)   post-auth {
    (0)     if (&control:AD-Group =~ /CoreSwitchWrite/) {
    (0)     ERROR: Failed retrieving values required to evaluate condition
    (0)     if (&control:AD-Group =~ /CoreSwitchRead/) {
    (0)     ERROR: Failed retrieving values required to evaluate condition
    (0)   } # post-auth = noop
    (0) Sent Access-Accept Id 230 from 192.168.8.42:1812 to 192.168.9.16:34024 length 20
    (0) Finished request
    Waking up in 4.9 seconds.
    (0) Cleaning up request packet ID 230 with timestamp +20 due to cleanup_delay was reached
    Ready to process requests
    Reply
  3. 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

    Reply
  4. Please help me,

    Bind success but notfound, i use previlage admin

    (1) Received Access-Request Id 10 from 127.0.0.1:53320 to 127.0.0.1:1812 length 86
    (1)   User-Name = "tester"
    (1)   User-Password = "xxxxxx"
    (1)   NAS-IP-Address = 127.0.1.1
    (1)   NAS-Port = 1812
    (1)   Message-Authenticator = 0xf5963b32e2e0d8562c0d36aba1ccb8d3
    (1) # Executing section authorize from file /etc/freeradius/sites-enabled/my_server
    (1)   authorize {
    rlm_ldap (ldap): Reserved connection (0)
    (1) ldap: EXPAND (sAMAccountName=%{%{Stripped-User-Name}:-%{User-Name}})
    (1) ldap:    --> (sAMAccountName=tester)
    (1) ldap: Performing search in "CN=LxxWifiUsers,OU=Lxx_Groups,DC=lws,DC=co,DC=id" with filter "(sAMAccountName=tester)", scope "sub"
    (1) ldap: Waiting for search result...
    (1) ldap: Search returned no results
    rlm_ldap (ldap): Released connection (0)
    Need 1 more connections to reach min connections (3)
    Need more connections to reach 10 spares
    rlm_ldap (ldap): Opening additional connection (6), 1 of 30 pending slots used
    rlm_ldap (ldap): Connecting to ldap://10.13.0.8:389
    rlm_ldap (ldap): Waiting for bind result...
    rlm_ldap (ldap): Bind successful
    rlm_ldap (ldap): You probably need to lower "min"
    rlm_ldap (ldap): Closing expired connection (5) - Hit idle_timeout limit
    (1)     [ldap] = notfound
    (1)     if (ok || updated)  {
    (1)     if (ok || updated)   -> FALSE
    (1)   } # authorize = notfound
    (1) ERROR: No Auth-Type found: rejecting the user via Post-Auth-Type = Reject
    (1) Failed to authenticate the user
    (1) Using Post-Auth-Type Reject
    (1) Post-Auth-Type sub-section not found.  Ignoring.
    (1) Delaying response for 1.000000 seconds
    Waking up in 0.3 seconds.
    Waking up in 0.6 seconds.
    (1) Sending delayed response
    (1) Sent Access-Reject Id 10 from 127.0.0.1:1812 to 127.0.0.1:53320 length 20
    Waking up in 3.9 seconds.
    (1) Cleaning up request packet ID 10 with timestamp +196 due to cleanup_delay was reached
    Ready to process requests
    (2) Received Access-Request Id 92 from 127.0.0.1:57000 to 127.0.0.1:1812 length 83
    (2)   User-Name = "administrator"
    (2)   User-Password = "***********"
    (2)   NAS-IP-Address = 127.0.1.1
    (2)   NAS-Port = 1812
    (2)   Message-Authenticator = 0xac736d72f0a0eaeff8d4d15ab67fdf74
    (2) # Executing section authorize from file /etc/freeradius/sites-enabled/my_server
    (2)   authorize {
    rlm_ldap (ldap): Reserved connection (0)
    (2) ldap: EXPAND (sAMAccountName=%{%{Stripped-User-Name}:-%{User-Name}})
    (2) ldap:    --> (sAMAccountName=administrator)
    (2) ldap: Performing search in "CN=LxxWifiUsers,OU=Lxx_Groups,DC=lws,DC=co,DC=id" with filter "(sAMAccountName=administrator)", scope "sub"
    (2) ldap: Waiting for search result...
    (2) ldap: Search returned no results
    rlm_ldap (ldap): Released connection (0)
    Need 1 more connections to reach min connections (3)
    Need more connections to reach 10 spares
    rlm_ldap (ldap): Opening additional connection (7), 1 of 30 pending slots used
    rlm_ldap (ldap): Connecting to ldap://10.13.0.8:389
    rlm_ldap (ldap): Waiting for bind result...
    rlm_ldap (ldap): Bind successful
    (2)     [ldap] = notfound
    (2)     if (ok || updated)  {
    (2)     if (ok || updated)   -> FALSE
    (2)   } # authorize = notfound
    (2) ERROR: No Auth-Type found: rejecting the user via Post-Auth-Type = Reject
    (2) Failed to authenticate the user
    (2) Using Post-Auth-Type Reject
    (2) Post-Auth-Type sub-section not found.  Ignoring.
    (2) Delaying response for 1.000000 seconds
    Waking up in 0.3 seconds.
    Waking up in 0.6 seconds.
    (2) Sending delayed response
    (2) Sent Access-Reject Id 92 from 127.0.0.1:1812 to 127.0.0.1:57000 length 20
    Waking up in 3.9 seconds.
    (2) Cleaning up request packet ID 92 with timestamp +223 due to cleanup_delay was reached
    Ready to process requests
    Reply
    • 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

      Reply
  5. 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.

    Reply
  6. 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.

    Reply
    • To address #1 I sugest Changing

      control:My_Group                ='memberOf'
      
      to
      
      control:My_Group                +='memberOf'

      Not to familiar with post-auth so #2 would still a concern for me.

      Reply
      • 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.

        Reply
        • I went ahead I tested ldap module in my lab with both ~=, +=, and checked debug output with radiusd -X

          ~= only returns the 1st memberOf result

             (1) ldap: control:My_Group += 'CN=Admin User,OU=My Groups,DC=domain,DC=local'

          += returns all memberOf results

             (1) ldap: control:My_Group += 'CN=Admin User,OU=My Groups,DC=domain,DC=local'
             (1) ldap: control:My_Group += 'CN=Network Admin,OU=My Groups,DC=domain,DC=local'
             (1) ldap: control:My_Group += 'CN=Network Operator,OU=My Groups,DC=domain,DC=local'
             (1) ldap: control:My_Group += 'CN=dev,OU=My Groups,DC=domain,DC=local'

          I confirmed that when neither group matches it logs me into my dell switch so I added a != to the end of the controls to enforce priv-lvl=0 like so

          POST-AUTH:
             post-auth {
             if (&control:My_Group =~ /Network Admin/ ) {
                     update reply {
                             Cisco-AVPair = "shell:priv-lvl=15"
                 }
             }
             if (&control:My_Group !~ /deny_the_rest/) {
                     update reply {
                             Cisco-AVPair = "shell:priv-lvl=0"
                 }
             }

          In 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..

          RADIUSD -X Example:
             ...
             (1) ldap: User object found at DN "CN=Michael Zietlow,OU=TestLab,OU=TestLab Users,DC=domain,DC=local"
             (1) ldap: Processing user attributes
             (1) ldap: control:My_Group += 'CN=Admin User,OU=My Groups,DC=domain,DC=local'
             (1) ldap: control:My_Group += 'CN=Network Admin,OU=My Groups,DC=domain,DC=local'
             (1) ldap: control:My_Group += 'CN=Network Operator,OU=My Groups,DC=domain,DC=local'
             (1) ldap: control:My_Group += 'CN=dev,OU=My Groups,DC=domain,DC=local'
             ...
             (1) # Executing section post-auth from file /etc/raddb/sites-enabled/inner-tunnel
             (1)   post-auth {
             (1)     if (&control:My_Group =~ /Network Admin/ ) {
             (1)     if (&control:My_Group =~ /Network Admin/ )  -> FALSE
             (1)     if (&control:My_Group !~ /deny_the_rest/) {
             (1)     if (&control:My_Group !~ /deny_the_rest/)  -> TRUE
             (1)     if (&control:My_Group !~ /deny_the_rest/)  {
             (1)       update reply {
             (1)         Cisco-AVPair = "shell:priv-lvl=0"
             (1)       } # update reply = noop
             (1)     } # if (&control:My_Group !~ /deny_the_rest/)  = noop
             (1)   } # post-auth = noop
             (1) Sent Access-Accept Id 56 from 192.168.1.2:1812 to 192.168.1.3:53175 length 0
             (1)   Cisco-AVPair = "shell:priv-lvl=0"
             (1) Finished request
          Reply
          • 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.

            "POST-AUTH:
                 foreach &control:My_Group {
                      if ("%{Foreach-Variable-0}" =~ /Network Admin/) {
                           update control { Tmp-String-0 = 1 }
                           break
                      }
                 }
                 update control { Tmp-String-0 = "%{%{control:Tmp-String-0}:-0}" }
            
                 if (control:Tmp-String-0 == "1") {
                      update reply { Cisco-AVPair = "shell:priv-lvl=15" }
                      ok
                 }else{
                      update reply { Reply-Message = "Not authorized" }
                      reject
                 }"
            
            Reply

Leave a Comment

GoLinuxCloud Logo


We try to offer easy-to-follow guides and tips on various topics such as Linux, Cloud Computing, Programming Languages, Ethical Hacking and much more.

Programming Languages

JavaScript

Python

Golang

Node.js

Java

Laravel