OpenLDAP does not enforce password expiration, reuse prevention, or failed-login lockout merely because user entries contain userPassword. These controls are provided by the ppolicy overlay, which implements the LDAP Password Policy Internet-Draft rather than a finalized RFC.
This guide explains how to:
- Load and verify the
ppolicymodule on the RHEL family - Attach the overlay to the MDB database
- Create
pwdPolicyentries and assign them in OpenLDAP 2.6 - Enforce minimum length, history, expiration, warnings, and lockout
- Initialize existing users for password aging
- Unlock accounts and force post-reset password changes
- Understand SSSD client behavior and replication caveats
The procedure applies to OpenLDAP 2.6 deployments across Red Hat Enterprise Linux, Rocky Linux, AlmaLinux, Oracle Linux, and CentOS Stream.
Tested on: Rocky Linux 10.2 with
openldap-servers2.6.10-1.el10_2 from EPEL 10.2. VerifiedpwdMinLength: 15, short-password rejection, password-history enforcement, failed-bind lockout,pwdFailureTimedelete protection, delegated-administrator unlock and forced reset (pwdReset), andpwdPolicySubentryassignment on this host after applying the split ACL examples below.
| Lab setting | Value |
|---|---|
| OpenLDAP server | ldap-server.example.com |
| Authentication client | ldap-client.example.com |
| Directory suffix | dc=example,dc=com |
| Users container | ou=people,dc=example,dc=com |
| System accounts container | ou=system,dc=example,dc=com |
| Policies container | ou=policies,dc=example,dc=com |
| Default policy | cn=default,ou=policies,dc=example,dc=com |
| Delegated password administrator | uid=pwdadmin,ou=system,dc=example,dc=com |
| Test user | uid=jdoe,ou=people,dc=example,dc=com |
| Data backend | MDB |
| Configuration system | cn=config |
| LDAP transport | StartTLS on TCP port 389 |
| CA certificate | /etc/openldap/certs/example-ldap-ca.crt |
Complete these guides first:
- Install and configure OpenLDAP on the RHEL family
- Configure OpenLDAP TLS with StartTLS
- OpenLDAP cn=config and MDB explained
- Manage OpenLDAP users and groups with LDIF
- OpenLDAP ACL configuration with practical examples
| Your goal | Go to |
|---|---|
| Understand how ppolicy fits together | How OpenLDAP ppolicy Works |
| Plan policy values before writing LDIF | Plan the Password Policy |
| Load the module and overlay | Load the ppolicy Module and Overlay |
| Create and assign policies | Create and Assign the Default Policy |
| Test length, history, and lockout | Test the Policy |
| Check SSSD behavior | Operate and Monitor ppolicy |
| Fix a policy that appears inactive | Troubleshooting |
How OpenLDAP ppolicy Works
Password binds and password changes pass through the ppolicy overlay before the MDB backend stores the result.
Password bind or password change
|
v
ppolicy overlay
|
+-- Select effective policy
|
+-- Check password age
+-- Check password history
+-- Check password length
+-- Record failed binds
+-- Lock or unlock account
+-- Return warning or grace-login control
|
v
MDB databaseModule, overlay and policy entry
Three layers work together:
| Layer | Purpose |
|---|---|
ppolicy module |
Makes password-policy functionality available to slapd |
ppolicy overlay |
Applies policy behavior to one database |
pwdPolicy entry |
Stores expiration, history, and lockout values |
Important behavior to keep in mind:
- The overlay applies to the database beneath which it is configured.
- A policy entry is normal directory data beneath the application suffix, not part of
cn=config. - Policy controls
userPassword; OpenLDAP does not accept another password attribute forpwdAttribute. - Password-policy operational state is stored on individual user entries.
- The
ppolicyimplementation enforces oneuserPasswordvalue even though the base schema permits multiple values on that attribute.
The ppolicy overlay does not replace ACLs. The overlay evaluates password-policy state during binds and password changes, while ACLs determine who may modify userPassword and protected operational attributes. See OpenLDAP ACL configuration with practical examples for the complete olcAccess policy.
Password-policy components
| Requirement | Policy attribute |
|---|---|
| Minimum time between changes | pwdMinAge |
| Password expiration | pwdMaxAge |
| Previous passwords retained | pwdInHistory |
| Password syntax checking | pwdCheckQuality |
| Minimum password length | pwdMinLength |
| Expiration warning | pwdExpireWarning |
| Grace-login count | pwdGraceAuthnLimit |
| Grace-login time window | pwdGraceExpiry |
| Enable failed-bind lockout | pwdLockout |
| Lockout duration | pwdLockoutDuration |
| Failed attempts before lockout | pwdMaxFailure |
| Failure counter interval | pwdFailureCountInterval |
| Maximum failed-bind timestamps retained | pwdMaxRecordedFailure |
| Force change after admin reset | pwdMustChange |
| Require old password for change | pwdSafeModify |
| Delay after failed bind | pwdMinDelay |
| Maximum delay between binds | pwdMaxDelay |
| Maximum period without successful authentication | pwdMaxIdle |
All time-based policy values are stored in seconds. pwdMaxRecordedFailure limits how many failed-bind timestamps are retained on the entry; it is distinct from pwdMaxFailure, which determines when the account locks.
OpenLDAP also provides advanced attributes such as pwdMinDelay, pwdMaxDelay, and pwdMaxIdle. They are not enabled in this guide. pwdMinDelay and pwdMaxDelay implement an increasing temporary lockout after failed authentication attempts rather than delaying the bind response itself. pwdMaxIdle can lock an account after an extended period without successful authentication, but it requires last-bind tracking to be enabled on the database with olcLastBind: TRUE.
Default and per-account policy selection in OpenLDAP 2.6
OpenLDAP 2.6 documents two policy-selection methods:
| Method | Where it is configured | When it applies |
|---|---|---|
olcPPolicyDefault |
On the ppolicy overlay entry in cn=config |
When the account has no pwdPolicySubentry value |
pwdPolicySubentry |
On the user or service account entry | When that account needs a different policy than the default |
The effective policy for an account is either its pwdPolicySubentry value or the policy named by olcPPolicyDefault on the overlay.
ppolicy_rules, olcPPolicyScopedRule, and related object classes). Do not apply development-version rule examples to an OpenLDAP 2.6 server unless the installed schema explicitly provides those object classes.
Plan the Password Policy
Decide policy values before you write LDIF. The examples below are for lab testing and compliance-style demonstrations, not universal security mandates.
Modern rotation and lockout guidance
Current NIST digital-identity guidance in SP 800-63B-4 advises against mandatory periodic password changes unless there is evidence that a password has been compromised. It also recommends at least 15 characters when a password is used as a single authentication factor, and at least 8 characters when the password is used only as part of multi-factor authentication. Many modern deployments set pwdMaxAge: 0 and require changes after suspected compromise, administrator reset, or another risk event. Use a fixed rotation period only when your regulatory or organizational policy requires it.
Failed-bind lockout can protect against repeated guessing, but it can also let an attacker deliberately lock known accounts. The Password Policy draft explicitly identifies denial-of-service risk in account-lockout designs. Use a temporary lockout duration, monitoring, rate limiting at the application or network layer, and a documented unlock procedure. Test service and privileged accounts separately.
Example lab and compliance-style policy
Two profiles help separate everyday policy from expiration testing:
| Profile | pwdMaxAge |
Purpose |
|---|---|---|
| Modern general baseline | 0 |
No forced periodic rotation |
| Expiration demonstration | 7776000 (90 days) |
Test expiry, warnings, and grace authentication |
The expiration demonstration profile uses these values:
| Requirement | Example value | Seconds or count |
|---|---|---|
| Minimum password length | 15 bytes | 15 |
| Maximum password age | 90 days | 7776000 |
| Expiration warning | 14 days | 1209600 |
| Password history | 5 passwords | 5 |
| Failed binds before lockout | 5 | 5 |
| Failure-count window | 15 minutes | 900 |
| Temporary lockout | 15 minutes | 900 |
| Maximum recorded failures | 5 | 5 |
| Grace logins after expiry | 2 | 2 |
| Grace-login validity | 7 days | 604800 |
| Force change after admin reset | Yes | TRUE |
| Minimum time between changes | None in baseline | 0 |
| Require old password in change request | No in baseline | FALSE |
pwdAttribute: userPassword
pwdCheckQuality: 2
pwdMinLength: 15
pwdMinAge: 0
pwdMaxAge: 7776000
pwdExpireWarning: 1209600
pwdInHistory: 5
pwdLockout: TRUE
pwdMaxFailure: 5
pwdMaxRecordedFailure: 5
pwdFailureCountInterval: 900
pwdLockoutDuration: 900
pwdGraceAuthnLimit: 2
pwdGraceExpiry: 604800
pwdMustChange: TRUE
pwdSafeModify: FALSEpwdMaxAge: 7776000 is included to demonstrate expiration, warnings, and grace authentication. It is not a universal security recommendation.
Interactions worth planning for:
pwdMaxAge: 0means passwords do not expire.pwdLockoutDuration: 0creates a lock that remains until an administrator intervenes.- A temporary lockout is safer for the first implementation than an indefinite lock.
pwdGraceAuthnLimit: 0blocks authentication immediately after expiry.pwdExpireWarningis useful only when the client requests and displays the Password Policy Request control.pwdSafeModify: TRUEcan break clients that do not include the old password in the Password Modify request.
pwdMinLength measures the password in bytes. It does not by itself enforce uppercase, lowercase, digits, special characters, or dictionary checks. Full quality checking requires a compatible external check_password() module, covered briefly under Operate and Monitor ppolicy.
This example uses a 15-byte minimum to align more closely with current NIST guidance for single-factor passwords. OpenLDAP counts bytes rather than Unicode characters, so pwdMinLength alone does not provide exact character-based NIST compliance for non-ASCII passwords.
Verify the Package, Module and Schema
On Rocky Linux 10.2, the EPEL openldap-servers package ships ppolicy.so and check_password.so. It does not ship a standalone /etc/openldap/schema/ppolicy.ldif file like some older distributions.
Confirm the packaged module paths:
rpm -ql openldap-servers | grep -E '/(ppolicy|check_password)(\.so|\.conf)'Sample output:
/etc/openldap/check_password.conf
/usr/lib64/openldap/check_password.so
/usr/lib64/openldap/check_password.so.1.1
/usr/lib64/openldap/ppolicy.so
/usr/lib64/openldap/ppolicy.so.2
/usr/lib64/openldap/ppolicy.so.2.0.200Check whether ppolicy is already loaded:
sudo ldapsearch -Q -LLL -Y EXTERNAL -H ldapi:/// -b cn=config '(&(objectClass=olcModuleList)(olcModuleLoad=*ppolicy*))' dn olcModulePath olcModuleLoadWhen ppolicy.so is not loaded, list all module entries and identify the DN whose olcModulePath matches the package's module directory:
sudo ldapsearch -Q -LLL -Y EXTERNAL -H ldapi:/// -b cn=config '(objectClass=olcModuleList)' dn olcModulePath olcModuleLoadLoading ppolicy.so registers the pwdPolicy schema dynamically. Verify policy attributes in cn=schema,cn=config:
sudo ldapsearch -Q -LLL -Y EXTERNAL -H ldapi:/// -b cn=schema,cn=config -s base '(objectClass=*)' olcObjectClasses olcAttributeTypes | grep -E 'pwdPolicy|pwdMaxAge|pwdAccountLockedTime'The pwdPolicy auxiliary object class should appear after the module loads.
Confirm the conventional schema file is absent before copying old documentation:
ls -l /etc/openldap/schema/ppolicy.ldifSample output on Rocky Linux 10.2:
ls: cannot access '/etc/openldap/schema/ppolicy.ldif': No such file or directoryDo not blindly run ldapadd -f /etc/openldap/schema/ppolicy.ldif unless that file exists on your host.
Back Up and Discover the MDB Database
Back up cn=config before loading modules, adding overlays, or changing policy assignment. See OpenLDAP backup and restore for recurring backup design.
Become root for protected backup paths, or save exports into a directory your current user can write:
sudo install -d -m 0700 /root/openldap-ppolicy-backupExport the current dynamic configuration:
sudo slapcat -n 0 -l /root/openldap-ppolicy-backup/cn-config-before-ppolicy.ldifShell redirection to /root requires root privileges even when ldapsearch itself runs as an LDAP identity. sudo commonly starts with a sanitized environment, so set LDAPTLS_CACERT inside the root shell:
sudo sh -c '
LDAPTLS_CACERT=/etc/openldap/certs/example-ldap-ca.crt \
ldapsearch -x -ZZ \
-H ldap://ldap-server.example.com \
-D "cn=admin,dc=example,dc=com" -W \
-b "uid=jdoe,ou=people,dc=example,dc=com" \
-s base -LLL "*" "+" \
> /root/openldap-ppolicy-backup/jdoe-before.ldif
'Enter the directory administrator password at the -W prompt inside the root shell.
Discover the MDB database DN dynamically:
MDB_DN=$(sudo ldapsearch -Q -LLL -Y EXTERNAL -H ldapi:/// -b cn=config '(&(objectClass=olcMdbConfig)(olcSuffix=dc=example,dc=com))' dn | awk '/^dn: / {sub(/^dn: /, ""); print; exit}')Fail safely when the suffix is missing:
if [[ -z "$MDB_DN" ]]; then echo "MDB database for dc=example,dc=com was not found." >&2; exit 1; fiprintf 'MDB database DN: %s\n' "$MDB_DN"Sample output:
MDB database DN: olcDatabase={2}mdb,cn=configDatabase index numbers vary by host. Always discover MDB_DN on the system you are changing.
Load the ppolicy Module and Add the Overlay
Module and overlay changes use the local ldapi:/// socket with SASL EXTERNAL. See OpenLDAP cn=config and MDB explained and the ldapmodify cheat sheet.
If ppolicy.so is not yet listed, identify the module-list entry from the verification step in the previous section. On the lab host the entry was cn=module{0},cn=config; set MODULE_DN to the exact DN whose olcModulePath matches the package module directory you verified:
MODULE_DN='cn=module{0},cn=config'Alternatively, filter by module path:
MODULE_DN=$(sudo ldapsearch -Q -LLL -Y EXTERNAL -H ldapi:/// -b cn=config '(&(objectClass=olcModuleList)(olcModulePath=/usr/lib64/openldap))' dn | awk '/^dn: / {sub(/^dn: /, ""); print; exit}')
printf 'Module list DN: %s\n' "$MODULE_DN"Generate load-ppolicy-module.ldif from the verified MODULE_DN:
cat > load-ppolicy-module.ldif <<EOF
dn: ${MODULE_DN}
changetype: modify
add: olcModuleLoad
olcModuleLoad: ppolicy.so
EOFApply the module load:
sudo ldapmodify -Q -Y EXTERNAL -H ldapi:/// \
-f load-ppolicy-module.ldifAdd the overlay without setting a default policy yet. Generate the LDIF from the discovered MDB_DN:
cat > add-ppolicy-overlay.ldif <<EOF
dn: olcOverlay=ppolicy,${MDB_DN}
objectClass: olcOverlayConfig
objectClass: olcPPolicyConfig
olcOverlay: ppolicy
EOFsudo ldapadd -Q -Y EXTERNAL -H ldapi:/// -f add-ppolicy-overlay.ldifDiscover the numbered overlay DN OpenLDAP assigned:
PPOLICY_DN=$(sudo ldapsearch -Q -LLL -Y EXTERNAL -H ldapi:/// -b "$MDB_DN" '(olcOverlay=ppolicy)' dn | awk '/^dn: / {sub(/^dn: /, ""); print; exit}')
printf 'ppolicy overlay DN: %s\n' "$PPOLICY_DN"Sample output:
ppolicy overlay DN: olcOverlay={3}ppolicy,olcDatabase={2}mdb,cn=configOverlay index numbers vary by host, like MDB database indices.
Create and Assign the Default Policy
Policy entries live in the application suffix, not in cn=config.
dc=example,dc=com
└── ou=policies
├── cn=default
├── cn=service-accounts
└── cn=privileged-usersCreate the policy entry
pwdPolicy is an auxiliary object class, so the entry also requires a structural class. Some installations load the optional namedPolicy schema; upstream OpenLDAP 2.6 still describes namedPolicy as a suitable structural class when that schema is present. This tested Rocky Linux configuration did not have namedPolicy loaded after enabling ppolicy.so, so the guide uses the standard organizationalRole structural class instead.
Create default-password-policy.ldif:
dn: ou=policies,dc=example,dc=com
objectClass: top
objectClass: organizationalUnit
ou: policies
dn: cn=default,ou=policies,dc=example,dc=com
objectClass: top
objectClass: organizationalRole
objectClass: pwdPolicy
cn: default
pwdAttribute: userPassword
pwdCheckQuality: 2
pwdMinLength: 15
pwdMinAge: 0
pwdMaxAge: 7776000
pwdExpireWarning: 1209600
pwdInHistory: 5
pwdLockout: TRUE
pwdMaxFailure: 5
pwdMaxRecordedFailure: 5
pwdFailureCountInterval: 900
pwdLockoutDuration: 900
pwdGraceAuthnLimit: 2
pwdGraceExpiry: 604800
pwdMustChange: TRUE
pwdSafeModify: FALSEAdd the policy data over StartTLS:
ldapadd -x -ZZ -H ldap://ldap-server.example.com -D "cn=admin,dc=example,dc=com" -W -f default-password-policy.ldifConfigure olcPPolicyDefault
Assign the default policy on the overlay entry using the discovered PPOLICY_DN:
cat > set-ppolicy-default.ldif <<EOF
dn: ${PPOLICY_DN}
changetype: modify
add: olcPPolicyDefault
olcPPolicyDefault: cn=default,ou=policies,dc=example,dc=com
EOFsudo ldapmodify -Q -Y EXTERNAL -H ldapi:/// -f set-ppolicy-default.ldifVerify the assignment:
sudo ldapsearch -Q -LLL -Y EXTERNAL -H ldapi:/// -b "$PPOLICY_DN" -s base olcOverlay olcPPolicyDefaultSample output:
dn: olcOverlay={3}ppolicy,olcDatabase={2}mdb,cn=config
olcOverlay: {3}ppolicy
olcPPolicyDefault: cn=default,ou=policies,dc=example,dc=comAccounts without pwdPolicySubentry now inherit cn=default,ou=policies,dc=example,dc=com.
Assign a different policy with pwdPolicySubentry
Create additional pwdPolicy entries when service accounts or privileged users need different limits. For example, add cn=service-accounts,ou=policies,dc=example,dc=com with a longer pwdMaxAge or pwdMaxAge: 0 for non-expiring service passwords.
Point one account at the alternate policy. Save assign-service-account-policy.ldif:
dn: uid=svcbackup,ou=people,dc=example,dc=com
changetype: modify
add: pwdPolicySubentry
pwdPolicySubentry: cn=service-accounts,ou=policies,dc=example,dc=compwdPolicySubentry is an operational attribute. The delegated password administrator ACL in this guide grants it only read, which is appropriate for monitoring, but that identity cannot assign the override. Use the database administrator or another identity explicitly granted sufficient management access. OpenLDAP uses this attribute as the direct per-account policy pointer, falling back to the configured default when it is absent.
ldapmodify -x -ZZ \
-H ldap://ldap-server.example.com \
-D "cn=admin,dc=example,dc=com" -W \
-f assign-service-account-policy.ldifVerify the override:
ldapsearch -x -ZZ \
-H ldap://ldap-server.example.com \
-D "cn=admin,dc=example,dc=com" -W \
-b "uid=svcbackup,ou=people,dc=example,dc=com" \
-s base -LLL pwdPolicySubentry pwdChangedTimeSample output:
dn: uid=svcbackup,ou=people,dc=example,dc=com
pwdChangedTime: 20260714135354Z
pwdPolicySubentry: cn=service-accounts,ou=policies,dc=example,dc=comUsing the database administrator here is appropriate for assigning directory policy metadata. The root-DN caveat applies to testing password history, quality, and reset behavior—not to assigning pwdPolicySubentry.
Per-entry assignment is supported in OpenLDAP 2.6, but administrators must maintain pwdPolicySubentry during account creation, migration, and auditing. Document which accounts override the default and review those attributes during access reviews.
Configure Password-Policy ACLs
ACL design belongs in OpenLDAP ACL configuration with practical examples. This section lists the minimum identities password policy depends on.
| Identity | Intended use |
|---|---|
| Database root DN | Emergency recovery and configuration |
| Delegated password administrator | Password resets, unlocks, forced-change workflow |
| User DN | Normal password change and history testing |
Normal user
Recommended direction for userPassword:
to attrs=userPassword
by self =xw
by anonymous auth
by * noneDelegated password administrator
Create a dedicated account such as uid=pwdadmin,ou=system,dc=example,dc=com. ACL permissions apply to every attribute named in a to attrs= clause, so do not combine userPassword with lockout or reset attributes in the same self-service rule. A user granted by self =xw on that combined target could modify their own pwdAccountLockedTime or pwdReset.
Use separate ACL examples:
to attrs=userPassword
by dn.exact="uid=pwdadmin,ou=system,dc=example,dc=com" manage
by self =xw
by anonymous auth
by * none
to attrs=pwdAccountLockedTime,pwdReset
by dn.exact="uid=pwdadmin,ou=system,dc=example,dc=com" manage
by * none
to attrs=pwdFailureTime,pwdChangedTime,pwdGraceUseTime,pwdPolicySubentry
by dn.exact="uid=pwdadmin,ou=system,dc=example,dc=com" read
by * none
to dn.subtree="ou=people,dc=example,dc=com" attrs=entry,uid
by dn.exact="uid=pwdadmin,ou=system,dc=example,dc=com" read
by * breakKeep pwdHistory protected. A delegated reset administrator normally does not need to read its stored password representations.
Apply the split ACL examples—or equivalent rules from OpenLDAP ACL configuration with practical examples—before testing uid=pwdadmin commands later in this guide. The final rule gives the delegated administrator enough access to locate user entries and read uid during monitoring searches. Password reset, lockout management, and operational-attribute access remain controlled by the preceding attribute-specific rules.
The exact ordered olcAccess policy remains in the ACL guide. The ppolicy overlay treats manage on userPassword as a password-administrator identity for pwdMustChange and pwdReset handling.
Root-DN caveat
Operations performed as cn=admin,dc=example,dc=com when that DN is the database rootDN can bypass password-history enforcement and may not set pwdReset the way a delegated password administrator does. Use the root DN only for break-glass recovery. Test policy behavior with uid=jdoe or uid=pwdadmin.
Initialize Existing Accounts
The overlay uses pwdChangedTime to calculate password age. If a user already has userPassword but no pwdChangedTime, that password does not expire merely because pwdMaxAge was enabled.
Inspect the test user:
ldapsearch -x -ZZ -H ldap://ldap-server.example.com -D "cn=admin,dc=example,dc=com" -W -b "uid=jdoe,ou=people,dc=example,dc=com" -s base -LLL pwdChangedTime pwdPolicySubentrySample output when the account inherits the default policy:
dn: uid=jdoe,ou=people,dc=example,dc=com
pwdChangedTime: 20260714135332ZInitialize policy state through a controlled password change as the user:
ldappasswd -x -ZZ -H ldap://ldap-server.example.com -D "uid=jdoe,ou=people,dc=example,dc=com" -W -A -S-A securely prompts for the old password, while -S prompts for the new password instead of exposing either value in shell history. The Password Modify extended operation lets slapd inspect and hash the cleartext password over the encrypted StartTLS session.
Because the user is also the bind identity, the current password may be requested once for the bind and again as the old password in the Password Modify operation. With pwdSafeModify: FALSE, supplying -A is optional, but it is retained here to test an explicit old-password workflow. The OpenLDAP command documentation distinguishes -W (bind password), -A (old password in the Password Modify request), and -S (new password).
Do not initialize users by manually fabricating pwdChangedTime in LDIF. For a large existing directory, plan user communication and a staged reset process before enabling a short pwdMaxAge.
Test the Policy
Test enforcement using the user's own identity or the delegated password administrator. The database root DN is not a reliable policy test bind.
Length and history
Attempt a password shorter than pwdMinLength:
ldappasswd -x -ZZ -H ldap://ldap-server.example.com -D "uid=jdoe,ou=people,dc=example,dc=com" -W -A -SSample output:
Result: Constraint violation (19)
Additional info: Password fails quality checking policyCycle through several passwords as the user, then attempt to reuse an older value. When history blocks reuse:
Result: Constraint violation (19)
Additional info: Password is in history of old passwordsWith pwdCheckQuality: 2, the server rejects passwords it cannot inspect. Use Password Modify through ldappasswd, not pre-hashed values from slappasswd in an ldapmodify change.
Resetting an old password as the database root DN may succeed even when the same password is rejected during a normal user-initiated change. Document both results so future validation uses the identity your users actually employ.
Expiration and warnings
In a disposable lab, temporarily lower pwdMaxAge and pwdExpireWarning on a test policy entry—for example 300 and 180 seconds—then restore production values after testing.
Request the password-policy control:
ldapsearch -x -ZZ -e ppolicy -H ldap://ldap-server.example.com -D "uid=jdoe,ou=people,dc=example,dc=com" -W -b "uid=jdoe,ou=people,dc=example,dc=com" -s base -LLL uidThe server sends an expiration warning only when the client requests the Password Policy Request control.
Grace authentication
After expiry, bind with -e ppolicy using ldapwhoami, record grace-login information from the policy response, and query pwdGraceUseTime with an administrative identity. Each successful expired-password bind consumes a grace login until the configured limit is exhausted.
Failed-bind lockout
Perform several intentionally incorrect binds as the user:
ldapwhoami -x -ZZ -e ppolicy -H ldap://ldap-server.example.com -D "uid=jdoe,ou=people,dc=example,dc=com" -WAfter enough failures, inspect policy state:
ldapsearch -x -ZZ -H ldap://ldap-server.example.com -D "uid=pwdadmin,ou=system,dc=example,dc=com" -W -b "uid=jdoe,ou=people,dc=example,dc=com" -s base -LLL pwdFailureTime pwdAccountLockedTimeSample output after five failed binds in the lab:
dn: uid=jdoe,ou=people,dc=example,dc=com
pwdFailureTime: 20260714135709.919486Z
pwdFailureTime: 20260714135709.951901Z
pwdFailureTime: 20260714135709.982220Z
pwdFailureTime: 20260714135710.042172Z
pwdFailureTime: 20260714135710.096818Z
pwdAccountLockedTime: 20260714135710ZWhile locked, even the correct password returns ldap_bind: Invalid credentials (49) unless you deliberately enable explicit lockout responses on the overlay.
Forced change after administrator reset
With pwdMustChange: TRUE, reset the password with the delegated administrator:
ldappasswd -x -ZZ -H ldap://ldap-server.example.com -D "uid=pwdadmin,ou=system,dc=example,dc=com" -W -S "uid=jdoe,ou=people,dc=example,dc=com"Verify reset state:
ldapsearch -x -ZZ -H ldap://ldap-server.example.com -D "uid=pwdadmin,ou=system,dc=example,dc=com" -W -b "uid=jdoe,ou=people,dc=example,dc=com" -s base -LLL pwdReset pwdChangedTimeSample output:
dn: uid=jdoe,ou=people,dc=example,dc=com
pwdChangedTime: 20260714135710Z
pwdReset: TRUEThe user should change the temporary password with ldappasswd -A -S through a compatible client. Test client support on SSSD OpenLDAP clients.
Operate and Monitor ppolicy
Unlock and administratively disable accounts
Remove the lock timestamp with the delegated password administrator:
dn: uid=jdoe,ou=people,dc=example,dc=com
changetype: modify
delete: pwdAccountLockedTimeldapmodify -x -ZZ -H ldap://ldap-server.example.com -D "uid=pwdadmin,ou=system,dc=example,dc=com" -W -f unlock-jdoe.ldifSample output:
modifying entry "uid=jdoe,ou=people,dc=example,dc=com"After deleting pwdAccountLockedTime, inspect the remaining pwdFailureTime values before returning the account to service. Old failures normally age out according to pwdFailureCountInterval, but a remaining near-threshold failure state can cause the account to lock again after another failed bind. Test the unlock against the same replica or load-balanced endpoint users normally authenticate through.
Attempting to delete pwdFailureTime directly often fails:
ldap_modify: Constraint violation (19)
additional info: pwdFailureTime: no user modification allowedFor permanent administrative disable, OpenLDAP recognizes:
pwdAccountLockedTime: 000001010000ZThe permanent-lock sentinel is enforced only when the account's effective password policy has pwdLockout: TRUE.
Use that sentinel only with a documented unlock process.
| Condition | Controlled by |
|---|---|
| Password too old | pwdMaxAge |
| Too many failed binds | pwdLockout, pwdMaxFailure |
| Administrative permanent lock | pwdAccountLockedTime sentinel value |
| Account valid only within dates | pwdStartTime, pwdEndTime |
Search operational attributes
Request operational attributes explicitly. A normal * search does not return them to unprivileged clients.
ldapsearch -x -ZZ -H ldap://ldap-server.example.com -D "uid=pwdadmin,ou=system,dc=example,dc=com" -W -b "ou=people,dc=example,dc=com" -LLL "(pwdAccountLockedTime=*)" dn uid pwdAccountLockedTime| Operational attribute | Meaning |
|---|---|
pwdPolicySubentry |
Per-account policy override DN; it may be absent when the account inherits olcPPolicyDefault |
pwdChangedTime |
Last server-recorded password change |
pwdFailureTime |
Failed-bind timestamps |
pwdAccountLockedTime |
Account lock timestamp |
pwdHistory |
Previously used password values |
pwdGraceUseTime |
Expired-password grace logins consumed |
pwdReset |
Administrator reset requires user change |
OpenLDAP 2.6 selects the per-entry policy when pwdPolicySubentry exists and otherwise uses the overlay's default policy.
Administrative monitoring searches for locked accounts, accounts with pwdFailureTime, and accounts with pwdReset=TRUE should run over StartTLS and remain restricted to delegated operators.
SSSD behavior
The OpenLDAP SSSD client guide owns complete NSS, PAM, and SSH setup. On current SSSD releases, these defaults already match the intended password-policy workflow:
ldap_pwmodify_modedefaults toexopldap_use_ppolicydefaults totrueldap_pwd_policydefaults tonone, with server-side ppolicy taking precedence when enabled
Set them explicitly in sssd.conf to document intent:
auth_provider = ldap
chpass_provider = ldap
ldap_uri = ldap://ldap-server.example.com
ldap_id_use_start_tls = true
# Current defaults, but explicit here for review
ldap_pwmodify_mode = exop
ldap_use_ppolicy = trueWhen grace logins are enabled, consider:
ldap_ppolicy_pwd_change_threshold = 2A value of 2 is the practical minimum because password-change processing may consume additional grace authentications.
| Server condition | Direct bind | SSSD/PAM login |
|---|---|---|
| Password valid | Succeeds | Login succeeds |
| Password nearing expiry | Warning only with ppolicy control | Warning depends on PAM/application display |
| Password expired with grace remaining | Bind may succeed with policy response | Client may prompt for password change |
| Grace logins exhausted | Bind fails | Login fails or password-change flow may fail |
| Account temporarily locked | Invalid credentials | Login denied |
Administrator reset with pwdMustChange |
Policy response requires change | Compatible PAM stack prompts for a new password |
| Client offline | Server is not contacted | Cached authentication behavior applies |
SSSD can cache credentials for offline authentication. A newly imposed server-side lockout or expiration cannot be evaluated while the client is disconnected from the LDAP server.
Replication caveats
Password-policy operational attributes live on user entries. The draft does not fully define their behavior in replicated deployments. A failed authentication attempt against a consumer may update only that consumer's local state, while provider-originated changes may later overwrite it.
The overlay provides ppolicy_forward_updates for forwarding bind-generated policy state from a consumer to a provider; that path requires additional updateref and chain-overlay configuration documented in OpenLDAP provider-consumer replication. Multi-provider designs should account for lockout state recorded on different nodes and for load balancers distributing attempts across providers. See OpenLDAP multi-provider replication.
Include policy entries and overlay settings in OpenLDAP backup and restore.
Optional stronger password quality
The EPEL package includes check_password.so, but external password-quality configuration is module- and package-specific. This guide tests only the built-in pwdMinLength behavior. Configure and validate olcPPolicyCheckModule, pwdPolicyChecker, and pwdUseCheckModule separately before enabling it in production.
Troubleshooting
| Error or symptom | Likely cause | Recommended action |
|---|---|---|
| Policy entry cannot be added | pwdPolicy schema unavailable or structural class missing |
Verify ppolicy.so is loaded; use organizationalRole + pwdPolicy when optional namedPolicy schema is not installed |
ppolicy.so cannot be loaded |
Wrong module path or package missing | Run rpm -ql openldap-servers and confirm olcModulePath |
| Policy exists but is not enforced | No olcPPolicyDefault and no pwdPolicySubentry on the account |
Verify overlay assignment and per-entry overrides |
| Password never expires | User lacks pwdChangedTime |
Perform a controlled user password change to initialize aging |
| Short password is accepted | Test used root DN, quality checking disabled, or password was pre-hashed | Test with a normal identity and pwdCheckQuality: 2 through ldappasswd -A -S |
| Pre-hashed password is rejected | Server cannot inspect hash at quality level 2 | Use Password Modify exop over TLS |
| Password history appears ineffective | Password was reset by root DN | Test with the user or delegated password administrator |
| Account does not lock | pwdLockout is false or binds hit different DNs/providers |
Inspect policy values and pwdFailureTime |
| Account locks but user sees only invalid credentials | Expected information-hiding behavior | Request ppolicy control for administrative testing |
| Account locks again immediately after unlock | Stale pwdFailureTime values remain near threshold |
Inspect failure timestamps and pwdFailureCountInterval; test on the same replica users normally use |
| Lockout never clears | pwdLockoutDuration is zero |
Unlock administratively or set a temporary duration |
| Grace warning is not displayed | Client did not request or render the ppolicy control | Test with -e ppolicy and SSSD/PAM |
pwdReset does not appear |
Reset used root DN or insufficient ACL level | Use uid=pwdadmin or another delegated password administrator |
| SSSD login fails with access rules | ldap_pwdlockout_dn misconfigured for access_provider |
Check ldap_pwdlockout_dn only when the domain uses access_provider = ldap with a ppolicy or lockout access rule; it is separate from normal server-side bind enforcement |
| Lockout differs between replicas | Operational state updated independently on each node | Review topology and ppolicy_forward_updates |
| Policy works online but offline login succeeds | SSSD is using cached credentials | Review offline credential policy and cache expiry |
Useful diagnostics:
sudo ldapsearch -Q -LLL -Y EXTERNAL -H ldapi:/// -b "$PPOLICY_DN" -s base olcOverlay olcPPolicyDefaultConfirms the ppolicy overlay is loaded and which policy DN is the default.
sudo slaptest -uValidates configuration after overlay or policy attribute changes.
sudo journalctl -u slapd -n 200 --no-pagerShows bind failures, lockout events, and ppolicy responses on the server.
sudo journalctl -u sssd -n 200 --no-pagerUse on the client when LDAP policy works but SSSD login or grace warnings fail.
References
- NIST SP 800-63B-4 — Digital Identity Guidelines: Authentication and Authenticator Management
- OpenLDAP 2.6 Administrator's Guide — Overlays (Password Policies section)
- slapo-ppolicy(5)
- slapd-config(5)
- ldappasswd(1)
- ldapsearch(1)
- ldapmodify(1)
- ldapwhoami(1)
- sssd-ldap(5) — upstream SSSD source
- draft-behera-ldap-password-policy-11
Summary
You can enforce OpenLDAP password expiration, history, length, lockout, grace logins, and forced resets on OpenLDAP 2.6 by loading ppolicy.so, attaching the overlay to the discovered MDB database, creating pwdPolicy entries, assigning the default with olcPPolicyDefault, and overriding individual accounts with pwdPolicySubentry when needed.
Initialize existing users with a real password change so pwdChangedTime is recorded, test enforcement with normal user identities or a delegated password administrator rather than the database root DN, protect password operations with StartTLS, align ACLs with overlay operational attributes, and validate SSSD and replication behavior before relying on lockout or expiry in production.

