Problem: Duplicate LDAP user entries on a Replica cause Dovecot authentication failures

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 entryCSN on 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 entryUUID on the Replica.
  • Check mail/dovecot/mailbox/delete on 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.