Configure Self-service Password and Profile ACIs in 389 Directory Server

Configure 389 Directory Server self-service ACIs with userdn ldap:///self for profile read and write, TLS-protected userPassword changes, selfwrite for opt-in group membership, and verification as the real bind user.

Published

Updated

Read time 12 min read

Reviewed byDeepak Prasad

389 Directory Server self-service ACI letting an LDAP user read and update their own profile attributes and change password over TLS

Self-service ACIs let authenticated users read and update their own directory entry without Directory Manager access. A typical policy:

  • View selected profile attributes on the bind user's entry
  • Update safe contact and display fields
  • Change userPassword over a protected connection
  • Opt in or out of low-risk groups using selfwrite
  • Block changes to identity, authorization, and administrative attributes

Directory Server supports userdn="ldap:///self" when the bind identity targets the user's own entry, and selfwrite when users may add or remove only their own DN from a DN-valued attribute such as member.

This guide builds on ACI examples and complements advanced ACI and delegated administration.

Before you start:

IMPORTANT
This guide covers self-service read, profile write, password change, and opt-in group membership on a user's own entry. It does not cover delegated administration of other users, advanced userattr bind rules, macro ACIs, or detailed Get Effective Rights analysis. See the dedicated chapters linked above for those topics.

Tested on: Rocky Linux 10.2; 389 Directory Server 3.2.0.

The plain ldap://localhost simple-bind commands in this guide are for an isolated local lab. Use StartTLS or LDAPS when connecting across a network. An unencrypted simple bind exposes credentials in transit. Red Hat Directory Server allows password binds over unencrypted connections by default and identifies this as a security risk.


How self-service ACIs work

text
Authenticated user DN
        |
        | userdn="ldap:///self"
        v
The user's own LDAP entry
ACI expression Purpose
userdn="ldap:///self" Match the bind user's own entry
allow (read,search,compare) Return permitted attributes in search results
allow (write) Modify permitted attribute values
allow (selfwrite) Add or remove only the bind user's own DN from a DN-valued attribute
ssf>="128" Require a sufficiently protected connection for sensitive operations

Place self-service ACIs on a container such as ou=People,dc=example,dc=com so they inherit to user entries below. Use an explicit targetattr allow list. Never grant write on * and try to exclude sensitive attributes afterward.


Prepare the test user

This lab uses uid=user1,ou=people,dc=example,dc=com on instance ldap1, suffix dc=example,dc=com.

Set a known password and store it in a root-owned file. Omit the password argument so dsidm prompts interactively. The value is not written to shell history:

bash
dsidm ldap1 account reset_password "uid=user1,ou=people,dc=example,dc=com"

Enter and confirm the same password at the dsidm prompts.

Sample output:

output
reset password for uid=user1,ou=people,dc=example,dc=com

I'll save that password for bind tests without putting it on the command line, then set umask 077 before creating the password file:

bash
umask 077
read -rsp 'Enter the user1 password: ' USER1_PW
echo
printf '%s\n' "$USER1_PW" > /root/user1.pw
unset USER1_PW

Confirm the account binds:

bash
ldapwhoami -x -H ldap://localhost:389 -D "uid=user1,ou=people,dc=example,dc=com" -y /root/user1.pw

Sample output:

output
dn: uid=user1,ou=people,dc=example,dc=com

Before you add self-service rules, list existing ACIs on the People container with ldapsearch:

bash
ldapsearch -LLL -o ldif-wrap=no -x -H ldap://localhost:389 -D "cn=Directory Manager" -y /root/dm.pw -b "ou=people,dc=example,dc=com" -s base "(objectClass=*)" aci

Record any inherited rules on dc=example,dc=com as well. Those rules can grant or deny access alongside new self-service ACIs.


Allow users to read their own entries

I'll add a read rule with userdn="ldap:///self". Include objectClass in targetattr with search permission so base-scope searches using (objectClass=*) can match the entry.

Save the following as /tmp/self-read.ldif:

ldif
dn: ou=People,dc=example,dc=com
changetype: modify
add: aci
aci: (targetattr="uid || cn || sn || mail || telephoneNumber || mobile || displayName || preferredLanguage || postalAddress || objectClass")(version 3.0; acl "Self read profile attributes"; allow (read,search,compare) userdn="ldap:///self";)

Apply as Directory Manager with ldapmodify:

bash
ldapmodify -x -H ldap://localhost:389 -D "cn=Directory Manager" -y /root/dm.pw -f /tmp/self-read.ldif

Search the user's own entry. Pass (objectClass=*) as the filter, then list the attributes to return:

bash
ldapsearch -LLL -x -H ldap://localhost:389 -D "uid=user1,ou=people,dc=example,dc=com" -y /root/user1.pw -b "uid=user1,ou=people,dc=example,dc=com" -s base "(objectClass=*)" uid cn mail

