389 Directory Server ACI Configuration with Practical Examples

Learn 389 Directory Server ACI syntax, placement, and access rights with ldapmodify examples for userdn and groupdn read, write, add, and delete permissions plus troubleshooting.

Published

Updated

Read time 19 min read

Reviewed byDeepak Prasad

389 Directory Server access control instruction blocks granting read write and delete LDAP rights to users and groups through aci attributes

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:

IMPORTANT

This guide covers beginner ACI syntax, placement, and seven practical examples with ldapsearch and ldapmodify. It does not cover:

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:

text
(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.

text
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):

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

Sample output:

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

Assign a separate password for user2. Later examples bind as each identity independently:

bash
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:

bash
printf '%s\n' 'User1Lab2026!' > /root/user1.pw
printf '%s\n' 'User2Lab2026!' > /root/user2.pw
chmod 600 /root/user1.pw /root/user2.pw

The 600 mode limits read access to the file owner.

IMPORTANT
The strings in /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:

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

The returned DN confirms simple bind authentication for user1.

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

user2 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:

bash
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
EOF

Apply the attribute adds as Directory Manager:

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

Sample output:

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:

bash
dsidm ldap1 group create --cn ldap-readers

Sample output:

output
Successfully created ldap-readers

If the group already exists, dsidm reports that instead. Continue to the membership step.

Add user1 as a member so Example 2 can test groupdn:

bash
dsidm ldap1 group add_member ldap-readers "uid=user1,ou=people,dc=example,dc=com"

Sample output:

output
added member: uid=user1,ou=people,dc=example,dc=com

The membership entry is what a groupdn bind rule matches later.

I'll create a disposable subtree for add and delete exercises:

bash
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
EOF

Add the organizational unit under ou=people:

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

Sample output:

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:

bash
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 uid

Sample output:

output
# numResponses: 1

Result 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:

bash
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=*)" aci

On 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:

bash
ldapsearch -LLL -x -H ldap://localhost:389 -D "cn=Directory Manager" -y /root/dm.pw -b "dc=example,dc=com" -s sub "(aci=*)" dn aci

Sample output (trimmed):

output
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:

bash
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";)
EOF

Post the new rule to the People container as Directory Manager:

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

Sample output:

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:

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 "(uid=user1)" mail cn sn uid

Sample output:

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

mail, 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:

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 "(uid=user1)" telephoneNumber

Sample output:

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

Only 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:

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 "(uid=user2)" mail cn uid

Sample output:

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

The 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:

bash
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)" mail

Sample output:

output
# numResponses: 1

An 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:

bash
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";)
EOF

Apply the group rule on ou=people. Example 1 left cross-user reads to this groupdn grant:

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

Sample output:

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:

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 "(uid=user2)" mail cn uid

Sample output:

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

Before 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:

bash
dsidm ldap1 group add_member ldap-readers "uid=user2,ou=people,dc=example,dc=com"

Sample output:

output
added member: uid=user2,ou=people,dc=example,dc=com

Bind as user2 and repeat the search against user1:

bash
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 uid

Sample output:

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

Group 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:

bash
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";)
EOF

Load the write rule on ou=people:

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

Sample output:

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:

bash
cat > /tmp/user2-mail.ldif <<'EOF'
dn: uid=user2,ou=people,dc=example,dc=com
changetype: modify
replace: mail
mail: [email protected]
EOF

Apply the change while bound as user2:

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

Sample output:

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:

bash
cat > /tmp/user2-uid.ldif <<'EOF'
dn: uid=user2,ou=people,dc=example,dc=com
changetype: modify
replace: uid
uid: user2-hacked
EOF

Run the denied modify as user2:

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

Sample output:

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:

bash
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";)
EOF

Attach the add rule to the test OU:

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

Sample output:

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:

bash
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
EOF

Submit the new entry while bound as user2:

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

Sample output:

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:

bash
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";)
EOF

Add the write-only rule. Note that delete is deliberately omitted from the rights list:

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

Sample output:

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:

bash
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
EOF

Apply the description 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/acitest1-modify.ldif

Sample output:

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:

bash
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:

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:

bash
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";)
EOF

Grant delete on user entries under ou=AciLab only. The uid=* wildcard target matches child user entries but not the ou=AciLab container itself:

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

Sample output:

output
modifying entry "ou=AciLab,ou=people,dc=example,dc=com"

Create a second disposable entry under ou=AciLab:

bash
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
EOF

Add the entry as user2:

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

Sample output:

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:

bash
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:

bash
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:

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:

bash
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";)
EOF

Store the entry-level rule on uid=user2 itself so it applies only to that one entry:

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

Sample output:

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

Search as user1 and request both permitted and excluded attributes:

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 "(uid=user2)" mail description cn telephoneNumber

Sample output:

output
dn: uid=user2,ou=people,dc=example,dc=com
mail: [email protected]
description: HR contact
cn: User Two

mail 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:

  1. Read the exact existing aci string with ldapsearch.
  2. Delete that value with ldapmodify and delete: aci.
  3. Add the corrected value with add: aci.
  4. 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:

bash
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";)
EOF

Apply the delete-and-add modify as Directory Manager:

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

Sample output:

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:

bash
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";)
EOF

Remove that single aci value:

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

Sample output:

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 all permissions.
  • Test allowed and denied cases for every new rule.
  • Export or record existing aci values before large changes.
  • Do not sign off using Directory Manager tests alone.

What's Next


References


Summary

  1. Place the ACI on the correct entry so inheritance matches your intent.
  2. Define a narrow target and explicit targetattr list.
  3. Grant only required rights (read and search are both common for directory browsing).
  4. Match the correct userdn or groupdn, with full DNs.
  5. Test with the real bind identity, not only Directory Manager.
  6. Review inherited rules and any deny that overrides your allow.

Frequently Asked Questions

1. Where are ACIs stored in 389 Directory Server?

ACIs are multi-valued values of the operational aci attribute on directory entries. An ACI on a container entry normally applies to that entry and its descendants unless a target rule narrows the scope.

2. What is the difference between read and search in a 389 DS ACI?

read returns attribute values in search results. search lets the bind identity use an attribute in an LDAP filter. Grant both when users must find entries and see attribute values.

3. Why does ldapsearch return Success but no attributes?

The bind identity may lack read or search rights on the requested attributes, or the filter uses attributes that are not permitted. Directory Manager bypasses ACIs—retest with the real application bind DN.

4. Can I use groupdn in an ACI?

Yes. groupdn matches users whose full DN appears in a supported group membership attribute such as member or uniqueMember; dynamic groups can use memberURL. Verify the complete group DN and full member DNs when a group-based ACI does not match.

5. How do I update an existing ACI?

Read the exact aci value, delete that value with ldapmodify, add the corrected value, and retest. Do not replace the entire aci attribute unless you intend to remove every rule on the entry.

6. What LDAP error means an ACI denied the operation?

Writes and deletes usually return result code 50 (insufficient access). Search failures can return 50 or an empty result set when read or search rights are missing on filter 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 …