Problem
In a UCS@school environment running UCS 5.2, users may be unable to access their mailboxes because duplicate LDAP entries exist on a Replica running Dovecot.
The affected users have two LDAP entries with different DNs (in different school OUs), but identical entryUUID and mailPrimaryAddress values. Only the current entry exists on the Primary.
Dovecot fails to resolve the affected users:
doveadm user -f home user@example.com
doveadm(user@example.com): Error: auth-master: userdb lookup(user@example.com): Auth USER lookup failed
Unaffected accounts return the expected mailbox home directory.
Investigation
Compare the affected user on the Primary and the Replica:
univention-ldapsearch -LLL '(mailPrimaryAddress=user@example.com)' dn entryUUID entryCSN
In the observed case:
- Primary: One LDAP entry with the current DN.
- Replica: Two entries in different school OUs, both with the same
entryUUID. - The current entry has the same
entryCSNon Primary and Replica. - The obsolete entry exists only on the Replica and has an older
entryCSN.
The Listener reports that replication is up to date.
Root Cause
An obsolete LDAP entry remained on the Replica after the user account had been moved between school OUs.
The Replica therefore contained two entries for the same user and email address. Removing the obsolete entry restored Dovecot user lookup and mailbox access.
Solution
Workaround: Move the affected user on the Primary temporarily into the OU containing the obsolete Replica entry, then move the user back to the correct OU.
This generates new LDAP move operations. In the observed environment, the procedure removed the duplicate entries and restored mailbox access.
The workaround was successfully applied to several hundred affected accounts.
Prerequisites and precautions:
- Create verified backups of LDAP and the affected mailboxes.
- Confirm that each affected user has exactly one entry on the Primary and two entries with the same
entryUUIDon the Replica. - Check
mail/dovecot/mailbox/deleteon the mail server. Automatic mailbox deletion should be disabled. - Test the procedure with one affected account before processing multiple users.
- Perform the operation during a maintenance window. Temporarily moving users between school OUs can affect other UCS@school components and integrations.
- For the automation below, passwordless SSH authentication from the Primary to the Replica must be configured.
Manual workaround
On the Primary, move the user into the LDAP container containing the obsolete entry:
udm users/user move \
--dn '<CURRENT_USER_DN>' \
--position '<OBSOLETE_PARENT_DN>'
Wait until the change has been replicated.
Then move the user back to the original container:
udm users/user move \
--dn '<TEMPORARY_USER_DN>' \
--position '<ORIGINAL_PARENT_DN>'
--position specifies the parent container DN, not the complete user DN.
Automated workaround
The following example is adapted from a customer-contributed script that successfully automated this procedure.
The adapted version includes additional checks for matching entryUUID values, unexpected LDAP results and replication completion. It stops processing if a user cannot be safely identified or verified.
The script is intended for the specific duplicate-entry scenario described above. Review it for the local environment before production use. The adapted version has not been independently validated on a production UCS@school installation.
Save the script as /root/fix-duplicate-ldap-users.sh on the Primary:
#!/bin/bash
# Example adapted from a customer-contributed script for UCS 5.2.
# Input: one primary email address per line.
set -uo pipefail
INPUT_FILE="${1:-doppelte_Mails.txt}"
REPLICA_HOST="<REPLICA-FQDN>"
lookup_primary() {
univention-ldapsearch -LLL "(mailPrimaryAddress=$1)" dn entryUUID |
awk -F ': ' '/^dn: / { dn=$2 } /^entryUUID: / { print dn "\t" $2 }'
}
lookup_replica() {
ssh -n -o BatchMode=yes "$REPLICA_HOST" \
"univention-ldapsearch -LLL '(mailPrimaryAddress=$1)' dn entryUUID" |
awk -F ': ' '/^dn: / { dn=$2 } /^entryUUID: / { print dn "\t" $2 }'
}
wait_for_replica() {
local email="$1" expected="$2" actual attempt
for ((attempt=0; attempt<24; attempt++)); do
actual=$(lookup_replica "$email") || return 1
[[ "$actual" == "$expected" ]] && return 0
sleep 5
done
return 1
}
[[ -r "$INPUT_FILE" ]] || {
echo "Input file not readable: $INPUT_FILE" >&2
exit 1
}
ssh -n -o BatchMode=yes -o ConnectTimeout=5 "$REPLICA_HOST" true || exit 1
while IFS= read -r email || [[ -n "$email" ]]; do
email=${email%$'\r'}
[[ -z "$email" || "$email" == \#* ]] && continue
if [[ ! "$email" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+$ ]]; then
echo "Invalid or unsupported email address: $email" >&2
exit 1
fi
primary=$(lookup_primary "$email") || exit 1
replica=$(lookup_replica "$email") || exit 1
if [[ -z "$primary" || "$primary" == *$'\n'* ]]; then
echo "Expected exactly one Primary entry for $email" >&2
exit 1
fi
IFS=$'\t' read -r current_dn uuid <<< "$primary"
if [[ -z "$current_dn" || -z "$uuid" ]]; then
echo "Missing DN or UUID for $email" >&2
exit 1
fi
if [[ "$replica" == "$primary" ]]; then
echo "Already synchronized: $email"
continue
fi
mapfile -t entries <<< "$replica"
if (( ${#entries[@]} != 2 )); then
echo "Expected two Replica entries for $email, found ${#entries[@]}" >&2
exit 1
fi
ghost_dn=""
current_count=0
for entry in "${entries[@]}"; do
IFS=$'\t' read -r dn replica_uuid <<< "$entry"
if [[ "$replica_uuid" != "$uuid" ]]; then
echo "Different entryUUID on Replica for $email" >&2
exit 1
fi
if [[ "$dn" == "$current_dn" ]]; then
((current_count+=1))
else
ghost_dn="$dn"
fi
done
if (( current_count != 1 )) || [[ -z "$ghost_dn" ]]; then
echo "Unexpected Replica DN structure for $email" >&2
exit 1
fi
current_position=${current_dn#*,}
ghost_position=${ghost_dn#*,}
echo "Moving $email to $ghost_position"
if ! udm users/user move \
--dn "$current_dn" \
--position "$ghost_position"; then
echo "First move failed; stopped." >&2
exit 1
fi
if ! wait_for_replica "$email" "$ghost_dn"$'\t'"$uuid"; then
echo "Replica did not reach intermediate DN for $email; restoring Primary." >&2
fi
intermediate=$(lookup_primary "$email") || exit 1
if [[ "$intermediate" != "$ghost_dn"$'\t'"$uuid" ]]; then
echo "Unexpected Primary DN after first move for $email; manual recovery required." >&2
exit 1
fi
echo "Moving $email back to $current_position"
if ! udm users/user move \
--dn "$ghost_dn" \
--position "$current_position"; then
echo "Return move failed for $email; manually restore the original DN!" >&2
exit 1
fi
if ! wait_for_replica "$email" "$current_dn"$'\t'"$uuid"; then
echo "Replica not clean for $email; stopped." >&2
exit 1
fi
echo "OK: $email"
done < "$INPUT_FILE"
Replace <REPLICA-FQDN> with the hostname of the affected Replica.
The script assumes ordinary user DNs without embedded commas in the first RDN. It accepts common email address formats; addresses rejected by its validation require individual handling.
Create an input file containing the affected users’ primary email addresses, one per line:
user1@example.com
user2@example.com
user3@example.com
Save it as /root/doppelte_Mails.txt.
For the initial test, include only one affected user.
Run the script on the Primary:
chmod 700 /root/fix-duplicate-ldap-users.sh
/root/fix-duplicate-ldap-users.sh /root/doppelte_Mails.txt
The script waits for the expected Replica LDAP state after each move, for up to approximately two minutes per step.
If an operation fails, the script stops processing. A failed return move may require manually restoring the user’s original DN on the Primary.
Verification
After processing an affected user, run the following command on the Replica:
univention-ldapsearch -LLL '(mailPrimaryAddress=user@example.com)' dn entryUUID entryCSN
Only the current LDAP entry should remain, matching the DN on the Primary.
On the mail server, verify that Dovecot can resolve the user:
doveadm user -f home user@example.com
The command should return the mailbox home directory instead of Auth USER lookup failed.
Finally, verify IMAP login and mail delivery for the affected account.