# ldapdock *_a configurable container running openLDAP_* Step by step approach on how to setup and run the openLDAP server on a classic systemd-less Docker image container _note about the dockerfile and running the generated image container on FG (foreground) or BG (background): by default the dockerfile generates an image to be run in FG, it expects to be run into it and launch slapd (openLDAP server) manually; to run the image container in BG and start slapd automatically without any user intervention, uncomment the line number 31 of the dockerfile._ ## _Creating the ldapdock image container_ build ldapdock ``` > docker build -t ldapdock /path/to/dockerfile ``` after build, check the docker image has been created properly with the given REPOSITORY name ``` > docker images REPOSITORY TAG IMAGE ID CREATED SIZE ldapdock latest 0e4a1521b346 6 hours ago 138MB ``` If you just want to jump in the container and right now don't care saving the configuration or directories, you can run it with this command: ``` > docker run -h example.com -i -t ldapdock /bin/bash ``` If you wish (and it is recommended in development) to save the configuration and LDAP directory structure (also called LDAP database) outside of the container, run this command instead: ``` > docker run -h example.com -i -t -v ldap_data:/var/lib/ldap -v ldap_config:/etc/ldap/slapd.d ldapdock /bin/bash ``` `Parameters explanation:`with -h we are specifying the name of the host, we are using example.com, this is very important. -i tells docker to run in an interactive way instead of running the container in the background. -t goes in hand with -i, and allocates a tty (terminal) so we can run commands. -v mounts a volume to save information (we use one to save the data and another one to save the configuration). ## _Explaining DN, parentDN, CN, and DC as parameters_ One of the key configuration of LDAP is our "DC" or "parent DN" and other terms, which to explain it in a pure pragmatic way, we will use some examples: we use per defect example.com as our domain, so the DC (Distinguished Name) that we would use it is **"dc=example,dc=com"**, instead, if our domain would be for example "ideas.lab.com", the parent DN would be "dc=ideas,dc=lab,dc=com". This configuration it's very often passed with the CN (Common Name) in concatenation with the DN (Distinguished Name), and the result it's very simple, in the case of the domain example.com, it is **DN: "cn=config,dn=example,dn=com"**, or for ideas.lab.com DN: "cn=config,dn=ideas,dn=lab,dn=com". ## _Inside the ldapdock image container_ Use the following command to start openLDAP ``` root@example:/# slapd -h "ldap:/// ldapi:///" -g openldap -u openldap -F /etc/ldap/slapd.d ``` It's always a good idea to test connectivity to slapd the first times ``` root@example:/# ldapsearch -x -H ldap://localhost -b "dc=example,dc=com" -s base "(objectclass=*)" # extended LDIF # # LDAPv3 # base with scope baseObject ... ``` ## _Create an Administrator account_ In order to create users with different attributes and permits, we need to create a new admin account besides the root one that comes with slapd by default.\ We will refer to the LDAP Administrator account as **admin or administrative account**, and to the **root account** simply the one sat by default. When running any *Administrative task* that requires the usage of either the admin or root account, like creating an Organizational Unit (ou) or a new user, both accounts will have set the same privileges, meaning both will work, but *it is strongly recommended to use the admin or administrative one created here.* An easy way to differentiate them it's setting different passwords for each one, as we will see... Generate a password hash for our administrator user, 1234 here being the password ``` root@example:/# slappasswd -s 1234 # Change 1234 to your desired password {SSHA}yxIgYTzcuRRdlesjfWkIN6K97/8jOrZF ``` Create the .ldif file that will create the admin user, editing the _userPassword_ attribute with our password hash ``` root@example:/# vim create_admin.ldif dn: cn=admin,dc=example,dc=com changetype: add objectClass: organizationalRole objectClass: simpleSecurityObject cn: admin userPassword: {SSHA}yxIgYTzcuRRdlesjfWkIN6K97/8jOrZF # Replace with the hash of your password description: LDAP administrator ``` Execute create_admin.ldif using the root password (which is the default container's: _admin_) ``` root@example:/etc/ldap# ldapadd -x -H ldap:/// -D "cn=admin,dc=example,dc=com" -w admin -f create_admin.ldif adding new entry "cn=admin,dc=example,dc=com" ``` Check the attributes of our new administrator user of our domain (parentDN) ``` root@example:/# ldapsearch -x -H ldap:/// -D "cn=admin,dc=example,dc=com" -w 1234 -b "cn=admin,dc=example,dc=com" "(objectclass=*)" # extended LDIF # # LDAPv3 # base with scope subtree # filter: (objectclass=*) # requesting: ALL # # admin, example.com dn: cn=admin,dc=example,dc=com objectClass: organizationalRole objectClass: simpleSecurityObject cn: admin userPassword:: e1NTSEF9eXhJZ1lUemN1UlJkbGVzamZXa0lONks5Ny84ak9yWkY= description: LDAP administrator ... ``` That's all, our administrator user was properly done. ## _First administrative tasks_ ### _Create our first Organizational Unit (ou) with a new user_ Prepare a new LDAP directory (ou) called Supergirls with the following data ``` root@example:/# vim add_ou.ldif dn: ou=Supergirls,dc=example,dc=com objectClass: organizationalUnit ou: Supergirls ``` Execute the .ldif file to create it in the LDAP server, and when asked for the **root password**, remember in the dockerfile by default is _admin_ ``` root@example:/# ldapadd -x -D "cn=admin,dc=example,dc=com" -W -f add_ou.ldif Enter LDAP Password: adding new entry "ou=Supergirls,dc=example,dc=com" ``` verify the entry in the LDAP server ``` root@example:/# ldapsearch -x -LLL -b "dc=example,dc=com" "(ou=Supergirls)" dn dn: ou=Supergirls,dc=example,dc=com ``` create a new LDAP password to manage our new directory, annotate both the entered _plain password_ and the result _hashed password_ ``` root@example:/# slappasswd New password: Re-enter new password: {SSHA}hashedpasswd ``` create a .ldif file with the necessary attributes to insert in our Supergirls directory ``` root@example:/# vim add_user_supergirls.ldif dn: uid=marisa,ou=Supergirls,dc=example,dc=com objectClass: inetOrgPerson objectClass: posixAccount cn: Marisa sn: Kirisame givenName: Marisa displayName: Marisa Kirisame uid: marisa uidNumber: 1001 gidNumber: 5000 homeDirectory: /home/marisa loginShell: /bin/bash userPassword: {SSHA}hashedpasswd mail: marisa@example.com ``` insert the new user (marisa) in our Supergirls directory (LDAP OU), still using the root password _admin_ ``` root@example:/# ldapadd -x -D "cn=admin,dc=example,dc=com" -W -f add_user_supergirls.ldif Enter LDAP Password: adding new entry "uid=marisa,ou=Supergirls,dc=example,dc=com" ``` verify the user (marisa) has been added to the Supergirls OU ``` root@example:/# ldapsearch -x -LLL -b "dc=example,dc=com" "(uid=marisa)" dn dn: uid=marisa,ou=Supergirls,dc=example,dc=com ``` ### _Modify users attributes_ create a new .ldif file with the attributes we want to change\ in this case we want to modify the _mail_ marisa@example.com of the user (_uid_) marisa from the group (_ou_) Supergirls ``` root@example:/home# vim modify_user.ldif dn: uid=marisa,ou=Supergirls,dc=example,dc=com changetype: modify replace: mail mail: marisa.kirisame@example.com ``` run the modify file, when asked for the root password, remember in the dockerfile by default is _admin_ ``` root@example:/home# ldapmodify -x -D "cn=admin,dc=example,dc=com" -W -f modify_user.ldif Enter LDAP Password: modifying entry "uid=marisa,ou=Supergirls,dc=example,dc=com" ``` verify the _mail_ attribute of the user marisa has been changed to marisa.kirisame@example.com ``` root@example:/home# ldapsearch -x -LLL -b "dc=example,dc=com" "(uid=marisa)" mail dn: uid=marisa,ou=Engineering,dc=example,dc=com mail: marisa.kirisame@example.com ``` ### _Modify user password_ In this examples, we are changing the special attribute password of the user marisa from ou Supergirls, using the old password.\ \ In order to change the password interactively (writing in the prompt when asked), we can run this command: ``` root@example:/etc/ldap# ldappasswd -H ldap:/// -x -D "uid=marisa,ou=Supergirls,dc=example,dc=com" -W -S "uid=marisa,ou=Supergirls,dc=example,dc=com" New password: newpasswd Re-enter new password: newpasswd Enter LDAP Password: oldpasswd ``` _newpasswd_ being the new password we want to use, and _oldpasswd_, the last password we were using for the user uid marisa.\ \ To change the password in an non interactive (sending the password directly via the command), we can run this: ``` root@example:/etc/ldap# ldappasswd -H ldap:/// -x -D "uid=marisa,ou=Supergirls,dc=example,dc=com" -w newpasswd "uid=marisa,ou=Supergirls,dc=example,dc=com" New password: 6vUj/2lE ``` _newpasswd_ being the new password we want to use. We can also notice the hashed output of our new password is not a typical LDAP SSHA hash, this is due to security implementations. ### _Reset user password_ In the likely common event that we forgot the old password of an specific user, we need to reset it.\ In this example we forgot the password of the user uid marisa, we can reset it with this command: ``` root@example:/etc/ldap# ldappasswd -H ldap:/// -x -D "cn=admin,dc=example,dc=com" -W -S "uid=marisa,ou=Supergirls,dc=example,dc=com" New password: newpasswd Re-enter new password: newpasswd Enter LDAP Password: admin ``` Note we need to use the **root** password (_admin_ by default) in the last query ("Enter LDAP Password") to reset an user's password. ### _Query as an specific user_ we already created the user (_uid_) marisa, and established the user's own password using slappasswd\ now we are gonna query our LDAP server using the user (_uid_) marisa credentials, and _the password we entered during slappasswd, called plain password (plainpasswd)_ ``` root@example:/etc/ldap# ldapsearch -D uid=marisa,ou=Supergirls,dc=example,dc=com -b "dc=example,dc=com" -w plainpasswd # extended LDIF # # LDAPv3 # base with scope subtree # filter: (objectclass=*) # requesting: ALL # # example.com dn: dc=example,dc=com objectClass: top objectClass: dcObject objectClass: organization o: nodomain dc: example # Supergirls, example.com dn: ou=Supergirls,dc=example,dc=com ... ``` we can narrow this search to get only specific attributes of the user marisa, remember we are using _the plainpasswd when asked_ ``` root@example:/etc/ldap# ldapsearch -D uid=marisa,ou=Supergirls,dc=example,dc=com -b "dc=example,dc=com" -w plainpasswd givenName uidNumber gidNumber homeDirectory # extended LDIF # # LDAPv3 # base with scope subtree # filter: (objectclass=*) # requesting: givenName uidNumber gidNumber homeDirectory # # example.com dn: dc=example,dc=com # Supergirls, example.com dn: ou=Supergirls,dc=example,dc=com # marisa, Supergirls, example.com dn: uid=marisa,ou=Supergirls,dc=example,dc=com givenName: Marisa uidNumber: 1001 gidNumber: 5000 homeDirectory: /home/marisa ``` ### _Reset root password_ Build line by line, the **.ldif** file we will need to reset root password, starting with the following command: ``` root@example:/# ldapsearch -Q -LLL -Y EXTERNAL -H ldapi:/// -b cn=config '(olcSuffix=dc=example,dc=com)' dn > rootpw.ldif ``` which writes to the rootpw.ldif file, the current rootDN (Distinguised Name): `dn: olcDatabase={1}mdb,cn=config`\ The next command will add the 'changetype' (modify, add, etc.) and what object are we working with: ``` root@example:/# echo -e 'changetype: modify\nreplace: olcRootPW: ' >> rootpw.ldif root@example:/etc/ldap# cat rootpw.ldif dn: olcDatabase={1}mdb,cn=config changetype: modify replace: olcRootPW ``` We run a simple sed command to delete blank lines ``` root@example:/# sed '/^$/d' rootpw.ldif > chrootpw.ldif root@example:/# cat chrootpw.ldif dn: olcDatabase={1}mdb,cn=config changetype: modify replace: olcRootPW ``` It's time to write our new password (_newpasswd_): ``` root@example:/# slappasswd -s 1234 {SSHA}2xbd33S4ZumAZW4Oks0GJidBFJYEVBPz ``` The last line it's our password 1234 hashed in SSHA cryptography. We will need to copy and paste it in the following command: ``` root@example:/# echo "olcRootPW: {SSHA}2xbd33S4ZumAZW4Oks0GJidBFJYEVBPz" >> chrootpw.ldif ``` The file that describes the variables needed to change our root password, **chrootpw.ldif** should be ready, we finally run: ``` root@example:/etc/ldap# ldapmodify -Q -Y EXTERNAL -H ldapi:/// -f chrootpw.ldif modifying entry "olcDatabase={1}mdb,cn=config" ``` If successful, the output will show the modified entry. ## _Loading and enabling policies_ Since no policy overlays are loaded in slapd in the container, we need to load our own. \ In the next command, notice we are using the -Q and -Y EXTERNAL -H ldap**i**:///, meaning SASL EXTERNAL authentication over the -x -H ldap:/// socket, which we usually use for binding as the root account. Using -Q -Y EXTERNAL -H ldap**i**:/// works because it binds as the openldap user and has sufficient permissions for cn=config. Run the following command to query our loaded modules list ``` root@example:/# ldapsearch -Q -Y EXTERNAL -H ldapi:/// -D "cn=admin,dc=example,dc=com" -b cn=config "(objectclass=olcModuleList)" # extended LDIF # # LDAPv3 # base with scope subtree # filter: (objectclass=olcModuleList) # requesting: ALL # # module{0}, config dn: cn=module{0},cn=config objectClass: olcModuleList cn: module{0} olcModulePath: /usr/lib/ldap olcModuleLoad: {0}back_mdb ``` Reading the output in detail, means we are only loading the default backend (olcModuleLoad: {0}back_mdb) that comes by default with LDAP to load basic schemas such as directories (OU) creation. Run the following command: ``` root@example:/# ls /usr/lib/ldap/ppolicy* /usr/lib/ldap/ppolicy-2.5.so.0 /usr/lib/ldap/ppolicy-2.5.so.0.1.14 /usr/lib/ldap/ppolicy.la /usr/lib/ldap/ppolicy.so ``` Our LDAP server may not come loaded with the policies we need to apply features such as passwords schemas and ACLs (Access Control Lists), but the modules exists inside the container image. We need to make use of new schemas and **policies**, which in large part exists in /usr/lib/ppolicy.so -since the module exists, we are going to create modify_ppolicy_module.ldif to be able to make use of it: ``` root@example:/# vim modify_ppolicy_module.ldif dn: cn=module{0},cn=config changetype: modify add: olcModuleLoad olcModuleLoad: ppolicy.so ``` Run modify_ppolicy_module.ldif ``` root@example:/# ldapmodify -Q -Y EXTERNAL -H ldapi:/// -f modify_ppolicy_module.ldif modifying entry "cn=module{0},cn=config" ``` Now we run the exact same command as before to check if the policy overlay was loaded ``` root@example:/# ldapsearch -Q -Y EXTERNAL -H ldapi:/// -D "cn=admin,dc=example,dc=com" -b cn=config "(objectclass=olcModuleList)" # extended LDIF # # LDAPv3 # base with scope subtree # filter: (objectclass=olcModuleList) # requesting: ALL # # module{0}, config dn: cn=module{0},cn=config objectClass: olcModuleList cn: module{0} olcModulePath: /usr/lib/ldap olcModuleLoad: {0}back_mdb olcModuleLoad: {1}ppolicy.so ``` Notice the addition of **olcModuleLoad: {1}ppolicy.so**. If we get a different result from the last command, we won't be able to enable the schemas or ACLs we need, and should check that we did input the right commands to reach this point, from the commands to run the container, if we started slapd with the right parameters, to the correct creation of the user administrator. To enable our new schemas and policies, that is, to load our new module ppolicy.so in our openLDAP server, we need to restart it, we are going to do it manually (using grep it's optional): ``` root@example:/# kill $(pidof slapd) root@example:/# ps ax | grep slap 30 pts/0 S+ 0:00 grep --color=auto slap root@example:/# slapd -h "ldap:/// ldapi:///" -g openldap -u openldap -F /etc/ldap/slapd.d root@example:/# ps ax | grep slap 32 ? Ssl 0:00 slapd -h ldap:/// ldapi:/// -g openldap -u openldap -F /etc/ldap/slapd.d 36 pts/0 S+ 0:00 grep --color=auto slap ``` Now that we restarted our openLDAP server, we can load the new module, so we create the following .ldif file: ``` root@example:/# vim enable_ppolicy.ldif dn: olcOverlay=ppolicy,olcDatabase={1}mdb,cn=config changetype: add objectClass: olcOverlayConfig objectClass: olcPPolicyConfig olcOverlay: ppolicy olcPPolicyDefault: cn=default,ou=policies,dc=example,dc=com ``` We load the module ``` root@example:/# ldapadd -Q -Y EXTERNAL -H ldapi:/// -f enable_ppolicy.ldif adding new entry "olcOverlay=ppolicy,olcDatabase={1}mdb,cn=config" ``` And then verify it is enabled ``` root@example:/# ldapsearch -Q -LLL -Y EXTERNAL -H ldapi:/// -b cn=config "(objectclass=olcOverlayConfig)" dn: olcOverlay={0}ppolicy,olcDatabase={1}mdb,cn=config objectClass: olcOverlayConfig objectClass: olcPPolicyConfig olcOverlay: {0}ppolicy olcPPolicyDefault: cn=default,ou=policies,dc=example,dc=com ``` If the same output was returned, we are done with creating and loading the policies module, and we can begin creating .ldif with our schemas. ## _Creating Passwords policies and schemas_ First of all, update our openLDAP ACL (Acess Control List) so we can have SASL EXTERNAL perms for the Linux openLDAP user, "openldap", so it can enforce all the following rules we are going to create. Create the file update_acl.ldif with the following content: ``` root@example:/# vim update_acl.ldif dn: olcDatabase={1}mdb,cn=config changetype: modify replace: olcAccess olcAccess: {0}to attrs=userPassword,pwdPolicySubentry by self write by anonymous auth by dn.exact="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" write by * none olcAccess: {1}to * by dn.exact="cn=admin,dc=example,dc=com" manage by * read ``` This probably looks confusing and even scary now, but it's pretty simple, it basically adds the pwdPolicySubentry attribute to the attributes SASL EXTERNAL can write. We will come back to it later anyways. ``` root@example:/# ldapmodify -Q -Y EXTERNAL -H ldapi:/// -f update_acl.ldif ``` Let's create a new basic LDAP directory with the Organizational Unit (ou) Supergirls and let's add the LDAP users (uid) Reimu and Marisa to the ou ``` root@example:/# vim create_directory.ldif dn: ou=Supergirls,dc=example,dc=com changetype: add objectClass: organizationalUnit ou: Supergirls dn: uid=reimu,ou=Supergirls,dc=example,dc=com changetype: add objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson uid: reimu cn: Reimu Hakurei sn: Hakurei userPassword: {SSHA}mRl... # Generate with: slappasswd -s ying dn: uid=marisa,ou=Supergirls,dc=example,dc=com changetype: add objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson uid: marisa cn: Marisa Kirisame sn: Kirisame userPassword: {SSHA}cgT... # Generate with: slappasswd -s yang ``` That's a lot of data, but it creates our Supergirls directory, and with it the users reimu and marisa. ### _Blocking user access with user's password after 3 tries_ Let's apply the following policy on the user reimu from the Organizational Unit Supergirls: after failing to interact in any way with the LDAP server using the user's wrong password, the LDAP server with block the user and it will disabled of any action until an administrator unlocks it. ``` root@example:/# vim apply_policy_reimu.ldif dn: uid=reimu,ou=Supergirls,dc=example,dc=com changetype: modify replace: pwdPolicySubentry pwdPolicySubentry: cn=default,ou=policies,dc=example,dc=com ``` And execute the apply_policy_reimu.ldif file ``` root@example:/# ldapmodify -Q -Y EXTERNAL -H ldapi:/// -f apply_policy_reimu.ldif modifying entry "uid=reimu,ou=Supergirls,dc=example,dc=com" ``` Run **again** the following taking note of the new hashed passwords ``` root@example:/# slappasswd -s ying {SSHA}QkBaHJh2CFSq9dup+Hiest9jnYMgVrll ``` Finally, create a new file reset_reimu_password.ldif and replace the userPassword with the correct hashed password ``` root@example:/# vim reset_reimu_password.ldif dn: uid=reimu,ou=Supergirls,dc=example,dc=com changetype: modify replace: userPassword userPassword: {SSHA}QkBaHJh2CFSq9dup+Hiest9jnYMgVrll ``` Execute reset_reimu_password.ldif ``` root@example:/# ldapmodify -Q -Y EXTERNAL -H ldapi:/// -f reset_reimu_password.ldif modifying entry "uid=reimu,ou=Supergirls,dc=example,dc=com" ```