LDAP group authorization with MSAD
I have Freeradius 3.0.20 from Ubuntu 20.04 working with bind as user to my Active Directory servers. https://freeradius.org/documentation/freeradius-server/3.2.4/concepts/module... I’m hoping to use this for network equipment to have a centralized login system. I’m having trouble figuring out how, with only the username like user@example.com used for the authentication, how do I look up their group membership for authorization. In the below configuration files I have sanitized my company name. Here is my file /etc/freeradius/3.0/sites-enabled/netauth: # THIS FILE IS MANAGED BY ANSIBLE server netauth { listen { ipaddr = * port = 1814 type = auth } authorize { auth_log update control { LDAP-UserDN := "%{User-Name}@example.com" Auth-Type := LDAP } if (LDAP-Group == "techops") { update reply { Juniper-Local-User-Name := "techops" } } elsif (LDAP-Group == "obsmonitor") { update reply { Juniper-Local-User-Name := "monitor" } } else { reject } } authenticate { Auth-Type LDAP { ldap } } client 10-0-0-0-8 { ipaddr = 10.0.0.0/8 secret = xxxxxx } client 172-16-0-0-12 { ipaddr = 172.16.0.0/12 secret = xxxxxx } } Here is my file /etc/freeradius/3.0/mods-enabled/ldap # THIS FILE IS MANAGED BY ANSIBLE ldap { # use the HAProxy on this same host server = 'ldap://localhost' identity = 'CN=Ldap Readonly,OU=svc_accounts,OU=Service,OU=Example Users,DC=ad,DC=example,DC=com' password = 'yyyyyyyy' group { base_dn = 'OU=LDAP PROXY,OU=Global Groups,OU=Users,OU=Example Users,DC=ad,DC=example,DC=com' filter = '(objectClass=group)' membership_filter = '' } } Here is the debug output during a successful login. The testobs user is in the "obsmonitor" group and NOT the "techops" group, but they are matching "techops" anyway I think because of membership_filter = "" ? I think membership_filter = "" allows any user to match the group, is that right? Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) Received Access-Request Id 248 from 172.30.255.19:57489 to 10.30.0.229:1814 length 86 Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) User-Name = "testobs" Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) User-Password = "Example123!" Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) NAS-Identifier = "sw102-2.sql1.corp" Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) Calling-Station-Id = "172.16.240.5" Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) NAS-IP-Address = 172.30.255.19 Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) # Executing section authorize from file /etc/freeradius/3.0/sites-enabled/netauth Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) authorize { Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) auth_log: EXPAND /var/log/freeradius/radacct/%{%{Packet-Src-IP-Address}:-%{Packet-Src-IPv6-Address}}/auth-detail-%Y%m%d Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) auth_log: --> /var/log/freeradius/radacct/172.30.255.19/auth-detail-20240123 Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) auth_log: /var/log/freeradius/radacct/%{%{Packet-Src-IP-Address}:-%{Packet-Src-IPv6-Address}}/auth-detail-%Y%m%d expands to /var/log/freeradius/radacct/172.30.255.19/auth-detail-20240123 Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) auth_log: EXPAND %t Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) auth_log: --> Tue Jan 23 04:49:52 2024 Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) [auth_log] = ok Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) update control { Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) EXPAND %{User-Name}@example.com Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) --> testobs@example.com Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) LDAP-UserDN := testobs@example.com Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) Auth-Type := LDAP Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) } # update control = noop Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) if (LDAP-Group == "techops") { Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) Searching for user in group "techops" Jan 23 04:49:52 ldaps3 freeradius[2798965]: rlm_ldap (ldap): Closing connection (0): Hit idle_timeout, was idle for 154 seconds Jan 23 04:49:52 ldaps3 freeradius[2798965]: rlm_ldap (ldap): You probably need to lower "min" Jan 23 04:49:52 ldaps3 freeradius[2798965]: rlm_ldap (ldap): Closing connection (1): Hit idle_timeout, was idle for 154 seconds Jan 23 04:49:52 ldaps3 freeradius[2798965]: rlm_ldap (ldap): You probably need to lower "min" Jan 23 04:49:52 ldaps3 freeradius[2798965]: rlm_ldap (ldap): Closing connection (2): Hit idle_timeout, was idle for 154 seconds Jan 23 04:49:52 ldaps3 freeradius[2798965]: rlm_ldap (ldap): You probably need to lower "min" Jan 23 04:49:52 ldaps3 freeradius[2798965]: rlm_ldap (ldap): Closing connection (3): Hit idle_timeout, was idle for 154 seconds Jan 23 04:49:52 ldaps3 freeradius[2798965]: rlm_ldap (ldap): You probably need to lower "min" Jan 23 04:49:52 ldaps3 freeradius[2798965]: rlm_ldap (ldap): Closing connection (4): Hit idle_timeout, was idle for 154 seconds Jan 23 04:49:52 ldaps3 freeradius[2798965]: rlm_ldap (ldap): You probably need to lower "min" Jan 23 04:49:52 ldaps3 freeradius[2798965]: rlm_ldap (ldap): 0 of 0 connections in use. You may need to increase "spare" Jan 23 04:49:52 ldaps3 freeradius[2798965]: rlm_ldap (ldap): Opening additional connection (5), 1 of 10 pending slots used Jan 23 04:49:52 ldaps3 freeradius[2798965]: rlm_ldap (ldap): Connecting to ldap://localhost:389 Jan 23 04:49:52 ldaps3 freeradius[2798965]: rlm_ldap (ldap): Waiting for bind result... Jan 23 04:49:52 ldaps3 freeradius[2798965]: rlm_ldap (ldap): Bind successful Jan 23 04:49:52 ldaps3 freeradius[2798965]: rlm_ldap (ldap): Reserved connection (5) Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) Using user DN from request "testobs@example.com" Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) Checking for user in group objects Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) EXPAND (&(cn=techops)(objectClass=group)) Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) --> (&(cn=techops)(objectClass=group)) Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) Performing search in "OU=LDAP PROXY,OU=Global Groups,OU=Users,OU=Example Users,DC=ad,DC=example,DC=com" with filter "(&(cn=techops)(objectClass=group))", scope "sub" Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) Waiting for search result... Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) User found in group object "OU=LDAP PROXY,OU=Global Groups,OU=Users,OU=Example Users,DC=ad,DC=example,DC=com" Jan 23 04:49:52 ldaps3 freeradius[2798965]: rlm_ldap (ldap): Released connection (5) Jan 23 04:49:52 ldaps3 freeradius[2798965]: Need 4 more connections to reach min connections (5) Jan 23 04:49:52 ldaps3 freeradius[2798965]: rlm_ldap (ldap): Opening additional connection (6), 1 of 9 pending slots used Jan 23 04:49:52 ldaps3 freeradius[2798965]: rlm_ldap (ldap): Connecting to ldap://localhost:389 Jan 23 04:49:52 ldaps3 freeradius[2798965]: rlm_ldap (ldap): Waiting for bind result... Jan 23 04:49:52 ldaps3 freeradius[2798965]: rlm_ldap (ldap): Bind successful Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) if (LDAP-Group == "techops") -> TRUE Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) if (LDAP-Group == "techops") { Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) update reply { Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) Juniper-Local-User-Name := "techops" Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) } # update reply = noop Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) } # if (LDAP-Group == "techops") = noop Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) ... skipping elsif: Preceding "if" was taken Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) ... skipping else: Preceding "if" was taken Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) } # authorize = ok Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) Found Auth-Type = LDAP Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) # Executing group from file /etc/freeradius/3.0/sites-enabled/netauth Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) Auth-Type LDAP { Jan 23 04:49:52 ldaps3 freeradius[2798965]: rlm_ldap (ldap): Reserved connection (5) Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) ldap: Login attempt by "testobs" Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) ldap: Using user DN from request "testobs@example.com" Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) ldap: Waiting for bind result... Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) ldap: Bind successful Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) ldap: Bind as user "testobs@example.com" was successful Jan 23 04:49:52 ldaps3 freeradius[2798965]: rlm_ldap (ldap): Released connection (5) Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) [ldap] = ok Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) } # Auth-Type LDAP = ok Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) Sent Access-Accept Id 248 from 10.30.0.229:1814 to 172.30.255.19:57489 length 0 Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) Juniper-Local-User-Name := "techops" Jan 23 04:49:52 ldaps3 freeradius[2798965]: (0) Finished request Jan 23 04:49:52 ldaps3 freeradius[2798965]: Waking up in 4.9 seconds. Jan 23 04:49:57 ldaps3 freeradius[2798965]: (0) Cleaning up request packet ID 248 with timestamp +154 Jan 23 04:49:57 ldaps3 freeradius[2798965]: Ready to process requests I read https://freeradius.org/documentation/freeradius-server/4.0.0/howto/modules/l... explaining the different ways looking up groups with some different LDAP schemas. In my user definitions I have memberOf attributes like with the testobs user: memberOf: CN=obsmonitor,OU=LDAP PROXY,OU=Global Groups,OU=Users,OU=Example Users,DC=ad,DC=example,DC=com I have member attributes in n the group definitions like with the obsmonitor group for the testobs user: member: CN=Observium Test,OU=OKTA Test Accounts,OU=Service,OU=Example Users,DC=ad,DC=example,DC=com I can use ldapsearch and binding as our read only LDAP user to query the group with a membership filter: nicks@eel:~$ ldapsearch -x -H ldaps://sjc-1.ldap.example.com -D "CN=Ldap Readonly,OU=svc_accounts,OU=Service,OU=Example Users,DC=ad,DC=example,DC=com" -W -b 'OU=LDAP PROXY,OU=Global Groups,OU=Users,OU=Example Users,DC=ad,DC=example,DC=com' '(&(cn=obsmonitor)(objectClass=group)(member=CN=Observium Test,OU=OKTA Test Accounts,OU=Service,OU=Example Users,DC=ad,DC=example,DC=com))' Enter LDAP Password: # extended LDIF # # LDAPv3 # base <OU=LDAP PROXY,OU=Global Groups,OU=Users,OU=Example Users,DC=ad,DC=example,DC=com> with scope subtree # filter: (&(cn=obsmonitor)(objectClass=group)(member=CN=Observium Test,OU=OKTA Test Accounts,OU=Service,OU=Example Users,DC=ad,DC=example,DC=com)) # requesting: ALL # # obsmonitor, LDAP PROXY, Global Groups, Users, Example Users, ad.example.com dn: CN=obsmonitor,OU=LDAP PROXY,OU=Global Groups,OU=Users,OU=Example Users,DC= ad,DC=example,DC=com objectClass: top objectClass: group cn: obsmonitor member: CN=Observium Test,OU=OKTA Test Accounts,OU=Service,OU=Example Users,DC =ad,DC=example,DC=com distinguishedName: CN=obsmonitor,OU=LDAP PROXY,OU=Global Groups,OU=Users,OU=Co nviva Users,DC=ad,DC=example,DC=com instanceType: 4 whenCreated: 20231125033343.0Z whenChanged: 20231125041833.0Z uSNCreated: 14442815 uSNChanged: 14442906 name: obsmonitor objectGUID:: /joNsBrPiUqDzN52+a7D0Q== objectSid:: AQUAAAAAAAUVAAAAaaNyTpKL8mVbop3/gVgAAA== sAMAccountName: obsmonitor sAMAccountType: 268435456 groupType: -2147483646 objectCategory: CN=Group,CN=Schema,CN=Configuration,DC=ad,DC=example,DC=com dSCorePropagationData: 16010101000000.0Z # search result search: 2 result: 0 Success # numResponses: 2 # numEntries: 1 nicks@eel:~$ With binding as the original user though, it only seems to bind and doesn't seem to query the user object, so how can I get the user's DN to check group membership in this way and with my MSAD LDAP schema? Thank you! -Nick Schmalenberger
On 23/01/2024 05:10, Nick Schmalenberger wrote:
With binding as the original user though, it only seems to bind and doesn't seem to query the user object, so how can I get the user's DN to check group membership in this way and with my MSAD LDAP schema? Call the ldap module in authorize (before you start referring to LDAP-Group - if you configure stuff correctly using the cached group options then you can minimise the number of LDAP lookups)
When ldap is called in authorize context it: - binds as the admin user - uses the "user" related module options to find the user's object - creates the attribute &control:LDAP-UserDN containing the object's DN - depending on the "group" related module options looks up the user's group membership and populates the &control:LDAP-Group attribute. -- Nick Porter
On Tue, Jan 23, 2024 at 11:47:06AM +0000, Nick Porter wrote:
>
> On 23/01/2024 05:10, Nick Schmalenberger wrote:
> > With binding as the original user though, it only seems to bind and doesn't seem to query the user object, so how can I get the user's DN to check group membership in this way and with my MSAD LDAP schema?
> Call the ldap module in authorize (before you start referring to LDAP-Group
> - if you configure stuff correctly using the cached group options then you
> can minimise the number of LDAP lookups)
>
> When ldap is called in authorize context it:
>
> - binds as the admin user
> - uses the "user" related module options to find the user's object
> - creates the attribute &control:LDAP-UserDN containing the object's DN
> - depending on the "group" related module options looks up the user's group
> membership and populates the &control:LDAP-Group attribute.
>
> --
>
> Nick Porter
>
Thank you! You put me on the right track, and with doing %{User-Name}@example.com in the user filter, I didn't need to override LDAP-UserDN at all :)
I can bind as user with the actual DN and use it in the group check too. Either of these work:
membership_filter = "(member=%{control:LDAP-UserDn})"
membership_attribute = 'memberOf'
Is it better to use memberOf because freeradius already has the user object and doesn't need to make another query? In packet capturing it looks like it requests at least the matching group object too.
Are these just different in performance, or useful in some different situations? Is one more readable or idiomatic in freeradius? Anything else I should consider?
-Nick
On 23/01/2024 15:33, Nick Schmalenberger wrote:
Thank you! You put me on the right track, and with doing %{User-Name}@example.com in the user filter, I didn't need to override LDAP-UserDN at all :) Great I can bind as user with the actual DN and use it in the group check too. Either of these work: membership_filter = "(member=%{control:LDAP-UserDn})" membership_attribute = 'memberOf'
Is it better to use memberOf because freeradius already has the user object and doesn't need to make another query? In packet capturing it looks like it requests at least the matching group object too.
Are these just different in performance, or useful in some different situations? Is one more readable or idiomatic in freeradius? Anything else I should consider?
The challenge with LDAP is that each directory is configured differently. E.g. some directories have a memberOf attribute and others don't - meaning different approaches have to be taken to resolving group membership. Typically the aim should be to minimise the number of queries being run (which you can see from running FreeRADIUS in debug mode). However, you also need to consider what indexes are set up to ensure that the queries are efficient. Given a memberOf attribute which is part of the user object, I would usually choose that option, and then make sure that the "cache" option matches the format that the data in that attribute is presented. This is because in that case, the group membership is returned in the same LDAP query as finding the user object. I.e. if the memberOf attribute returns DNs, then use cacheable_dn = 'yes', and do all your work using those DN formats for group names. If memberOf returns DNs and you set cacheable_name = 'yes' then further queries will be run to map the group DNs to their names. In addition, if you are using LDAP binds to authenticate, if you have not already called the ldap module in authorize, then the first thing that FreeRADIUS will do is lookup the user's DN. This typically means the most efficient thing to do is to call ldap in authorize to retrieve the user DN and group membership, before calling it again in authenticate to bind as the user. It's all about understanding how data is returned from the directory, and adapting your policy to match that (if possible). As an aside, with AD, it can also be beneficial to use the Global Catalog rather than the standard LDAP port - if all the data you require is available in the Global Catalog. Using the standard LDAP port can result in redirects, depending on the base DN that searches are performed in. -- Nick Porter
participants (2)
-
Nick Porter -
Nick Schmalenberger