Enhancing OpenLDAP security with argon2

Storing passwords securely has always presented challenges to developers and system administrators. By default, OpenLDAP offers multiple hashing algorithms such as MD5 and SHA1, along with their salted versions SMD5 and SSHA. All of these hashing algorithms have known weaknesses and are considered insecure for the purpose of hashing sensitive information.

In this blog post we will show you the steps necessary to compile OpenLDAP with argon2 support and enable argon2 as the default hashing algorithm in OpenLDAP 2.6.

Introducing argon2 hashes

Argon2 is a modern password hashing algorithm that won the Password Hashing Competition in 2015. Compared to the traditional hashing algorithms such as MD5 or SHA1 before, argon2 offers a significant advantage as it was specifically designed to be resistant against brute-force as well as side-channel attacks.

The need for migration

Until recently, our OpenLDAP servers were still running on CentOS7. As support for CentOS7 will end in June 2024, we initiated the migration plan to move all our OpenLDAP servers to Rocky Linux 9. Support for argon2 was first introduced in OpenLDAP 2.4.50 as a contrib module, and later in version 2.5 in mainline of the openldap-servers package. Currently, the OpenLDAP version available in the official Rocky9 repositories is 2.6.2 and thus it supports argon2 hashes out of the box. Right?

 

Unfortunately, it is not that easy. For OpenLDAP to support argon2 hashes, it must be compiled using the correct flags (--enable-argon2) and there are some build dependencies (libargon2 or libsodium) which must be present during the build. The upstream sources used to build the openldap packages are missing the required parameters to compile OpenLDAP with argon2 support and since Rocky Linux aims to be bug-for-bug compatible, these parameters are also missing in the sources used to build the Rocky packages.

Building our own OpenLDAP packages

As mentioned above, the OpenLDAP packages provided by the Rocky Linux repositories do not meet our requirements. If we want to use argon2 hashes, we are therefore forced to build the OpenLDAP packages ourselves and provide the necessary adjustments to the packages‘ spec file. And that is exactly what we did. We used the sources from the official Rocky Linux OpenLDAP repository and updated the spec file to include the flags and build dependencies. The packages are then built using the Fedora COPR build system and the resulting artifacts are available at the following url: https://copr.fedorainfracloud.org/coprs/puzzle/puzzle/. The source repository used to build the packages is also publicly available: https://gitlab.puzzle.ch/package-builds/openldap.

Success

With our custom-built packages now ready, we can go ahead and perform our initial tests. First of all, we need to load the argon2 module. As we use Ansible to manage all our configurations, we use the community.general.ldap_attrs module to modify the cn=config of our OpenLDAP servers:

- name: configure modules
  community.general.ldap_attrs:
    dn: "cn=module{0},cn=config"
    attributes:
      olcModuleLoad: "argon2 m=32768"
    state: present

In the YAML code above, we specified the m parameter for the argon2 module. It is essential to pay attention to this parameter, as it determines the amount of memory used to calculate a single hash. A busy OpenLDAP server might process numerous bind requests (and therefore has to calculate many hashes) in parallel or in quick succession. To determine an appropriate value for the m parameter, aim for a large value while staying within the memory capacity of your server and its expected load. The recommendations in the RFC (2GiB and 64MiB) are likely too high for many use-cases and OWASP’s recommendation of 12 MiB already offers significant benefit. We chose 32MiB as a practical compromise between the RFC and our available RAM. Remember to allocate some reserves and define rate limits to prevent a single client from overloading your OpenLDAP server.
Check the man page of slappw-argon2 to see more details about the available parameters. The short summary, however, would be this: m is the most important of the three. Any amount in the MiB range slows the hash rate of an attacker using commercially available GPUs massively – from billions of hashes per second (on weak algorithms) to mere thousands. Whether it is 16 or 64 MiB makes a small difference by comparison but can matter a lot for your OpenLDAP instance that has to handle hundreds of requests at a time. The recommendation from the RFC of using 2GiB obviously slows attackers down even further, but is also only useful if you are handling a single-digit amount of hashes at a time or have terabytes of memory.
t, the minimum number of iterations, and p, the degree of parallelism, by comparison, are not nearly as impactful as m and can be used for fine-tuning the timing of a single hash calculation. Either of the recommendations from the RFC or OWASP will be fine: t=3 and p=1 or p=4.

Next, we configure the password policy and instruct OpenLDAP to hash new passwords using argon2:

- name: Set password hash policy
  community.general.ldap_attrs:
    dn: "olcDatabase={-1}frontend,cn=config"
    attributes:
      olcPasswordHash: "{ARGON2}"


Finally we can update the password of a user and check the userPassword attribute:

{ARGON2}$argon2id$v=19$m=32768,t=3,p=1$XYZ$XYZ 

Learnings and conclusion

When initially compiling OpenLDAP, we relied on libargon2 as a build dependency. During that time, we observed that the password hashes were formatted as {ARGON2}$argon2i$v=19$..., indicating the use of the argon2i variant for password hashing. The Argon2 algorithm offers three variants: one primary variant and two supplementary variants:

  • argon2i
    Optimized to resist side-channel attacks.
  • argon2d
    Designed to provide the highest resistance against GPU cracking attacks and time-memory trade-off attacks.
  • argon2id
    This is the primary variant which is a hybrid of both argon2i and argon2d and provides both resistance to side-channel attacks as well as resistance against GPU cracking attacks.

Argon2id seems to be the best choice since it combines the advantages of both variants, which is also what the RFC for Argon2 recommends. We took a closer look at the source code of OpenLDAP to figure out why argon2i was chosen over argon2id. Apparently when using libsodium OpenLDAP calls the argon2i_hash_encoded method to perform the hashing and, as the name implies and the documentation confirms, this method uses argon2i. We also noted that when libsodium was used instead of libargon2, OpenLDAP uses argon2id. This behavior seemed somewhat inconsistent, and we were unable to find any documentation explaining the reason behind this decision. Nevertheless we proceeded to recompile OpenLDAP with libsodium and generated a new password, confirming that when compiled with libargon2, the generated hashes indeed use the argon2i variant, while with libsodium argon2id is used.

The migration from Centos7 to Rocky Linux 9 for our OpenLDAP servers was driven by the desire to strengthen password security through the use of argon2 hashes. Despite encountering challenges in the default packages provided by the official Rocky Linux repositories, we overcame them by building custom packages and sharing them with the community through our COPR repository.

2 Kommentare

  • Tiago Stapenhorst Martins, 23. August 2023

    Hey, great article!
    How you managed to find out if your OpenLDAP installation supported argon2? Have you used any specific command?
    You used CentOS and Rocky Linux as an example but would be intresting to diagnose that on other distros as well such as Debian or Ubuntu.
    I would be greatly appreciated if you could share that.

  • mm
    Reto Kupferschmid, 25. August 2023

    Thank you for the comment.
    You can try to load the argon2 module by using the olcModuleLoad attribute described above. If the module is not available on your system, this command will fail. You can also look for the compiled argon2 module which should be a file named argon2.so. By default, the modules are located under /usr/lib/ldap but this path might be different on Ubuntu/Debian systems.