Sample output:

output
dn: uid=user1,ou=people,dc=example,dc=com
uid: user1
cn: User One
mail: [email protected]

user1 sees only attributes listed in targetattr. I'll search for another user's entry to confirm the self-read rule does not leak across accounts:

bash
ldapsearch -LLL -x -H ldap://localhost:389 -D "uid=user1,ou=people,dc=example,dc=com" -y /root/user1.pw -b "uid=user2,ou=people,dc=example,dc=com" -s base "(objectClass=*)" mail

With only the self-read ACI configured, the search returns no entry for user2. The ldap:///self bind rule does not apply when the target DN belongs to another user.


Allow users to update safe profile attributes

Grant write only on contact and display fields.

Save the following as /tmp/self-write-profile.ldif:

ldif
dn: ou=People,dc=example,dc=com
changetype: modify
add: aci
aci: (targetattr="mail || telephoneNumber || mobile || displayName || preferredLanguage || postalAddress")(version 3.0; acl "Self write profile attributes"; allow (write) userdn="ldap:///self";)

Apply as Directory Manager:

bash
ldapmodify -x -H ldap://localhost:389 -D "cn=Directory Manager" -y /root/dm.pw -f /tmp/self-write-profile.ldif

Update mail as user1. Save the following as /tmp/user1-mail.ldif:

ldif
dn: uid=user1,ou=people,dc=example,dc=com
changetype: modify
replace: mail
mail: [email protected]

Apply the self-service mail update as user1:

bash
ldapmodify -x -H ldap://localhost:389 -D "uid=user1,ou=people,dc=example,dc=com" -y /root/user1.pw -f /tmp/user1-mail.ldif

Sample output:

output
modifying entry "uid=user1,ou=people,dc=example,dc=com"

An attempt to change uid is denied because it is outside the allow list. Save the following as /tmp/user1-uid-deny.ldif:

ldif
dn: uid=user1,ou=people,dc=example,dc=com
changetype: modify
replace: uid
uid: user1-changed

Attempt the forbidden uid change as user1:

bash
ldapmodify -x -H ldap://localhost:389 -D "uid=user1,ou=people,dc=example,dc=com" -y /root/user1.pw -f /tmp/user1-uid-deny.ldif

Sample output:

output
ldap_modify: Insufficient access (50)
	additional info: Insufficient 'write' privilege to the 'uid' attribute of entry 'uid=user1,ou=people,dc=example,dc=com'.

Protect identity and privileged attributes

Do not grant self-service write on attributes such as:

text
uid
uidNumber
gidNumber
manager
memberOf
nsRoleDN
nsAccountLock
aci
objectClass
creatorsName
modifiersName

The exact protected set depends on your schema and applications. Prefer an explicit allow list of safe profile attributes rather than targetattr="*". Broad write rules are difficult to audit and easy to over-grant.


Require an encrypted connection for self-service password changes

I'll create one ACI that targets only userPassword and requires a protected connection. Red Hat Directory Server documents ldap:///self for self-password changes and ssf>="128" to require an encrypted channel.

The ssf>="128" condition requires an encrypted connection with a security strength of at least 128. This can be provided by TLS, LDAPS, or a sufficiently strong SASL security layer. This lab verifies the rule with StartTLS.

Save the following as /tmp/self-password-tls.ldif:

ldif
dn: ou=People,dc=example,dc=com
changetype: modify
add: aci
aci: (targetattr="userPassword")(version 3.0; acl "Self change password over TLS"; allow (write) userdn="ldap:///self" and ssf>="128";)

Apply as Directory Manager:

bash
ldapmodify -x -H ldap://localhost:389 -D "cn=Directory Manager" -y /root/dm.pw -f /tmp/self-password-tls.ldif

Password policy constraints (minimum length, history, expiration) are enforced by the password policy configuration in addition to ACIs.

This rule grants password-write access only when SSF is at least 128. Review inherited ACIs to ensure another rule does not independently grant the same write access over cleartext LDAP. Matching deny rules override allows, while permissions from multiple allow rules can combine.

Create the password-change LDIF with restrictive permissions. Prompt for the new password so it is not recorded in shell history. The LDIF file still contains the value in plaintext until you remove it:

bash
umask 077
read -rsp 'Enter the new user1 password: ' USER1_NEW_PW
echo

{
  printf '%s\n' \
    'dn: uid=user1,ou=people,dc=example,dc=com' \
    'changetype: modify' \
    'replace: userPassword'
  printf 'userPassword: %s\n' "$USER1_NEW_PW"
} > /root/user1-password.ldif

A cleartext LDAP modify is denied when no overlapping allow rule exists:

bash
ldapmodify -x -H ldap://localhost:389 -D "uid=user1,ou=people,dc=example,dc=com" -y /root/user1.pw -f /root/user1-password.ldif

Sample output:

output
ldap_modify: Insufficient access (50)
	additional info: Insufficient 'write' privilege to the 'userPassword' attribute of entry 'uid=user1,ou=people,dc=example,dc=com'.

StartTLS on port 389 succeeds when SSF meets the threshold. Use -ZZ so the command fails if StartTLS cannot be established. This lab uses a self-signed certificate, so use LDAPTLS_REQCERT=never only for local testing:

bash
LDAPTLS_REQCERT=never ldapmodify -ZZ -x -H ldap://localhost:389 -D "uid=user1,ou=people,dc=example,dc=com" -y /root/user1.pw -f /root/user1-password.ldif

Sample output:

output
modifying entry "uid=user1,ou=people,dc=example,dc=com"

Update the bind password file and remove the LDIF that contains the new password:

bash
printf '%s\n' "$USER1_NEW_PW" > /root/user1.pw unset USER1_NEW_PW rm -f /root/user1-password.ldif

Confirm the new password succeeds:

bash
ldapwhoami -x -H ldap://localhost:389 -D "uid=user1,ou=people,dc=example,dc=com" -y /root/user1.pw

Sample output:

output
dn: uid=user1,ou=people,dc=example,dc=com

Profile updates and password changes remain separate ACIs. In this lab, only the password ACI uses an SSF condition. Add the same condition to profile-update ACIs when those attributes must also be changed only over encrypted connections.


Allow users to manage their own group membership

For opt-in groups, place the ACI on the group entry and use userdn="ldap:///all" with selfwrite on member. Do not use ldap:///self here. The target entry is the group, not the user.

A groupOfNames entry requires cn and at least one member value. Seed the group with user2, who remains a member while user1 performs the join and leave tests. This prevents user1 from removing the final required member value.

Save the following as /tmp/self-service-optin-group.ldif:

ldif
dn: cn=self-service-optin,ou=Groups,dc=example,dc=com
objectClass: groupOfNames
cn: self-service-optin
member: uid=user2,ou=people,dc=example,dc=com

Create the group as Directory Manager:

bash
ldapadd -x -H ldap://localhost:389 -D "cn=Directory Manager" -y /root/dm.pw -f /tmp/self-service-optin-group.ldif

Add the membership ACI on the group entry.

Save the following as /tmp/self-service-optin-aci.ldif:

ldif
dn: cn=self-service-optin,ou=Groups,dc=example,dc=com
changetype: modify
add: aci
aci: (targetattr="member")(version 3.0; acl "Self opt-in group membership"; allow (selfwrite) userdn="ldap:///all";)

Apply as Directory Manager:

bash
ldapmodify -x -H ldap://localhost:389 -D "cn=Directory Manager" -y /root/dm.pw -f /tmp/self-service-optin-aci.ldif

Red Hat Directory Server documents this narrow scope: selfwrite alone lets authenticated users add or remove only their own DN in member. It does not grant general read access to the full membership list. Add a separate read ACI only when your application requires users to view the group or its members.

Test in this order as user1:

  1. Join the group by adding your own DN. Save the following as /root/user1-join-group.ldif:
ldif
dn: cn=self-service-optin,ou=Groups,dc=example,dc=com
changetype: modify
add: member
member: uid=user1,ou=people,dc=example,dc=com

Join the opt-in group as user1:

bash
ldapmodify -x -H ldap://localhost:389 -D "uid=user1,ou=people,dc=example,dc=com" -y /root/user1.pw -f /root/user1-join-group.ldif

Sample output:

output
modifying entry "cn=self-service-optin,ou=Groups,dc=example,dc=com"
  1. Leave the group by deleting only your own DN (user2 remains as the seed member during this test). Save the following as /root/user1-leave-group.ldif:
ldif
dn: cn=self-service-optin,ou=Groups,dc=example,dc=com
changetype: modify
delete: member
member: uid=user1,ou=people,dc=example,dc=com

Leave the group by removing only your own DN:

bash
ldapmodify -x -H ldap://localhost:389 -D "uid=user1,ou=people,dc=example,dc=com" -y /root/user1.pw -f /root/user1-leave-group.ldif
  1. Rejoin with the same add: member LDIF from step 1.

  2. Confirm you cannot add another user's DN. Save the following as /root/user1-add-user3-group.ldif:

ldif
dn: cn=self-service-optin,ou=Groups,dc=example,dc=com
changetype: modify
add: member
member: uid=user3,ou=people,dc=example,dc=com

Attempt to add another user to the group as user1:

bash
ldapmodify -x -H ldap://localhost:389 -D "uid=user1,ou=people,dc=example,dc=com" -y /root/user1.pw -f /root/user1-add-user3-group.ldif

Sample output:

output
ldap_modify: Insufficient access (50)
	additional info: Insufficient 'write' privilege to the 'member' attribute of entry 'cn=self-service-optin,ou=groups,dc=example,dc=com'.

Never use self-service membership for administrator, operator, or authorization groups.


Expected results for this lab

Operation performed as user1 Expected result
Read permitted attributes from user1 Allow
Read user2 using only the self-read ACI No result
Modify mail on user1 Allow
Modify uid on user1 Deny
Change user1 password over plain LDAP Deny when no overlapping ACI exists
Change user1 password over StartTLS Allow
Add or remove user1 from the opt-in group Allow
Add user3 to the opt-in group Deny

For entry- and attribute-level permission analysis, use Get Effective Rights in the dedicated effective rights chapter.


Update or remove self-service ACIs

Follow the safe update process from ACI examples:

  1. Read the exact aci value with ldapsearch.
  2. Delete only the intended value using delete: aci in a modify LDIF.
  3. Add the revised value when required.
  4. Retest allowed and denied operations as the bind user.

Do not delete the entire aci attribute unless you intend to remove every rule on the entry.


Troubleshoot self-service ACIs

Symptom Likely cause Fix
User can read the entry DN but no attributes appear Missing (objectClass=*) filter, missing objectClass in read targetattr, or missing search right Use (objectClass=*) before attribute names; add objectClass to targetattr with read,search,compare
User can update some profile fields but not others Attribute omitted from write targetattr Add the attribute to the allow list
Password change returns insufficient access Missing userPassword in target, wrong bind rule, or SSF too low Confirm ldap:///self, TLS/StartTLS, and password policy
Password change works over cleartext unexpectedly Broader inherited allow on userPassword Search aci on ancestors; remove or narrow the overlapping rule
selfwrite on a group always fails Used ldap:///self on the group entry Use ldap:///all with selfwrite on the group; target is the group DN
User can add another user's DN to the group This or an inherited ACI grants write on member Remove or narrow the write grant; retain only selfwrite for opt-in membership
Users can display the complete membership list An ACI grants read on member Remove or narrow the read grant if membership must remain private
Users can test or infer whether a DN is a member An ACI grants search or compare on member Remove or narrow those rights when membership queries must also be restricted
User can modify uid or aci Over-broad targetattr or inherited write rule Switch to explicit allow list; inspect ancestor ACIs
Leave opt-in group fails with object-class violation Last member value removed from groupOfNames Keep at least one other member in the group during tests; only delete the bind user's own DN

When result code 50 persists, see fix LDAP error 50.


What's Next


References


Summary

  1. Use ldap:///self for read and write on the authenticated user's own entry.
  2. Allow only explicitly approved profile attributes in targetattr.
  3. Create a password ACI with ssf>="128" so changes require an encrypted connection.
  4. Use allow (selfwrite) on member with ldap:///all on opt-in group entries, not ldap:///self, and not read on membership unless the application requires it.
  5. Distinguish write (profile and password values) from selfwrite (own DN in member).
  6. Test both allowed and denied operations as the real bind user.

Remove the bind-password file when the lab is complete. Retain it only when it is intentionally required by later exercises and remains protected with mode 0600.

bash
rm -f /root/user1.pw

Frequently Asked Questions

1. What is the difference between write and selfwrite in a self-service ACI?

write lets the bind user modify attribute values on the target entry. selfwrite lets the bind user add or remove only their own DN from a DN-valued attribute such as member. Profile and password changes use write; opt-in group membership typically uses selfwrite without write on member.

2. Why does ldap:///self fail on a group membership ACI?

ldap:///self matches when the bind DN equals the target entry DN. A user binds as uid=user1 but modifies cn=group—the target is the group entry. Use userdn="ldap:///all" with selfwrite on member so authenticated users can add or remove only their own DN.

3. Can a conditional SSF allow rule block cleartext password changes by itself?

Not always. A conditional allow grants password write only when SSF is high enough. If another inherited ACI grants userPassword write without an SSF bind rule, cleartext changes may still succeed. Review inherited ACIs before relying on SSF alone.

4. Why does ldapsearch on my own entry return no attributes?

ldapsearch expects filter [attrs...]. Use an explicit filter such as (objectClass=*) before attribute names. The self-read ACI must also include objectClass in targetattr with search permission so base-scope searches can match the entry.

5. Should users be allowed to write their own uid or memberOf?

No. Use an explicit allow list for safe profile attributes. Never grant self-service write to uid, nsRoleDN, memberOf, aci, or other identity and authorization attributes.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive …