`userPassword` field.
Again, the best method is to test authentication is with the
-xref:howto:modules/ldap/ldapsearch.adoc[ldapsearch] tool.
+xref:howto:modules/ldap/ldapsearch/index.adoc[ldapsearch] tool.
These tests *must* be run prior to configuring FreeRADIUS. We strongly
recommend having the LDAP database return the `userPassword` field to
FreeRADIUS, so that FreeRADIUS can authenticate the user.
*** xref:modules/eap/index.adoc[EAP]
*** xref:modules/expiration/index.adoc[Expiration]
*** xref:modules/krb5/index.adoc[Krb5]
+
*** xref:modules/ldap/index.adoc[LDAP]
-**** xref:modules/ldap/bootstrap_openldap.adoc[Installing and Configuring OpenLDAP]
-**** xref:modules/ldap/ldapsearch.adoc[Mapping and testing with ldapsearch]
+
+**** xref:modules/ldap/bootstrap_openldap/index.adoc[Installing and Configuring OpenLDAP]
+***** xref:modules/ldap/bootstrap_openldap/docker.adoc[Docker]
+***** xref:modules/ldap/bootstrap_openldap/packages.adoc[Packages]
+
+**** xref:modules/ldap/ldapsearch/index.adoc[Mapping and testing with ldapsearch]
+***** xref:modules/ldap/ldapsearch/before_starting.adoc[Before starting]
+***** xref:modules/ldap/ldapsearch/connection_parameters.adoc[Determining connection parameters]
+***** xref:modules/ldap/ldapsearch/locating_objects.adoc[Locating objects]
+***** xref:modules/ldap/ldapsearch/translating_to_the_ldap_module.adoc[Translating ldapsearch arguments to LDAP configuration items]
**** xref:modules/ldap/configuration.adoc[Enabling the LDAP module]
**** xref:modules/ldap/authorization.adoc[Configuring authorization]
**** xref:modules/ldap/authentication.adoc[Configuring authentication]
**** xref:modules/ldap/accounting.adoc[Configuring accounting]
+
*** xref:modules/mschap/index.adoc[MS-CHAP]
*** xref:modules/pam/index.adoc[PAM]
*** xref:modules/passwd/index.adoc[Passwd]
= Configuring Authorization
-=== Group Membership
+
+The `authorize` method of the LDAP module is responsible for locating the
+authenticating user's LDAP object.
+
+In addition to determining where the user is, the authorize method also
+performs LDAP to FreeRADIUS attribute mappings,
+
+== 1. Locating the user
+
+No matter how the LDAP module is called (via its `authorize`, `authenticate`,
+`accounting` methods or an `LDAP-Group` comparison) the first operation the
+module performs it to populate `&control:LDAP-UserDN` with the location of
+the authenticating user's object in LDAP.
+
+How the LDAP-UserDN value is discovered is determined by the contents of the
+`user { ... }` section of the LDAP module instance.
+
+Below is an example of configuring a user section for OpenLDAP, with callouts
+explaining how the different configuration items are used. See
+xref:modules/ldap/ldapsearch/index.adoc[ldapsearch] for how to determine the
+appropriate values for your directory.
+
+[source,unlang]
+----
+user {
+ base_dn = "ou=people,${..base_dn}" <1>
+ filter =
+}
+----
+
+== 2. Disambiguating users
+
+dn: olcOverlay=sssvlv,olcDatabase={1}mdb,cn=config
+> objectClass: olcSssVlvConfig
+> olcOverlay: sssvlv
+> olcSssVlvMax: 10
+> olcSssVlvMaxKeys: 5
+
+== 2. Configuring the enable/disable attribute
+
+== Group Membership
Depending on the LDAP vendor you use, LDAP group membership is either handled
using the LDAP attributes:
--- /dev/null
+= Using OpenLDAP in a docker container
+
+== Bootstrap
+
+https://github.com/osixia[Osixia!] provides a
+https://github.com/osixia/docker-openldap[fully functionally OpenLDAP container]
+which can be instantiated using the docker invocation below.
+
+This docker invocation also sets up a readonly user, and loads the custom
+FreeRADIUS schemas required for RADIUS to LDAP attribute mapping, dynamic client
+definitions, and attribute profiles.
+
+=== Define site specific variables
+Change the values here to match local paths and your site specific
+configuration.
+
+[source,shell]
+----
+# Where to store a temporary shallow clone of the FreeRADIUS source
+# Or the path to an existing copy of the FreeRADIUS source
+FREERADIUS_SRC="/tmp/freeradius-src"
+
+# Base DN
+LDAP_BASE_DN="dc=example,dc=com"
+
+# Domain
+LDAP_DOMAIN="example.com"
+
+# DN Suffix
+LDAP_BASE_DN="dc=example,dc=com"
+
+# Password for the administrative user
+LDAP_ADMIN_PASSWORD="secret"
+
+# Password for the read only user
+LDAP_READONLY_PASSWORD="readonly"
+----
+
+=== Instantiate an OpenLDAP docker container
+
+[source,shell]
+----
+if [ ! -d "${FREERADIUS_SRC}" ]; then
+ git clone --depth 1 https://github.com/FreeRADIUS/freeradius-server.git "${FREERADIUS_SRC}"
+fi
+docker run -it --rm -p 389:389 \
+ --env LDAP_DOMAIN=${LDAP_DOMAIN} \
+ --env LDAP_ADMIN_PASSWORD=${LDAP_ADMIN_PASSWORD} \
+ --env LDAP_READONLY_USER=true \
+ --env LDAP_READONLY_USER_PASSWORD=${LDAP_READONLY_PASSWORD} \
+ --volume "${FREERADIUS_SRC}/doc/schemas/ldap/openldap/freeradius.schema:/container/service/slapd/assets/config/bootstrap/schema/mmc/radius.schema:ro" \
+ --volume "${FREERADIUS_SRC}/doc/schemas/ldap/openldap/freeradius-clients.schema:/container/service/slapd/assets/config/bootstrap/schema/mmc/freeradius-clients.schema:ro" \
+ osixia/openldap:1.2.5 --copy-service
+----
+
+== Populate with test data
+Once docker is running, there should now be an LDAP server running on
+`localhost`, it can now be populated with test data.
+
+For test data we will be using the object definitions from the LDAP
+module's CIT (Continuous Integration Testing) script.
+
+These object definitions have been designed to exercise all features
+of the FreeRADIUS LDAP module.
+
+[source,shell]
+----
+sed -e '1,/^description:/ d' "${FREERADIUS_SRC}/src/tests/modules/ldap/example.com.ldif" \
+ | ldapadd -H ldap://localhost -x -D cn=admin,${LDAP_BASE_DN} -w admin
+----
+
+There should be no errors, and the console with the running LDAP
+server should show requests being handled.
+
+If the LDAP server server is stopped, as with `CTRL-C`, then the database
+contents will be lost. All of the steps above will have to be re-done the next
+time the server is started.
+
+In order to make the LDAP database persistent, see the
+https://github.com/osixia/docker-openldap[osixia/openldap
+instructions].
+
+== Check it works
+
+As a final step you should verify that test data has been loaded correctly.
+This can be done using the `ldapsearch` utility using the LDAP read only user.
+
+The command below will retrieve the entry for one of the test `radiusClient`
+entries. You should see a single search result returned if everything worked
+correctly.
+
+[source,shell]
+----
+ldapsearch -LLL -H ldap://localhost -x -D cn=readonly,<base_dn> -w <readonly_password> -b <base_dn> '(&(objectClass=radiusClient)(radiusClientShortname=client2))'
+----
+
+.Searching for a RADIUS Client
+====
+[source,shell]
+----
+ldapsearch -LLL -H ldap://localhost -x -D cn=readonly,dc=example,dc=com -w readonly -b dc=example,dc=com '(&(objectClass=radiusClient)(radiusClientShortname=client2))'
+----
+
+.Expected output
+[source,ldiff]
+----
+# extended LDIF
+#
+# LDAPv3
+# base <dc=example,dc=com> with scope subtree
+# filter: (&(objectClass=radiusClient)(radiusClientShortname=client2))
+# requesting: ALL
+#
+
+# 2.2.2.2, clients, example.com
+dn: radiusClientIdentifier=2.2.2.2,ou=clients,dc=example,dc=com
+objectClass: radiusClient
+radiusClientIdentifier: 2.2.2.2
+radiusClientSecret: 123secret
+radiusClientShortname: client2
+radiusClientType: cisco
+radiusClientRequireMa: TRUE
+radiusClientComment: Another test client
+
+# search result
+search: 2
+result: 0 Success
+
+# numResponses: 2
+# numEntries: 1
+----
+====
--- /dev/null
+= Installing and Configuring OpenLDAP
+
+This section of the howto describes two methods of creating an OpenLDAP instance for
+testing purposes.
+
+== xref:modules/ldap/bootstrap_openldap/docker.adoc[Docker]
+
+If you're looking to create a temporary instance of OpenLDAP and spin it up as
+quickly as possible, you should consider using the
+https://docs.docker.com/install/[Docker containerisation service].
+
+A pre-configured docker container allow you to create a self-contained OpenLDAP
+instance with a minimum amount of effort.
+
+== xref:modules/ldap/bootstrap_openldap/packages.adoc[Packages]
+
+If you're looking to create a more permanent installation of OpenLDAP or
+are not comfortable using docker, then you may wish to install OpenLDAP from packages.
+
+OpenLDAP is available pre-packaged for many distributions. We recommend
+using the OpenLDAP LTB packages available under the
+"Packaging and OpenLDAP extensions" heading https://ltb-project.org/documentation[here].
-= Installing and Configuring OpenLDAP
+= Installing OpenLDAP from packages
-This page describes two methods (using <<bootstrap_with_docker,Docker>> and via
-<<bootstrap_with_packages,Packages>>) of creating an OpenLDAP instance for
-testing purposes.
-
-If you're looking to create a temporary instance of OpenLDAP and spin it up as
-quickly as possible, you should consider using the
-https://docs.docker.com/install/[Docker containerisation service].
-
-A pre-configured docker container allow you to create a self-contained OpenLDAP
-instance with a minimum amount of effort.
-
-If you're looking to create a more permanent installation of OpenLDAP or
-are not comfortable using docker, then you may wish to install OpenLDAP from packages.
-
-OpenLDAP is available pre-packaged for many distributions. We recommend
-using the OpenLDAP LTB packages available under the
-"Packaging and OpenLDAP extensions" heading https://ltb-project.org/documentation[here].
-
-[#bootstrap_with_docker]
-== Using OpenLDAP in a docker container
-
-=== Bootstrap
-
-https://github.com/osixia[Osixia!] provides a
-https://github.com/osixia/docker-openldap[fully functionally OpenLDAP container]
-which can be instantiated using the docker invocation below.
-
-This docker invocation also sets up a readonly user, and loads the custom
-FreeRADIUS schemas required for RADIUS to LDAP attribute mapping, dynamic client
-definitions, and attribute profiles.
-
-==== Define site specific variables
-Change the values here to match local paths and your site specific
-configuration.
-
-[source,shell]
-----
-# Where to store a temporary shallow clone of the FreeRADIUS source
-# Or the path to an existing copy of the FreeRADIUS source
-FREERADIUS_SRC="/tmp/freeradius-src"
-
-# Base DN
-LDAP_BASE_DN="dc=example,dc=com"
-
-# Domain
-LDAP_DOMAIN="example.com"
-
-# DN Suffix
-LDAP_BASE_DN="dc=example,dc=com"
-
-# Password for the administrative user
-LDAP_ADMIN_PASSWORD="secret"
-
-# Password for the read only user
-LDAP_READONLY_PASSWORD="readonly"
-----
-
-==== Instantiate an OpenLDAP docker container
-
-[source,shell]
-----
-if [ ! -d "${FREERADIUS_SRC}" ]; then
- git clone --depth 1 https://github.com/FreeRADIUS/freeradius-server.git "${FREERADIUS_SRC}"
-fi
-docker run -it --rm -p 389:389 \
- --env LDAP_DOMAIN=${LDAP_DOMAIN} \
- --env LDAP_ADMIN_PASSWORD=${LDAP_ADMIN_PASSWORD} \
- --env LDAP_READONLY_USER=true \
- --env LDAP_READONLY_USER_PASSWORD=${LDAP_READONLY_PASSWORD} \
- --volume "${FREERADIUS_SRC}/doc/schemas/ldap/openldap/freeradius.schema:/container/service/slapd/assets/config/bootstrap/schema/mmc/radius.schema:ro" \
- --volume "${FREERADIUS_SRC}/doc/schemas/ldap/openldap/freeradius-clients.schema:/container/service/slapd/assets/config/bootstrap/schema/mmc/freeradius-clients.schema:ro" \
- osixia/openldap:1.2.5 --copy-service
-----
-
-=== Test data
-Once docker is running, there should now be an LDAP server running on
-`localhost`, it can now be populated with test data.
-
-For test data we will be using the object definitions from the LDAP
-module's CIT (Continuous Integration Testing) script.
-
-These object definitions have been designed to exercise all features
-of the FreeRADIUS LDAP module.
-
-[source,shell]
-----
-sed -e '1,/^description:/ d' "${FREERADIUS_SRC}/src/tests/modules/ldap/example.com.ldif" \
- | ldapadd -H ldap://localhost -x -D cn=admin,${LDAP_BASE_DN} -w admin
-----
-
-There should be no errors, and the console with the running LDAP
-server should show requests being handled.
-
-If the LDAP server server is stopped, as with `CTRL-C`, then the database
-contents will be lost. All of the steps above will have to be re-done the next
-time the server is started.
-
-In order to make the LDAP database persistent, see the
-https://github.com/osixia/docker-openldap[osixia/openldap
-instructions].
-
-As a final step you should <<verifying_openldap_setup,verify OpenLDAP is
-setup correctly>>.
-
-[#bootstrap_with_packages]
-== Installing OpenLDAP from packages
-=== Bootstrap
-==== Switch to privileged user
+== Switch to privileged user
[source,shell]
----
root user (`UID=0`, `GID=0`). When configuring OpenLDAP via OLC the `ldapadd`
utility will be run as root so our changes are authorised.
-==== Install packages
+== Bootstrap
+
+=== Install packages
OpenLDAP LTB packages available under the
"Packaging and OpenLDAP extensions" heading https://ltb-project.org/documentation[here].
You should add the appropriate repository definitions and install the `openldap`
package (`yum install openldap` or `apt-get install openldap`).
-==== Define site specific variables
+=== Define site specific variables
Change the values here to match local paths and your site specific
configuration.
LDAP_READONLY_PASSWORD="readonly"
----
-==== Switch OpenLDAP from using static configuration files to OLC
+=== Switch OpenLDAP from using static configuration files to OLC
After installing OpenLDAP, `slapd` (the main OpenLDAP daemon) needs to be
switched to using OLC (on-line configuration).
systemctl start slapd
----
-==== Define a database and set appropriate ACLs
+=== Define a database and set appropriate ACLs
[source,shell]
----
EOF
----
-.Interpreting OpenLDAP ACLs
-****
+.Wondering what those ACLs mean?
+[%collapsible]
+====
The OpenLDAP ACL syntax can be difficult to understand for new users. To help
with implementing site-specific ACLs, the humanly readable translation of the
* `to dn.base="" by * read`
** Any user may access the metadata at the top of the directory. The is useful for the
autodiscovery functionality in LDAP browsers.
+====
-****
-
-==== Populate the top level object, and add credentials for the readonly user
+=== Populate the top level object, and add credentials for the readonly user
[source,shell]
----
EOF
----
-=== Loading schemas
+== Load schemas
For our tests we need to load some basic bundled OpenLDAP schemas and some
FreeRADIUS specific schemas for defining profiles, RADIUS to LDAP mappings and
done
----
-=== Test data
+== Populate with test data
Once slapd is running and appropriately configured with database definitions an
admin user, a readonly user, and the prerequisite schemas, it can now be
populated with test data.
| ldapadd -Y EXTERNAL -H ldapi://%2Fvar%2Frun%2Fslapd%2Fldapi
----
-=== Switch to an unprivileged user
+== Switch to an unprivileged user
[source,shell]
----
exit
should note that the `ldapadd`, `ldapmodify` commands must be called as root
with the `-Y EXTERNAL` argument.
-[#verifying_openldap_setup]
-== Verifying OpenLDAP setup
+== Check it works
As a final step you should verify that test data has been loaded correctly.
This can be done using the `ldapsearch` utility using the LDAP read only user.
= Enabling the LDAP module
The configuration step should be little more than
-xref:modules/ldap/ldapsearch.adoc#_translating_ldapsearch_arguments_to_rlm_ldap_configuration_items[copying the parameters used by ldapsearch]
+xref:modules/ldap/ldapsearch/index.adoc#_translating_ldapsearch_arguments_to_rlm_ldap_configuration_items[copying the parameters used by ldapsearch]
to the xref:raddb:mods-available/ldap.adoc[ldap module] configuration file.
The xref:raddb:mods-available/ldap.adoc[ldap module] configuration file
to being correct. All that is necessary is to make minor changes, and
_test_ them. FreeRADIUS should look for data.
-If the xref:modules/ldap/search[ldapsearch] tests above pass,
+If the xref:modules/ldap/ldapsearch/index.adoc[ldapsearch] tests above pass,
then the LDAP module configuration can be copied directly from the
command-line options to that tool using the
-xref:modules/ldap/ldapsearch.adoc#_translating_ldapsearch_arguments_to_rlm_ldap_configuration_items[translation table] on that page.
+xref:modules/ldap/ldapsearch/index.adoc#_translating_ldapsearch_arguments_to_rlm_ldap_configuration_items[translation table] on that page.
At a minimum you will need to set the following configuration items
in in xref:raddb:mods-available/ldap.adoc[mods-available/ldap] using
the values you discovered
-running the xref:modules/ldap/search[ldapsearch] tests.
+running the xref:modules/ldap/ldapsearch/index.adoc[ldapsearch] tests.
[source,config]
----
user {
base_dn = "ou=people,${..base_dn}" <5>
- filter = '(objectClass=posixUser)' <6>
+ filter = '(&(uid=%{%{Stripped-User-Name}:-%{User-Name}})(objectClass=posixUser))' <6>
}
}
----
to reduce network overhead.
<4> The object containing all other objects we're interested in.
<5> The object containing all the users we need to perform AAA functions for.
-<6> Filter to select only valid user objects.
+<6> Filter to select the object matching the authenticating user.
We do _not_ recommend immediately configuring TLS unless you are testing against
a production server. The best approach is to test one piece in isolation,
=== 2. Testing
Once an LDAP server is available, it should be tested via the command-line
-xref:modules/ldap/ldapsearch.adoc[ldapsearch] tool. This is to ensure that
+xref:modules/ldap/ldapsearch/index.adoc[ldapsearch] tool. This is to ensure that
the LDAP server has been configured correctly. If testing via `ldapsearch`
fails, then that *MUST* those issues must be resolved before configuring
FreeRADIUS.
=== 3. Configuring the LDAP module
-Once the xref:modules/ldap/ldapsearch.adoc[ldapsearch] validation tests pass, the
+Once the xref:modules/ldap/ldapsearch/index.adoc[ldapsearch] validation tests pass, the
next step is to xref:modules/ldap/configuration.adoc[configure the LDAP module].
OpenLDAP configuration examples detailing how to install appropriate schemas and
--- /dev/null
+= Before starting
+
+Try to determine answers to the following questions:
+
+* What's the IP address of the LDAP server being tested?
+** If using an FQDN does the FQDN resolve to this IP address?
+* Does the LDAP server requires encrypted connections?
+** If yes:
+*** Does the LDAP server use the StartTLS extension to transition to
+encrypted communication, or is communication encrypted from the start (LDAPS)?
+*** What are the necessary root certificates and intermediary certificates
+needed to validate the identity of the LDAP server?
+* What port is used to connect to the LDAP server? Usually this will be `389`
+(unencrypted or StartTLS) or `636` (LDAPS).
+* Do connections need to be bound to perform searches on the LDAP directory?
+** If yes:
+*** Determine what credentials are needed for binding, and whether a client
+certificate is required.
+* Determine the Base DN, the object deepest in the tree that contains the object
+representing users, groups, clients etc...
--- /dev/null
+= Determining connection parameters
+
+`ldapsearch` accepts a large number of different arguments, and allow relatively
+complex commands to be sent to the LDAP server.
+
+It will likely take a number of attempts to find the correct `ldapsearch` invocation.
+If you have no prior information about the LDAP server follow the examples below,
+adding progressively more connection parameters until `ldapsearch` returns a positive
+result (`$? == 0` and one or more entries written to stdout).
+
+[source,ldif]
+----
+dn: uid=john,ou=people,dc=example,dc=com
+objectClass: inetOrgPerson
+uid: john
+userPassword: password
+----
+
+If the `ldapsearch` program fails to return anything useful, then additional
+arguments should be added until the search succeeds. Unfortunately, every piece
+of advice in this section is site-specific, and is independent of RADIUS.
+
+== Search without bind (anonymous)
+At a minimum, unless you have defaults set in your local `ldap.conf` file,
+you will need to provide the following arguments:
+
+- `-z 1` - Limit the number of results to 1.
+- `-x` - Use simple authentication instead of SASL (correct for the majority of servers).
+- `-H <ldap uri>` - The LDAP server to connect to.
+- `-b <dn>` - A point in the LDAP tree to start the search from.
+
+.Performing an anonymous search
+====
+[source,shell]
+----
+ldapsearch -z 1 -x \
+ -H ldap://ldap.example.com:389 \
+ -b "cn=freeradius,dc=example,dc=com"
+----
+====
+
+== Failure - Bad FQDN/IP or attempt blocked by firewall
+
+```
+ldap_sasl_bind(SIMPLE): Can't contact LDAP server (-1)
+```
+
+* Verify (using `dig` or similar) that the FQDN of the LDAP
+server can be resolved.
+* Verify that there is no firewall blocking access. Some techniques to check this
+which _may_ work.
+** `sudo traceroute -Tn -p 636 ldap.example.com` will be successful (and not
+continue indefinitely) when there is _no_ firewall blocking.
+** `netcat -vz -w3 ldap.example.com 636` will return `succeeded` when there is no
+firewall blocking access.
+
+== Failure - Bind required
+
+```
+ldap_bind: Server is unwilling to perform (53)
+ additional info: unauthenticated bind (DN with no password) disallowed
+```
+
+The majority of LDAP servers will also require the LDAP client to identify itself,
+this is done with a bind operation.
+
+If a message similar to the one above is returned, it means a bind operation
+must be performed before attempting any operations on the LDAP server.
+
+The following arguments should be added to identify the LDAP client to the LDAP
+server:
+
+- `-D <dn>` - The bind DN. A unique identifier for the user being bound.
+- `-w <password>` - The bind password.
+
+.Performing a search with a bound user
+====
+[source,shell]
+----
+ldapsearch -z 1 -x \
+ -H ldap://ldap.example.com:389 \
+ -D "cn=freeradius,dc=example,dc=com" \
+ -w mypassword \
+ -b "dc=example,dc=com"
+----
+====
+
+== Failure - Encryption (confidentiality) required
+
+If a message similar to the one below is returned by `ldapsearch`. TLS must
+be enabled on the connection.
+
+```
+ldap_bind: Confidentiality required (13)
+ additional info: TLS confidentiality required
+```
+
+If you're not sure which encryption method is used (LDAPS or StartTLS) try
+each of them in turn.
+
+In order to configure either encryption method you will need to have acces
+to the certificate chain for the certificate the LDAP server presents.
+
+See the section below on retrieving certificate chains if you don't have
+access to these certificates.
+
+=== Common parameters for both LDAPS and StartTLS)
+
+- `LDAPTLS_CACERT=<ca_cert_and_intermediaries.pem>` - An environmental variable
+specifying a PEM file containing CA and chain of trusted certificates, that
+joins with the chain presented by the LDAP server. These certificates must be
+provided in order to validate the LDAP server's certificate.
+
+- `LDAPTLS_REQCERT=hard` - An environmental variable requiring the LDAP server
+provide a certificate.
+
+=== Encryption with LDAPS
+
+LDAPS is configured by changing the URI scheme passed as the value to `-H`.
+
+Where an unencrypted or StartTLS connection uses `ldap://`, LDAPS requires
+`ldaps://`.
+
+The port for LDAPS is different than for unencrypted communication. Where an
+unencrypted or StartTLS connection normally uses `TCP/389`, LDAPS normally uses
+`TCP/636`.
+
+For LDAPS an unencrypted or StartTLS URI is changed from:
+```
+-H ldap://<fqdn>:389
+```
+
+to
+
+```
+-H ldaps://<fqdn>:636
+```
+
+.Performing a search with a bound user with LDAPS
+====
+[source,shell]
+----
+LDAPTLS_CACERT=cert_bundle.pem LDAPTLS_REQCERT=hard \
+ ldapsearch -z 1 -x \
+ -H ldaps://ldap.example.com:636 \
+ -D "cn=freeradius,dc=example,dc=com" \
+ -w mypassword \
+ -b "dc=example,dc=com"
+----
+====
+
+=== Encryption with StartTLS
+
+StartTLS connections runs on the same port as unencrypted LDAP. StartTLS
+is an LDAP
+
+- `-ZZ` - Transition to encrypted communication using the StartTLS extension,
+and fail if we can't.
+
+.Performing a search with a bound user with StartTLS
+====
+[source,shell]
+----
+LDAPTLS_CACERT=cert_bundle.pem LDAPTLS_REQCERT=hard \
+ ldapsearch -z 1 -x \
+ -H ldap://ldap.example.com:389 \
+ -D "cn=freeradius,dc=example,dc=com" \
+ -w mypassword \
+ -b "dc=example,dc=com" \
+ -ZZ
+----
+====
+
+.Retrieving certificate chains from the server
+****
+If using LDAP over TLS `openssl s_client` can display information about the
+certificates presented by the LDAP server. The information returned
+(particularly the certificate issuer(s)) in useful to determine what
+certificates need to be available to the LDAP client.
+
+The `openssl` invocation is different depending on whether StartTLS or LDAPS
+is used.
+
+.LDAPS - Retrieving the certificate chain of the fictitious ldap.example.com server
+====
+```
+echo -n | openssl s_client -host ldap.example.com -port 636 -prexit -showcerts
+CONNECTED(00000003)
+depth=1 C = OT, ST = Tentacle Cove, O = FreeRADIUS, OU = Services, CN = example.com, emailAddress = support@example.com
+verify return:0
+---
+Certificate chain
+ 0 s:/C=OT/ST=Tentacle Cove/L=Grenoble/O=FreeRADIUS/OU=Services/CN=ldap.example.com/emailAddress=support@example.com
+ i:/C=OT/ST=Tentacle Cove/O=FreeRADIUS/OU=Services/CN=example.com/emailAddress=support@example.com
+-----BEGIN CERTIFICATE-----
+MIIHDjCCBPagAwIBAgIJANAO5znieeLNMA0GCSqGSIb3DQEBCwUAMIGSMQswCQYD
+...
+```
+====
+
+.StartTLS - Retrieving the certificate chain of the fictitious ldap.example.com server
+====
+```
+echo -n | openssl s_client -host ldap.example.com -port 389 -prexit -showcerts -starttls ldap
+CONNECTED(00000003)
+depth=1 C = OT, ST = Tentacle Cove, O = FreeRADIUS, OU = Services, CN = example.com, emailAddress = support@example.com
+verify return:0
+---
+Certificate chain
+ 0 s:/C=OT/ST=Tentacle Cove/L=Grenoble/O=FreeRADIUS/OU=Services/CN=ldap.example.com/emailAddress=support@example.com
+ i:/C=OT/ST=Tentacle Cove/O=FreeRADIUS/OU=Services/CN=example.com/emailAddress=support@example.com
+-----BEGIN CERTIFICATE-----
+MIIHDjCCBPagAwIBAgIJANAO5znieeLNMA0GCSqGSIb3DQEBCwUAMIGSMQswCQYD
+...
+```
+[NOTE]
+.Availability of `-starttls ldap`
+Not all builds of `openssl s_client` support `-starttls ldap`. As of OpenSSL
+1.1.1 this feature is still only available in the OpenSSL master branch. See
+this GitHub Pull Request for details:
+https://github.com/openssl/openssl/pull/2293.
+====
+****
--- /dev/null
+= Mapping and testing with ldapsearch
+
+Every LDAP directory is different. Each directory has different connection parameters,
+locations for objects, and attributes within those objects.
+
+This section of the LDAP howto will guide you through discovering the values
+needed to configure the LDAP module.
+
+We _strongly_ recommend running these checks in this section on the same machine
+that will be used to host the RADIUS server. A common cause of connectivity
+issues is that a firewall is preventing network access from the RADIUS server to
+the LDAP server. Running `ldapsearch` on a third machine may not reveal these
+firewall issues.
+
+== xref:modules/ldap/ldapsearch/before_starting.adoc[Before starting]
+
+What questions to ask your LDAP administrator before you begin working with the
+directory.
+
+== xref:modules/ldap/ldapsearch/connection_parameters.adoc[Determining connection parameters]
+
+Discover through trial and error, what connection parameters the LDAP
+directory requires.
+
+== xref:modules/ldap/ldapsearch/locating_objects.adoc[Locating objects]
+
+Discover where users and groups are located within the directory.
+
+== xref:modules/ldap/ldapsearch/translating_to_the_ldap_module.adoc[Using results to configure the LDAP module]
+
+Translate LDAP search arguments and the results from previous sections
+into LDAP module configuration items
--- /dev/null
+= Locating objects
+
+Once the correct xref:modules/ldap/ldapsearch/connection_parameters.adoc[connection parameters]
+ have been determined, the next step in building your configuration
+is to locate where user and group objects are stored in the directory.
+
+== How to locate user objects
+Users are represented by a fairly limited subset of ObjectClasses. The
+following filters are usually sufficient to identify users in different
+directory types.
+
+- OpenLDAP - `(|(ObjectClass=organizationalPerson)(ObjectClass=posixAccount)(ObjectClass=Person))`
+- ActiveDirectory - `(|(ObjectClass=User)(ObjectClass=posixAccount))`
+- Novell eDir - `(ObjectClass=User)`
+
+.Performing a search with a user object filter
+====
+```
+ldapsearch -z 10 -x -H ldap://ldap.example.com:389 -b "dc=example,dc=com" "(|(ObjectClass=organizationalPerson)(ObjectClass=PosixAccount)(ObjectClass=Person))"
+
+# extended LDIF
+#
+# LDAPv3
+# base <dc=example,dc=com> with scope subtree
+# filter: (|(ObjectClass=organizationalPerson)(ObjectClass=PosixAccount)(ObjectClass=Person))
+# requesting: ALL
+#
+
+...
+
+# doctopus, octopuses, example.com
+dn: uid=doctopus,ou=octopuses,dc=example,dc=com
+objectClass: person
+objectClass: organizationalPerson
+objectClass: inetOrgPerson
+cn: Doctopus McTentacles
+sn: McTentacles
+uid: dpus
+givenName: Doctopus
+userPassword:: MGN0MHB1NTNzUnVsMw==
+
+# search result
+search: 3
+result: 0 Success
+
+# numResponses: 18
+# numEntries: 17
+```
+====
+
+There's a lot of useful information in this result:
+
+- We know where the user objects are likely to be located, as it's fairly
+obvious from the DN (`...,ou=octopuses,dc=example,dc=com`). _Note: In the
+majority of non-FreeRADIUS themed LDAP directories the user object container
+will be `ou=people`_.
+- The result shows a `uid` attribute. This will almost always be the correct
+attribute for identifying a user.
+- The result shows a base64 encoded `userPassword` attribute. This
+means users can be authenticated without rebinding the connection (more
+efficient).
+- There's no `memberOf` attributes. This means user to group mappings
+are likely stored in group objects instead of the user objects themselves.
+
+=== What to record
+
+- `user_object_base_dn` - The DN higher in the tree than relevant users
+objects.
+- `user_object_class_filter` - The filter which matches the objectClass(es) of
+user objects.
+- `uid_attribute` - The attribute used to identify the user
+(usually `uid`, but can vary considerably between instances).
+- `user_password_attribute` - The attribute used to hold password data (if
+present).
+- `account_disabled_attribute` - Any attributes used to indicate whether an
+account is disabled. To determine if this attribute exists, repeat the user
+search (above) with a filter for a user account known to be disabled e.g.
+`(uid=a-disabled-user)`.
+- `account_enabled_attribute` - Any attributes used to indicate whether an
+account is enabled. Should be present in the search results already obtained.
+
+.Locating users in "mature" LDAP directories
+****
+The above result represents an ideal scenario. In reality, LDAP directories
+often accumulate a lot of cruft. Users objects may be located in
+multiple container objects, and at multiple levels within a directory.
+
+If you believe this to be the case with your directory, remove the `-z` argument
+limiting the number of result, and pipe the output of `ldapsearch` through a
+command chain _such as_: `grep dn: | sed -e 's/dn: [^,]*,//' | sort | uniq -c`.
+
+This operation will return a list of objects which _contain_ user objects,
+and how many user objects they contain.
+
+.Determining which objects contain users
+====
+```
+ldapsearch -x -H ldap://ldap.example.com:389 -b "dc=example,dc=com" "(|(ObjectClass=organizationalPerson)(ObjectClass=PosixAccount)(ObjectClass=Person))" | grep dn: | sed -e 's/dn: [^,]*,//' | sort | uniq -c`
+2 ou=octopuses,dc=example,dc=com
+10 ou=people,dc=example,dc=com
+1 ou=people,dc=created-by-someone-who-didnt-know-what-dc-meant,dc=example,dc=com
+2 ou=people,ou=remnant-of-an-ancient-edir-instance,dc=example,dc=com
+2 ou=giraffes,dc=example,dc=com
+```
+====
+
+The user object base DN selected for your rlm_ldap configuration must be higher
+in the object tree than the objects containing users. In the above example the
+user object base DN would be `dc=example,dc=com`.
+
+If an error message is returned similar to `Size limit exceeded (4)`, it means
+that the LDAP server is refusing to provide the complete set of results. If the
+LDAP directory supports result paging, adding a paging request such as `-E
+'pr=100'` may negate the per-search result limit.
+
+For large result sets you should also specify a list of attributes to retrieve
+after the search filter. Limiting the attributes returned reduces load on the
+LDAP server, and generally results in a quicker response.
+
+```
+ldapsearch ... -E 'pr=100' "(|(ObjectClass=...))" "dn" | ...
+```
+****
+
+== How to locate group objects
+
+Groups are represented by a fairly limited subset of ObjectClasses. The
+following filters are usually sufficient to identify groups in different
+directory types.
+
+- OpenLDAP - `(objectClass=groupOfNames)`
+- ActiveDirectory - `(objectClass=group)`
+- Novell eDir - `(objectClass=group)`
+
+.Performing a search with a group object filter
+====
+```
+ldapsearch -z 10 -x -H ldap://ldap.example.com:389 -b "dc=example,dc=com" "(objectClass=posixGroup)"
+
+# extended LDIF
+#
+# LDAPv3
+# base <ou=people,dc=example,dc=com> with scope subtree
+# filter: (ObjectClass=posixGroup)
+# requesting: ALL
+#
+
+...
+
+dn: cn=mariner-alert,ou=groups,dc=example,dc=com
+cn: mariner-alert
+description: Heads up to all delicious denizens of the directory
+member: uid=doctopus,ou=octopuses,dc=example,dc=com
+member: uid=rocktopus,ou=octopuses,dc=example,dc=com
+objectClass: groupOfNames
+objectClass: fdGroupMail
+mail: mariner-alert@example.com
+
+# search result
+search: 3
+result: 0 Success
+
+# numResponses: 10
+# numEntries: 9
+```
+====
+
+.Locating groups in "mature" directories
+****
+As with users, groups may be located in multiple areas of the directory.
+
+.Determining which objects contain groups
+====
+```
+ldapsearch -x -H ldap://ldap.example.com:389 -b "dc=example,dc=com" -E 'pr=100' "(ObjectClass=posixGroup)" dn | grep dn: | sed -e 's/dn: [^,]*,//' | sort | uniq -c`
+3 ou=groups,dc=example,dc=com
+1 ou=groups,o=long-defunct-org-that-was-merged-with-example-com,dc=example,dc=com
+3 ou=groups,ou=interns-summer-project,dc=example,dc=com
+2 ou=groups,ou=backup-restored-in-the-wrong-place,dc=example,dc=com
+```
+====
+****
+
+=== Group membership scheme variants
+Groups memberships can be represented in multiple ways. There are four main
+variants:
+
+1. User objects which reference groups by DN, _usually_ with multiple
+instances of the `memberOf` attribute.
+2. User objects which reference groups by name, again, _usually_
+with multiple instances of the `memberOf` attribute.
+3. Group objects which reference their members by DN, _usually_ with multiple
+instances of the `member` attribute.
+4. Group objects which reference their members by name, _usually_ with
+multiple instances of the `memberUid` attribute.
+
+In order to perform efficient group lookups which group membership scheme the
+LDAP server uses must be determined.
+
+Repeat the search for user objects using a filter that matches a user known
+to be members of one or more groups e.g.
+`(&(objectClass=posixUser)(uid=doctopus))`.
+
+- If the result shows a attribute containing the DN of known group, the LDAP
+server implements _variant 1_.
+
+- If the result shows a attribute containing the name of known group, the LDAP
+server implements _variant 2_.
+
+Repeat the the search for group objects using a filter that matches a group
+the user is known to be a member of e.g. `(&(objectClass=groupOfNames)(cn=mariner-alert))`.
+
+- If the result shows an attribute containing the DN of a user, the ldap server
+implements _variant 3_.
+
+- If the result shows an attribute containing the name of a user, the ldap server
+implements _variant 4_.
+
+=== If the variant can't be determined
+
+- There's a typographical error in the search filter or `ldapsearch` arguments.
+- The bound user doesn't have sufficient permission to view user or group
+objects. Contact your LDAP administrator.
+- The LDAP directory uses a special scheme to represent groups which requires
+querying a special OID, or providing additional search controls (outside of the
+scope of this howto).
+- Group memberships are represented with 'operational' attributes. Repeat the
+user object search with '+' set as the second positional argument.
+
+.Searching for a user object and returning operational attributes
+```
+ldapsearch -z 10 -x -H ldap://ldap.example.com:389 -b "dc=example,dc=com" "(ObjectClass=posixUser)" +
+```
+
+=== What to record
+
+* `group_object_base_dn` - The DN higher in the tree than all
+relevant group objects.
+* `group_name_attribute` - The attribute used to identify the group
+(usually `cn`).
+* _variant 1_
+** `membership_attribute` - User object attribute containing group
+membership information.
+* _variant 2_
+** `membership_attribute` - User object attribute containing group
+membership information.
+* _variant 3_
+** `membership_user_dn_filter` - A filter matching users by DN.
+* _vairant 4_
+** `membership_user_name_filter` - A filter matching users by user name.
--- /dev/null
+= Translating ldapsearch arguments to LDAP configuration items
+
+[width="100%",cols="30%,20%,50%",options="header",]
+|===
+| Purpose | ldapsearch argument | `ldap { ... }` config item
+| Limit number of search results | ```-z``` | Not supported.
+| Use basic authentication | ```-x``` | Set by default.
+| LDAP Host URI | ```-H <uri>``` | ```server = '<uri>'``` +
+ ```port = (389\|<custom port>)```
+| Base DN | ```-b <dn>``` | ```base_dn = '<dn>'```
+| Bind DN | ```-D <dn>``` | ```identity = '<dn>'```
+| Bind Password | ```-w <password>``` | ```password = '<password>'```
+| Enable LDAPS | ```-H ldaps://<uri>``` | ```server = 'ldaps://<uri>'``` +
+ ```port = (636\|<custom port>)```
+| Enable StartTLS | ```-ZZ``` | ```tls { start_tls = yes }```
+| Specify RootCA and intermediaries | ```LDAPTLS_CACERT=<ca_cert_and_intermediaries.pem>``` | ```tls { ca_file = '<ca_cert_and_intermediaries.pem>' }```
+| Require cert validation to succeed | ```LDAPTLS_REQCERT=hard``` | ```tls { require_cert = 'demand' }```
+|===
+
+= Translating ldapsearch results to rlm_ldap configuration items
+
+== Users
+
+[width="100%",cols="30%,70%",options="header",]
+|===
+| Purpose | `ldap { user { ... } }` config item
+| Specify where to search for users | ```base_dn = '<user_object_base_dn>'```
+| Specify how to find a user | ```filter = "(&(<user_object_class_filter>)(<uid_attribute>=%{%{Stripped-User-Name}:-%{User-Name}})"```
+| Retrieve a "known good" password | ```update { &control:Password-With-Header = <user_password_attribute>```
+| Allow accounts to be explicitly disabled | ```access_attribute = '<account_disabled_attribute>'``` +
+ ```access_positive = 'no'```
+| Require accounts to be explicitly enabled | ```access_attribute = '<account_enabled_attribute>'``` +
+ ```access_positive = 'yes'```
+|===
+
+== Groups - Common
+
+[width="100%",cols="30%,70%",options="header",]
+|===
+| Purpose | `ldap { group { ... } }` config item
+| Specify where to search for group | ```base_dn = '<group_object_base_dn>'```
+| Specify which objects are groups | ```filter = '<group_object_class_filter>'```
+| Specify which attribute in a group object
+ identifies the group | ```name_attribute = '<group_name_attribute>'```
+|===
+
+== Groups - variant 1
+
+User objects reference groups using DNs.
+
+[width="100%",cols="30%,70%",options="header",]
+|===
+| Purpose | `ldap { group { ... } }` config item
+| Specify how to find group objects by DN, when referenced by a user object. | ```membership_attribute = '<group_object_base_dn>'```
+|===
+
+== Groups - variant 2
+
+User objects reference groups using group names.
+
+[width="100%",cols="30%,70%",options="header",]
+|===
+| Purpose | `ldap { group { ... } }` config item
+| Specify how to find group objects by name, when referenced by a user object. | ```membership_attribute = '<group_object_base_dn>'```
+|===
+
+== Groups - variant 3
+
+Group objects reference users using DNs.
+
+[width="100%",cols="30%,70%",options="header",]
+|===
+| Purpose | `ldap { group { ... } }` config item
+| Specify how to find group objects referencing a user by DN. | ```membership_filter = "(<membership_user_dn_filter>=%{control:Ldap-UserDn})"```
+|===
+
+== Groups - variant 4
+
+Group objects reference users using user names.
+
+[width="100%",cols="30%,70%",options="header",]
+|===
+| Purpose | `ldap { group { ... } }` config item
+| Specify how to find group objects referencing a user by name. | ```membership_filter = "(<membership_user_name_filter>=%{%{Stripped-User-Name}:-%{User-Name}})"```
+|===
+
+.Mixing and matching group membership schemes
+****
+Although rare, it is possible to have all four group membership scheme variants
+in a single directory. FreeRADIUS supports this configuration.
+
+For _variant 1_ and _variant 2_ FreeRADIUS will automatically determine if the
+user object attribute contained a DN or group name.
+
+For _variant 3_ and _variant 4_ it's possible to construct a filter which matches
+both on user DN and user name e.g.
+
+[source,config]
+----
+membership_filter = "(|(<membership_user_dn_filter>=%{control:Ldap-UserDn})(<membership_user_name_filter>=%{%{Stripped-User-Name}:-%{User-Name}}))"
+----
+****