389 Directory Server denies LDAP access unless an applicable Access Control Instruction (ACI) grants the requested operation. Directory Manager bypasses normal ACIs, so day-to-day security depends on the aci values you place on entries in the Directory Information Tree (DIT).
This guide is the foundation for 389 Directory Server access control. You will read ACI syntax, add rules with ldapmodify, grant read, search, compare, write, add, and delete rights, restrict entries and attributes with target and targetattr, and verify both allowed and denied operations as real bind identities.
Before you start:
- Install 389 Directory Server — running instance
ldap1on port389 - Manage users and groups —
uid=user1,uid=user2, and groupcn=ldap-readers - LDAP basics — DNs, filters, and LDAP result codes (from the OpenLDAP tutorial)
- OpenLDAP vs 389 Directory Server — how ACI strings differ from OpenLDAP
olcAccess - OpenLDAP administrators: compare with OpenLDAP ACL examples
This guide covers beginner ACI syntax, placement, and seven practical examples with ldapsearch and ldapmodify. It does not cover:
targetfilter,userattr, IP or DNS bind rules, or time limits- Macro ACIs, anonymous access, self-service ACI, or delegated administration
- Detailed Get Effective Rights analysis
Those topics belong in dedicated follow-on chapters.
Tested on: Rocky Linux 10.2; 389 Directory Server 3.2.0.
How 389 Directory Server ACIs work
An ACI is a single string stored on a directory entry. The general format is:
(target rules)
(version 3.0;
acl "Descriptive ACI name";
allow or deny (rights)
bind rule;
)| Part | Purpose |
|---|---|
| Target rule | Entries and attributes affected (target, targetattr, and advanced keywords in later chapters) |
| Permission rule | allow or deny plus the rights granted or blocked |
| Bind rule | Identity receiving the rights (userdn, groupdn, and advanced forms later) |
The acl "name" string is a label for administrators. Use unique, descriptive names so you can delete or replace one rule without touching others on the same entry.
Understand ACI placement and scope
Directory Server stores ACIs as values of the multi-valued operational aci attribute. An ACI on a container normally applies to that entry and its descendants unless a target rule narrows scope.
dc=example,dc=com
├── ou=People
│ ├── uid=user1
│ └── uid=user2
└── ou=Groups
└── cn=ldap-readers| Placement | Typical effect |
|---|---|
dc=example,dc=com |
Broad inheritance across the suffix—use sparingly |
ou=people,dc=example,dc=com |
Rights for the People subtree (container DN may display as ou=People) |
| One user entry | Narrowest scope—rights only where the ACI is stored and below |
Place ACIs as close as practical to the subtree they protect. A rule on the suffix root is easier to write but harder to audit than a rule on ou=people or a dedicated organizational unit.
Understand ACI access rights
| Right | Allows |
|---|---|
read |
Return attribute values in search results |
search |
Use attributes in LDAP search filters |
compare |
LDAP compare operations on attributes |
write |
Modify attribute values on existing entries |
add |
Create child entries below the target |
delete |
Remove entries |
selfwrite |
Add or remove the bind user's own DN from an attribute |
proxy |
Operations using proxied authorization |
You can grant all, but list only the rights a role needs. Broad all or targetattr="*" rules are difficult to audit and easy to over-grant.
Creating an entry often requires add on the parent (or target entry) plus write permission for every attribute value in the new entry.
Prepare users, groups, and test entries
This lab uses instance ldap1, suffix dc=example,dc=com, and identities from manage users and groups:
| Entry | Role in this lab |
|---|---|
uid=user1,ou=people,dc=example,dc=com |
Member of cn=ldap-readers; read and modify tests |
uid=user2,ou=people,dc=example,dc=com |
Non-member initially; write, add, and delete tests |
cn=ldap-readers,ou=Groups,dc=example,dc=com |
Group for groupdn read example |
Set bind passwords after user creation. The second argument sets the password non-interactively (omit it to be prompted instead):
dsidm ldap1 account reset_password "uid=user1,ou=people,dc=example,dc=com" 'User1Lab2026!'Sample output:
reset password for uid=user1,ou=people,dc=example,dc=comAssign a separate password for user2. Later examples bind as each identity independently:
dsidm ldap1 account reset_password "uid=user2,ou=people,dc=example,dc=com" 'User2Lab2026!'I'll create password files for later commands with the same values you assigned above, then set mode 600 with the chmod command so other local users cannot read the bind passwords:
printf '%s\n' 'User1Lab2026!' > /root/user1.pw
printf '%s\n' 'User2Lab2026!' > /root/user2.pw
chmod 600 /root/user1.pw /root/user2.pwThe 600 mode limits read access to the file owner.
/root/user1.pw and /root/user2.pw must match the passwords set with dsidm account reset_password. A mismatch produces ldap_bind: Invalid credentials (49) on every bind test.
Confirm each account binds before you add ACIs:
ldapwhoami -x -H ldap://localhost:389 -D "uid=user1,ou=people,dc=example,dc=com" -y /root/user1.pwSample output:
dn:uid=user1,ou=people,dc=example,dc=comThe returned DN confirms simple bind authentication for user1.
ldapwhoami -x -H ldap://localhost:389 -D "uid=user2,ou=people,dc=example,dc=com" -y /root/user2.pwuser2 should print its own DN the same way. If either command returns Invalid credentials (49), recheck the password file contents against the dsidm reset step.
I'll add contact attributes used in later examples. Directory Manager is required because ordinary users cannot write these values yet:
cat > /tmp/user-attrs.ldif <<'EOF'
dn: uid=user1,ou=people,dc=example,dc=com
changetype: modify
add: mail
mail: [email protected]
-
add: telephoneNumber
telephoneNumber: +1-555-0101
dn: uid=user2,ou=people,dc=example,dc=com
changetype: modify
add: mail
mail: [email protected]
-
add: description
description: HR contact
EOFApply the attribute adds as Directory Manager:
ldapmodify -x -H ldap://localhost:389 -D "cn=Directory Manager" -y /root/dm.pw -f /tmp/user-attrs.ldifSample output:
modifying entry "uid=user1,ou=people,dc=example,dc=com"
modifying entry "uid=user2,ou=people,dc=example,dc=com"This lab assumes these attribute values do not already exist. By default, ldapmodify stops after the first error. If an attribute already exists, inspect its current value and use replace or adjust the LDIF instead of rerunning the add operation blindly.
Do not add -c here because silently skipping failed preparation changes could produce inconsistent later results.
I'll create the ldap-readers group when it does not already exist:
dsidm ldap1 group create --cn ldap-readersSample output:
Successfully created ldap-readersIf the group already exists, dsidm reports that instead. Continue to the membership step.
Add user1 as a member so Example 2 can test groupdn:
dsidm ldap1 group add_member ldap-readers "uid=user1,ou=people,dc=example,dc=com"Sample output:
added member: uid=user1,ou=people,dc=example,dc=comThe membership entry is what a groupdn bind rule matches later.
I'll create a disposable subtree for add and delete exercises:
cat > /tmp/acilab-ou.ldif <<'EOF'
dn: ou=AciLab,ou=people,dc=example,dc=com
objectClass: top
objectClass: organizationalUnit
ou: AciLab
description: Disposable ACI test subtree
EOFAdd the organizational unit under ou=people:
ldapadd -x -H ldap://localhost:389 -D "cn=Directory Manager" -y /root/dm.pw -f /tmp/acilab-ou.ldifSample output:
adding new entry "ou=AciLab,ou=people,dc=example,dc=com"ou=AciLab becomes the narrow target for add, modify, and delete examples later in the chapter.
Without any custom ACIs, user1 can bind but receives no user attributes when searching with a filter on objectClass. Retest with the ldapsearch command as the application bind DN:
ldapsearch -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=*)" mail cn uidSample output:
# numResponses: 1Result code 0 with an empty entry body means the search succeeded but no attributes were returned. That is the default deny until an ACI grants access.
List existing ACIs
Review existing rules before adding new ones. Conflicting or overlapping ACIs are easier to spot when you know what is already on the entry.
Search one entry for aci values:
ldapsearch -LLL -x -H ldap://localhost:389 -D "cn=Directory Manager" -y /root/dm.pw -b "ou=people,dc=example,dc=com" -s base "(objectClass=*)" aciOn a fresh People container you may see only the entry DN with no aci lines yet. After adding rules, the same command lists each multi-line aci value.
Find every entry in the suffix that carries an aci attribute. That is useful before a large change:
ldapsearch -LLL -x -H ldap://localhost:389 -D "cn=Directory Manager" -y /root/dm.pw -b "dc=example,dc=com" -s sub "(aci=*)" dn aciSample output (trimmed):
dn: dc=example,dc=com
aci: (targetattr="dc || description || objectClass")(targetfilter="(objectClass=domain)")(version 3.0; acl "Enable anyone domain read"; allow (read,search,compare)(userdn="ldap:///anyone");)
dn: ou=People,dc=example,dc=com
aci: (targetattr="uid || cn || sn || mail")...The domain-level rule is created during instance setup. Your custom rules on ou=People appear alongside it.
Add an ACI with ldapmodify
ACI changes use ldapmodify LDIF files. The pattern below adds one new value to the multi-valued aci attribute. Write the file, then apply it:
cat > /tmp/add-aci.ldif <<'EOF'
dn: ou=people,dc=example,dc=com
changetype: modify
add: aci
aci: (targetattr="uid || cn || sn || mail")(target="ldap:///uid=user1,ou=people,dc=example,dc=com")(version 3.0; acl "Allow user1 to read basic profile attributes"; allow (read,search,compare) userdn="ldap:///uid=user1,ou=people,dc=example,dc=com";)
EOFPost the new rule to the People container as Directory Manager:
ldapmodify -x -H ldap://localhost:389 -D "cn=Directory Manager" -y /root/dm.pw -f /tmp/add-aci.ldifSample output:
modifying entry "ou=people,dc=example,dc=com"Directory Manager can always modify aci values. Application bind identities cannot unless another ACI grants that right. Example 1 below verifies the new rule as user1.
Example 1: Allow one user to read selected attributes
Grant user1 read, search, and compare on uid, cn, sn, and mail on its own entry only. The ACI is stored on ou=people, but target narrows scope to uid=user1 so other People entries stay protected until a separate rule grants access.
Verify allowed attributes with a filter on an attribute listed in targetattr:
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 "(uid=user1)" mail cn sn uidSample output:
dn: uid=user1,ou=people,dc=example,dc=com
mail: [email protected]
cn: User One
uid: user1mail, cn, and uid are all listed in targetattr, so user1 can read them on its own entry.
telephoneNumber is outside targetattr, so it is omitted even though the entry stores a value:
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 "(uid=user1)" telephoneNumberSample output:
dn: uid=user1,ou=people,dc=example,dc=comOnly the entry DN is returned. telephoneNumber is not readable because it is outside targetattr.
user1 cannot read other People entries yet. The target names only user1's DN:
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 "(uid=user2)" mail cn uidSample output:
dn: uid=user2,ou=people,dc=example,dc=comThe entry DN may appear, but mail, cn, and uid are withheld until Example 2 grants group read access.
user2 does not inherit user1's rule when reading user1:
ldapsearch -x -H ldap://localhost:389 -D "uid=user2,ou=people,dc=example,dc=com" -y /root/user2.pw -b "uid=user1,ou=people,dc=example,dc=com" -s base "(uid=user1)" mailSample output:
# numResponses: 1An empty result body means user2 has no rights from Example 1. The bind rule names only user1.
Use (uid=…) (or another permitted filter attribute) instead of (objectClass=*) unless objectClass is included in targetattr. Filters need search rights on every attribute they reference.
Example 2: Allow a group to read a subtree
Add a second ACI on ou=people for the readers group:
cat > /tmp/add-aci-ldap-readers.ldif <<'EOF'
dn: ou=people,dc=example,dc=com
changetype: modify
add: aci
aci: (targetattr="uid || cn || sn || mail")(target="ldap:///ou=people,dc=example,dc=com")(version 3.0; acl "Allow ldap-readers group to read People subtree"; allow (read,search,compare) groupdn="ldap:///cn=ldap-readers,ou=Groups,dc=example,dc=com";)
EOFApply the group rule on ou=people. Example 1 left cross-user reads to this groupdn grant:
ldapmodify -x -H ldap://localhost:389 -D "cn=Directory Manager" -y /root/dm.pw -f /tmp/add-aci-ldap-readers.ldifSample output:
modifying entry "ou=people,dc=example,dc=com"The People container now carries a second aci value. The group rule supplies subtree read access that Example 1 deliberately omitted.
user1 is a group member and can read user2:
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 "(uid=user2)" mail cn uidSample output:
dn: uid=user2,ou=people,dc=example,dc=com
mail: [email protected]
cn: User Two
uid: user2Before user2 is added to ldap-readers, the same search returns an entry shell with no attributes.
Add user2 to the group after the ACI exists to confirm membership is evaluated dynamically:
dsidm ldap1 group add_member ldap-readers "uid=user2,ou=people,dc=example,dc=com"Sample output:
added member: uid=user2,ou=people,dc=example,dc=comBind as user2 and repeat the search against user1:
ldapsearch -LLL -x -H ldap://localhost:389 -D "uid=user2,ou=people,dc=example,dc=com" -y /root/user2.pw -b "uid=user1,ou=people,dc=example,dc=com" -s base "(uid=user1)" mail cn uidSample output:
dn: uid=user1,ou=people,dc=example,dc=com
mail: [email protected]
cn: User One
uid: user1Group membership took effect immediately. No ACI edit was required.
Use the full group DN in groupdn and store full member DNs in the group entry. Directory Server evaluates groupdn through supported membership attributes including member, uniqueMember, and memberURL. Advanced group-filter bind rules belong in advanced ACI.
Example 3: Allow a user to modify selected attributes
Grant user2 read, search, compare, and write on contact fields for its own entry only. The rule is stored on ou=people, but target restricts it to uid=user2:
cat > /tmp/add-aci-user2-write.ldif <<'EOF'
dn: ou=people,dc=example,dc=com
changetype: modify
add: aci
aci: (targetattr="mail || telephoneNumber || description")(target="ldap:///uid=user2,ou=people,dc=example,dc=com")(version 3.0; acl "Allow user2 to update own contact attributes"; allow (read,search,compare,write) userdn="ldap:///uid=user2,ou=people,dc=example,dc=com";)
EOFLoad the write rule on ou=people:
ldapmodify -x -H ldap://localhost:389 -D "cn=Directory Manager" -y /root/dm.pw -f /tmp/add-aci-user2-write.ldifSample output:
modifying entry "ou=people,dc=example,dc=com"user2 can modify only mail, telephoneNumber, and description on its own entry, not on other accounts under ou=people.
Test an allowed change first. Update user2's own mail value:
cat > /tmp/user2-mail.ldif <<'EOF'
dn: uid=user2,ou=people,dc=example,dc=com
changetype: modify
replace: mail
mail: [email protected]
EOFApply the change while bound as user2:
ldapmodify -x -H ldap://localhost:389 -D "uid=user2,ou=people,dc=example,dc=com" -y /root/user2.pw -f /tmp/user2-mail.ldifSample output:
modifying entry "uid=user2,ou=people,dc=example,dc=com"The successful modify confirms write on mail is granted.
Next, attempt to change uid, which is outside targetattr:
cat > /tmp/user2-uid.ldif <<'EOF'
dn: uid=user2,ou=people,dc=example,dc=com
changetype: modify
replace: uid
uid: user2-hacked
EOFRun the denied modify as user2:
ldapmodify -x -H ldap://localhost:389 -D "uid=user2,ou=people,dc=example,dc=com" -y /root/user2.pw -f /tmp/user2-uid.ldifSample output:
ldap_modify: Insufficient access (50)
additional info: Insufficient 'write' privilege to the 'uid' attribute of entry 'uid=user2,ou=people,dc=example,dc=com'.Result code 50 on a single attribute is the expected denial when targetattr excludes that field. Attributes such as userPassword, memberOf, and nsAccountLock stay protected for the same reason.
Example 4: Allow creation of entries below an OU
Place an ACI on the disposable ou=AciLab container:
cat > /tmp/add-aci-acilab-add.ldif <<'EOF'
dn: ou=AciLab,ou=people,dc=example,dc=com
changetype: modify
add: aci
aci: (targetattr="uid || cn || sn || objectClass")(target="ldap:///ou=AciLab,ou=people,dc=example,dc=com")(version 3.0; acl "Allow user2 to add entries under AciLab"; allow (add,read,search,compare,write) userdn="ldap:///uid=user2,ou=people,dc=example,dc=com";)
EOFAttach the add rule to the test OU:
ldapmodify -x -H ldap://localhost:389 -D "cn=Directory Manager" -y /root/dm.pw -f /tmp/add-aci-acilab-add.ldifSample output:
modifying entry "ou=AciLab,ou=people,dc=example,dc=com"user2 now has add and attribute write rights limited to ou=AciLab. Create a minimal inetOrgPerson entry to exercise those rights:
cat > /tmp/acitest1.ldif <<'EOF'
dn: uid=acitest1,ou=AciLab,ou=people,dc=example,dc=com
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
cn: ACI Test One
sn: Test
uid: acitest1
EOFSubmit the new entry while bound as user2:
ldapadd -x -H ldap://localhost:389 -D "uid=user2,ou=people,dc=example,dc=com" -y /root/user2.pw -f /tmp/acitest1.ldifSample output:
adding new entry "uid=acitest1,ou=AciLab,ou=people,dc=example,dc=com"Both add on the parent and write on each attribute in the LDIF were required. Missing either produces result code 50.
Example 5: Allow modification but not deletion
Grant user1 modify rights in ou=AciLab without delete:
cat > /tmp/add-aci-acilab-write.ldif <<'EOF'
dn: ou=AciLab,ou=people,dc=example,dc=com
changetype: modify
add: aci
aci: (targetattr="description")(target="ldap:///ou=AciLab,ou=people,dc=example,dc=com")(version 3.0; acl "Allow user1 modify but not delete in AciLab"; allow (read,search,compare,write) userdn="ldap:///uid=user1,ou=people,dc=example,dc=com";)
EOFAdd the write-only rule. Note that delete is deliberately omitted from the rights list:
ldapmodify -x -H ldap://localhost:389 -D "cn=Directory Manager" -y /root/dm.pw -f /tmp/add-aci-acilab-write.ldifSample output:
modifying entry "ou=AciLab,ou=people,dc=example,dc=com"Only description is writable. Avoid targetattr="*" with write in production. Wildcard write access can let users modify the aci attribute and grant themselves additional rights.
Confirm user1 can change description on acitest1:
cat > /tmp/acitest1-modify.ldif <<'EOF'
dn: uid=acitest1,ou=AciLab,ou=people,dc=example,dc=com
changetype: modify
replace: description
description: Updated by user1
EOFApply the description change as user1:
ldapmodify -x -H ldap://localhost:389 -D "uid=user1,ou=people,dc=example,dc=com" -y /root/user1.pw -f /tmp/acitest1-modify.ldifSample output:
modifying entry "uid=acitest1,ou=AciLab,ou=people,dc=example,dc=com"Write access works. Next, confirm delete is still denied for the same identity:
ldapdelete -x -H ldap://localhost:389 -D "uid=user1,ou=people,dc=example,dc=com" -y /root/user1.pw "uid=acitest1,ou=AciLab,ou=people,dc=example,dc=com"Sample output:
ldap_delete: Insufficient access (50)
additional info: Insufficient 'delete' privilege to delete the entry 'uid=acitest1,ou=AciLab,ou=people,dc=example,dc=com'.Omitting delete from the ACI is enough to block removal even when write is granted.
Example 6: Allow deletion of selected entries
Add a separate, narrow delete grant for user2 on the same test OU:
cat > /tmp/add-aci-acilab-delete.ldif <<'EOF'
dn: ou=AciLab,ou=people,dc=example,dc=com
changetype: modify
add: aci
aci: (target="ldap:///uid=*,ou=AciLab,ou=people,dc=example,dc=com")(version 3.0; acl "Allow user2 delete user entries in AciLab"; allow (delete) userdn="ldap:///uid=user2,ou=people,dc=example,dc=com";)
EOFGrant delete on user entries under ou=AciLab only. The uid=* wildcard target matches child user entries but not the ou=AciLab container itself:
ldapmodify -x -H ldap://localhost:389 -D "cn=Directory Manager" -y /root/dm.pw -f /tmp/add-aci-acilab-delete.ldifSample output:
modifying entry "ou=AciLab,ou=people,dc=example,dc=com"Create a second disposable entry under ou=AciLab:
cat > /tmp/acitest2.ldif <<'EOF'
dn: uid=acitest2,ou=AciLab,ou=people,dc=example,dc=com
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
cn: ACI Test Two
sn: Test
uid: acitest2
EOFAdd the entry as user2:
ldapadd -x -H ldap://localhost:389 -D "uid=user2,ou=people,dc=example,dc=com" -y /root/user2.pw -f /tmp/acitest2.ldifSample output:
adding new entry "uid=acitest2,ou=AciLab,ou=people,dc=example,dc=com"Delete that entry as user2, who holds the narrow delete grant:
ldapdelete -x -H ldap://localhost:389 -D "uid=user2,ou=people,dc=example,dc=com" -y /root/user2.pw "uid=acitest2,ou=AciLab,ou=people,dc=example,dc=com"The delete command exits silently on success. acitest2 is removed from the directory.
Attempt the same operation on an entry outside ou=AciLab:
ldapdelete -x -H ldap://localhost:389 -D "uid=user2,ou=people,dc=example,dc=com" -y /root/user2.pw "uid=user3,ou=people,dc=example,dc=com"Sample output:
ldap_delete: Insufficient access (50)
additional info: Insufficient 'delete' privilege to delete the entry 'uid=user3,ou=people,dc=example,dc=com'.Grant delete only on subtrees you are willing to empty.
Example 7: Exclude sensitive attributes with targetattr
Instead of targetattr="*", list ordinary attributes explicitly. targetattr affects read, search, write, and which attributes may appear in add operations.
Place a narrow ACI on one entry to demonstrate attribute-level restriction:
cat > /tmp/add-aci-user2-entry.ldif <<'EOF'
dn: uid=user2,ou=people,dc=example,dc=com
changetype: modify
add: aci
aci: (targetattr="mail || description")(version 3.0; acl "Allow user1 read only mail and description on user2"; allow (read,search,compare) userdn="ldap:///uid=user1,ou=people,dc=example,dc=com";)
EOFStore the entry-level rule on uid=user2 itself so it applies only to that one entry:
ldapmodify -x -H ldap://localhost:389 -D "cn=Directory Manager" -y /root/dm.pw -f /tmp/add-aci-user2-entry.ldifSample output:
modifying entry "uid=user2,ou=people,dc=example,dc=com"Search as user1 and request both permitted and excluded attributes:
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 "(uid=user2)" mail description cn telephoneNumberSample output:
dn: uid=user2,ou=people,dc=example,dc=com
mail: [email protected]
description: HR contact
cn: User Twomail and description come from this entry-level ACI. cn is still visible because the group ACI on ou=people also applies. Multiple ACIs combine. telephoneNumber stays hidden.
Update an existing ACI
Directory Server does not support in-place edits of one aci value. Use this workflow:
- Read the exact existing
acistring withldapsearch. - Delete that value with
ldapmodifyanddelete: aci. - Add the corrected value with
add: aci. - Retest as the affected bind identities.
Example rename of the ACL label on uid=user2. Write the delete-and-add modify record, then apply it:
cat > /tmp/update-aci-user2.ldif <<'EOF'
dn: uid=user2,ou=people,dc=example,dc=com
changetype: modify
delete: aci
aci: (targetattr="mail || description")(version 3.0; acl "Allow user1 read only mail and description on user2"; allow (read,search,compare) userdn="ldap:///uid=user1,ou=people,dc=example,dc=com";)
-
add: aci
aci: (targetattr="mail || description")(version 3.0; acl "Allow user1 read contact fields on user2"; allow (read,search,compare) userdn="ldap:///uid=user1,ou=people,dc=example,dc=com";)
EOFApply the delete-and-add modify as Directory Manager:
ldapmodify -x -H ldap://localhost:389 -D "cn=Directory Manager" -y /root/dm.pw -f /tmp/update-aci-user2.ldifSample output:
modifying entry "uid=user2,ou=people,dc=example,dc=com"The delete and add values must match character-for-character except for the change you intend. List aci on the entry again to confirm only the label changed.
Delete an ACI safely
Delete one exact aci value, not the entire attribute unless you mean to remove every rule on the entry:
cat > /tmp/delete-aci-user2.ldif <<'EOF'
dn: uid=user2,ou=people,dc=example,dc=com
changetype: modify
delete: aci
aci: (targetattr="mail || description")(version 3.0; acl "Allow user1 read contact fields on user2"; allow (read,search,compare) userdn="ldap:///uid=user1,ou=people,dc=example,dc=com";)
EOFRemove that single aci value:
ldapmodify -x -H ldap://localhost:389 -D "cn=Directory Manager" -y /root/dm.pw -f /tmp/delete-aci-user2.ldifSample output:
modifying entry "uid=user2,ou=people,dc=example,dc=com"List aci on ou=people and uid=user2 to confirm the named rule is gone while other values remain, then retest access as user1.
Test ACI behavior
For every example, test as:
| Identity | Purpose |
|---|---|
| Directory Manager | Confirm the data change or entry exists—not sufficient alone for security sign-off |
| Intended user or group member | Verify the grant works |
| Unintended user | Verify default deny still applies |
| Anonymous client | Only when your design includes userdn="ldap:///anyone" rules |
When result code 50 appears on writes, see fix LDAP error 50 for broader diagnosis. For deep permission debugging, use Get Effective Rights in the dedicated effective rights chapter rather than guessing from ACIs alone.
Understand allow and deny behavior
Multiple applicable ACIs can contribute permissions on the same operation. A deny rule takes precedence over an allow rule when both match. A broad deny on a subtree can override narrower allow rules you expected to win.
Prefer narrow allow rules at the lowest practical entry. Keep detailed conflict evaluation and compound expressions in advanced ACI.
Troubleshoot common ACI problems
| Symptom | Likely cause | Fix |
|---|---|---|
| ACI exists but the user still has no access | Wrong placement, target, targetattr, bind DN, or group membership |
List aci on ancestors; verify full DNs; confirm member values |
| Search returns no entries | Missing search right or filter uses disallowed attributes |
Grant search on filter attributes; avoid (objectClass=*) unless permitted |
| Entry found but attributes are missing | targetattr excludes them or read is missing |
Add attributes to targetattr and grant read |
| Add fails with insufficient access | Missing add on parent or write on attributes in the new entry |
Grant both; include objectClass in targetattr when needed |
| Group ACI does not match | Wrong group DN or incomplete member DNs | Use full group DN; confirm bind user DN is in member, uniqueMember, or dynamic memberURL |
| Access works only as Directory Manager | DM bypasses ACIs | Retest with the application bind identity |
| Modification denied on one attribute | Attribute not in targetattr or explicit deny |
Adjust targetattr or remove conflicting deny |
ACI security recommendations
- Follow least privilege—grant only required rights and attributes.
- Place ACIs as close as possible to the protected subtree.
- Use descriptive, unique
acl "name"strings. - List permitted attributes explicitly; avoid routine
targetattr="*". - Avoid unnecessary
allpermissions. - Test allowed and denied cases for every new rule.
- Export or record existing
acivalues before large changes. - Do not sign off using Directory Manager tests alone.
What's Next
- Advanced ACI patterns —
targetfilter,targetattr, and bind rules - Self-service ACI — let users update their own attributes safely
- Delegated administration — operational admin groups
References
- Red Hat Directory Server 13 — Managing access control — Chapter 2 covers ACI placement, syntax, evaluation, targets, permissions, and bind rules; Section 2.1.6 (displaying, adding, deleting, and updating ACIs), Section 2.1.7 (ACI targets), Section 2.1.9 (permissions and required rights), Section 2.1.10 (
userdn,groupdn, and other bind rules), and Section 2.4 (Get Effective Rights) - 389 Directory Server documentation
Summary
- Place the ACI on the correct entry so inheritance matches your intent.
- Define a narrow
targetand explicittargetattrlist. - Grant only required rights (
readandsearchare both common for directory browsing). - Match the correct
userdnorgroupdn, with full DNs. - Test with the real bind identity, not only Directory Manager.
- Review inherited rules and any
denythat overrides yourallow.

