ppm(5)

📦
version 2.6.13 · 2 versions
  • 2.6.13 · debian trixie, ubuntu noble, ubuntu questing, ubuntu resolute, ubuntu stonking (latest)
  • 2.5.20 · ubuntu jammy
, in debian trixie, ubuntu noble, ubuntu questing, ubuntu resolute, ubuntu stonking

💖 ppm(5) — the Password Policy Module 💖

okay so you found the manual page for ppm, which lives in the File Formats Manual section 5. It's giving "your password has to be actually good" energy, and honestly? Iconic. Let's get into it, bestie 🫶


✨ NAME ✨

ppm (Password Policy Module) — an extension of the password policy overlay. That's the whole name, no cap.


✨ SYNOPSIS ✨

ETCDIR/ppm.example

✨ DESCRIPTION ✨

So like, ppm is an OpenLDAP module that checks how quality your passwords are when somebody modifies them. The way it works is it looks at whether certain character classes are present or absent in the password. Simple as that 💅

This module is used as an extension of the OpenLDAP password policy controls — go peek at slapo-ppolicy(5), section pwdCheckModule, if you wanna see where it slots in.


✨ USAGE ✨

First things first: you gotta create a password policy entry, point it at the path of the ppm.so library, and then hand it the content of whatever policy you want. And because the policy gets stored inside pwdCheckModuleArg, you need a base64 tool to code / decode that content. Yes bestie, base64, we're doing that 🙈

Here's an example, and this one is for OpenLDAP 2.6:

dn: cn=default,ou=policies,dc=my-domain,dc=com
objectClass: pwdPolicy
objectClass: top
objectClass: pwdPolicyChecker
objectClass: person
pwdCheckQuality: 2
pwdAttribute: userPassword
sn: default
cn: default
pwdMinLength: 6
pwdCheckModuleArg:: bWluUXVhbGl0eSAzCmNoZWNrUkROIDAKY2hlY2tBdHRyaWJ1dGVzCmZvcmJpZGRlbkNoYXJzCm1heENvbnNlY3V0aXZlUGVyQ2xhc3MgMAp1c2VDcmFja2xpYiAwCmNyYWNrbGliRGljdCAvdmFyL2NhY2hlL2NyYWNrbGliL2NyYWNrbGliX2RpY3QKY2xhc3MtdXBwZXJDYXNlIEFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaIDAgMSAwCmNsYXNzLWxvd2VyQ2FzZSBhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5eiAwIDEgMApjbGFzcy1kaWdpdCAwMTIzNDU2Nzg5IDAgMSAwCmNsYXNzLXNwZWNpYWwgPD4sPzsuOi8hwqfDuSUqwrVewqgkwqPCsibDqX4iIyd7KFstfMOoYF9cw6dew6BAKV3CsD19KyAwIDEgMAoK
pwdUseCheckModule: TRUE

Now for OpenLDAP 2.5, plot twist: you have to add a pwdCheckModule attribute that points at the ppm module (like, for example, /usr/local/lib/ppm.so), and remove the pwdUseCheckModule attribute. Different era, different rules 😭

Go read slapo-ppolicy for the full tea, but to sum up:

  • enable the ppolicy overlay in your database.
  • define a default password policy in the OpenLDAP configuration, or use the pwdPolicySubentry attribute to point at the given policy.

This example shows the activation for a slapd.conf file on OpenLDAP 2.6 (and hey, check out slapd-config and slapo-ppolicy if you need more info about cn=config configuration):

overlay ppolicy
ppolicy_default "cn=default,ou=policies,dc=my-domain,dc=com"
ppolicy_check_module /usr/local/openldap/libexec/openldap/ppm.so
#ppolicy_use_lockout   # for having more infos about the lockout

But for OpenLDAP 2.5, you must remove the ppolicy_check_module parameter, because it's managed in the password policy definition instead. Periodt.


✨ FEATURES ✨

Here's the main character stuff this thing can do:

  • 4 character classes are defined by default: upper case, lower case, digits and special characters. The classics 🎀
  • more character classes can be defined — just write your own, be free, be creative 💫
  • passwords must match the amount of quality points. A point is validated when at least m characters of the corresponding character class are present in the password.
  • passwords must have at least n of the corresponding character class present, else they are rejected.
  • passwords must have at the most x occurrences of characters from the corresponding character class, else they are rejected.
  • the three previous criteria are checked against any specific character class defined.
  • if a password contains any of the forbidden characters, then it is rejected. Bye 👋
  • if a password contains tokens from the RDN, then it is rejected.
  • if a password contains tokens from defined attributes, then it is rejected.
  • if a password does not pass the cracklib check, then it is rejected.

✨ CONFIGURATION ✨

Since OpenLDAP 2.5, the ppm configuration is held in a binary attribute of the password policy, and that attribute is pwdCheckModuleArg.

The example file (/usr/local/etc/openldap/ppm.example by default) is meant to be treated as an example configuration that you import into the pwdCheckModuleArg attribute. It's also what's used for testing passwords with the test program that's provided.

And if for some reason a parameter isn't found? No stress, it just gets its default value. Slay.

Note: you can still compile ppm to use the configuration file, by enabling PPM_READ_FILE in ppm.h — but ngl, that's deprecated now. If you do decide to go that route anyway, you can use the PPM_CONFIG_FILE environment variable for overloading the configuration file path.

The syntax of a configuration line is:

parameter value [min] [minForPoint] [max]

with spaces being the delimiters, and a Line Feed (LF) ending the line.

Parameter names are case sensitive, so don't get cute with the capitalization bestie.

Lines beginning by a # are considered as comments.

The default configuration is the following:

# minQuality parameter
# Format:
# minQuality [NUMBER]
# Description:
# One point is granted for each class for which MIN_FOR_POINT criteria is fulfilled.
# defines the minimum point numbers for the password to be accepted.
minQuality 3

# checkRDN parameter
# Format:
# checkRDN [0 | 1]
# Description:
# If set to 1, password must not contain a token from the RDN.
# Tokens are separated by the following delimiters : space tabulation _ - , ; £
checkRDN 0

# checkAttributes parameter
# Format:
# checkAttributes [ATTR1,ATTR2,...]
# Description:
# Password must not contain a token from the values in the given list of attributes
# Tokens are substrings of the values of the given attributes,
# delimited by: space tabulation _ - , ; @
# For example, if uid="the wonderful entry",
# password must not contain "the", nor "wonderful", nor "entry"
checkAttributes

# forbiddenChars parameter
# Format:
# forbiddenChars [CHARACTERS_FORBIDDEN]
# Description:
# Defines the forbidden characters list (no separator).
# If one of them is found in the password, then it is rejected.
forbiddenChars

# maxConsecutivePerClass parameter
# Format:
# maxConsecutivePerClass [NUMBER]
# Description:
# Defines the maximum number of consecutive character allowed for any class
maxConsecutivePerClass 0

# useCracklib parameter
# Format:
# useCracklib [0 | 1]
# Description:
# If set to 1, the password must pass the cracklib check
useCracklib 0

# cracklibDict parameter
# Format:
# cracklibDict [path_to_cracklib_dictionary]
# Description:
# directory+filename-prefix that your version of CrackLib will go hunting for
# For example, /var/pw_dict resolves as /var/pw_dict.pwd,
# /var/pw_dict.pwi and /var/pw_dict.hwm dictionary files
cracklibDict /var/cache/cracklib/cracklib_dict

# classes parameter
# Format:
# class-[CLASS_NAME] [CHARACTERS_DEFINING_CLASS] [MIN] [MIN_FOR_POINT] [MAX]
# Description:
# [CHARACTERS_DEFINING_CLASS]: characters defining the class (no separator)
# [MIN]: If at least [MIN] characters of this class is not found in the password, then it is rejected
# [MIN_FOR_POINT]: one point is granted if password contains at least [MIN_FOR_POINT] character numbers of this class
# [MAX]: if > [MAX] occurrences of characters from this class are found, then the password is rejected (0 means no maximum)
class-upperCase ABCDEFGHIJKLMNOPQRSTUVWXYZ 0 1 0
class-lowerCase abcdefghijklmnopqrstuvwxyz 0 1 0
class-digit 0123456789 0 1 0
class-special <>,?;.:/!§ù%*µ^¨$£²&é~"#'{([-|è`_\ç^à@)]°=}+ 0 1 0

✨ EXAMPLE ✨

Okay so imagine this policy, she's a lot but she's doing her best:

minQuality 4
forbiddenChars .?,
checkRDN 1
checkAttributes mail
class-upperCase ABCDEFGHIJKLMNOPQRSTUVWXYZ 0 5 0
class-lowerCase abcdefghijklmnopqrstuvwxyz 0 12 0
class-digit 0123456789 0 1 0
class-special <>,?;.:/!§ù%*µ^¨$£²&é~"#'{([-|è`_\ç^à@)]°=}+ 0 1 0
class-myClass :) 1 1 0

With that policy, the password ThereIsNoCowLevel) is working, and here's why, bestie:

  • it has 4 character classes validated: upper, lower, special, and myClass
  • it has no character among .,? (wait, careful — the forbidden chars here are .? ,)
  • it has at least one character among : or )

But it won't work for the user uid=John Cowlevel,ou=people,cn=example,cn=com, because the token “Cowlevel” from his RDN exists in the password — and that check is case insensitive, so don't try it 🙅

Also, it won't work for a mail attribute containing: “thereis@domain.com”, because the part “thereis” matches the password. Caught. 💀

📋 LOGS

okay so when a user's password gets rejected by ppm, they don't get the whole drama — they get the short version, bestie. 💅

Here's the typical user message that comes from ldappasswd(5):

Result: Constraint violation (19)
Additional info: Password for dn="%s" does not pass required number of strength checks (2 of 3)

But the detailed version? That goes straight to the server log. That's where all the tea gets spilled. ✨

So while you're checking out a password change, you should lowkey see something like this in the logs:

ppm: entry uid=jack.oneill,ou=people,dc=my-domain,dc=com
ppm: Reading pwdCheckModuleArg attribute
ppm: RAW configuration: minQuality 3#012checkRDN 0#012checkAttributes mail,uid#012forbiddenChars#012maxConsecutivePerClass 0#012useCracklib 0#012cracklibDict /var/cache/cracklib/cracklib_dict#012class-upperCase ABCDEFGHIJKLMNOPQRSTUVWXYZ 0 1 0#012class-lowerCase abcdefghijklmnopqrstuvwxyz 0 1 0#012class-digit 0123456789 0 1 0#012class-special <>,?;.:/!§ù%*µ^¨$£²&é~"#'{([-|è`_\ç^à@)]°=}+ 0 1 0
ppm: Parsing pwdCheckModuleArg attribute
ppm: get line: minQuality 3
ppm: Param = minQuality, value = 3, min = (null), minForPoint= (null)
ppm:  Accepted replaced value: 3
ppm: get line: checkRDN 0
ppm: Param = checkRDN, value = 0, min = (null), minForPoint= (null)
ppm:  Accepted replaced value: 0
ppm: get line: checkAttributes mail,uid
ppm: Param = checkAttributes, value = mail,uid, min = (null), minForPoint= (null)
ppm:  Accepted replaced value: mail,uid
ppm: get line: forbiddenChars
ppm: No value, goto next parameter
ppm: get line: maxConsecutivePerClass 0
ppm: Param = maxConsecutivePerClass, value = 0, min = (null), minForPoint= (null)
ppm:  Accepted replaced value: 0
ppm: get line: useCracklib 0
ppm: Param = useCracklib, value = 0, min = (null), minForPoint= (null)
ppm:  Accepted replaced value: 0
ppm: get line: cracklibDict /var/cache/cracklib/cracklib_dict
ppm: Param = cracklibDict, value = /var/cache/cracklib/cracklib_dict, min = (null), minForPoint= (null)
ppm:  Accepted replaced value: /var/cache/cracklib/cracklib_dict
ppm: get line: class-upperCase ABCDEFGHIJKLMNOPQRSTUVWXYZ 0 1 0
ppm: Param = class-upperCase, value = ABCDEFGHIJKLMNOPQRSTUVWXYZ, min = 0, minForPoint = 1, max = 0
ppm:  Accepted replaced value: ABCDEFGHIJKLMNOPQRSTUVWXYZ
ppm: get line: class-lowerCase abcdefghijklmnopqrstuvwxyz 0 1 0
ppm: Param = class-lowerCase, value = abcdefghijklmnopqrstuvwxyz, min = 0, minForPoint = 1, max = 0
ppm:  Accepted replaced value: abcdefghijklmnopqrstuvwxyz
ppm: get line: class-digit 0123456789 0 1 0
ppm: Param = class-digit, value = 0123456789, min = 0, minForPoint = 1, max = 0
ppm:  Accepted replaced value: 0123456789
ppm: get line: class-special <>,?;.:/!§ù%*µ^¨$£²&é~"#'{([-|è`_\ç^à@)]°=}+ 0 1 0
ppm: Param = class-special, value = <>,?;.:/!§ù%*µ^¨$£²&é~"#'{([-|è`_\ç^à@)]°=}+, min = 0, minForPoint = 1, max = 0
ppm:  Accepted replaced value: <>,?;.:/!§ù%*µ^¨$£²&é~"#'{([-|è`_\ç^à@)]°=}+
ppm: 1 point granted for class class-upperCase
ppm: 1 point granted for class class-lowerCase
ppm: Reallocating szErrStr from 64 to 179
check_password_quality: module error: (/usr/local/openldap/libexec/openldap/ppm.so) Password for dn="uid=jack.oneill,ou=people,dc=my-domain,dc=com" does not pass required number of strength checks (2 of 3).[1]

See? Every single param getting parsed and accepted, every point getting granted, and then boom — rejected anyway. It's giving "so close bestie". 😭


🧪 TESTS

There's a unit test script for this, bestie: unit_tests.sh, and it literally shows you some passwords getting checked. Perfect for vibe-checking your config. ✨

And if you wanna test one specific password, you can just hit up the test program directly:

cd /usr/local/lib
LD_LIBRARY_PATH=. ./ppm_test "uid=test,ou=users,dc=my-domain,dc=com" "my_password" "/usr/local/etc/openldap/ppm.example" && echo OK

📁 FILES

  • /usr/local/etc/openldap/ppm.example — an example of ppm configuration, the kind you'd insert into the pwdCheckModuleArg attribute of whatever password policy you're running. 🎀
  • ppm.so — the ppm library itself, fr fr. It gets loaded by the pwdCheckModule attribute of a given password policy (that's OpenLDAP 2.5), or by the ppolicy_check_module / olcPPolicyCheckModule parameters of the ppolicy overlay (that's OpenLDAP 2.6). 👑
  • ppm_test — a small test program for checking a password right from the command line. Iconic for debugging. 💅

👀 SEE ALSO


💖 ACKNOWLEDGEMENTS

This module was developed in 2014-2022 by David Coutadeur. Respect the legend. 🫶


and that's the whole page, bestie! 🎀 this manual is ppm, dated August 24, 2021, and it lives at ppm(5). go forth and make those passwords strong 💪✨ periodt 💅