
Provision an OpenLDAP User and Add Them to Groups With a Script
Before you run this
This script creates one LDAP user entry (an inetOrgPerson / posixAccount) under your people container and adds that user's uid as a memberUid to one or more existing groups. Its purpose is to make user provisioning repeatable instead of hand-crafting LDIF every time.
- Privileges: You do not need root to run it. You do need the directory's rootDN (admin) bind credentials — by default
cn=admin,dc=example,dc=comon a Debian/Ubuntuslapdinstall — because writing to the directory requires an authenticated bind. The script prompts for that password with-W; it never puts it on the command line. - Tooling:
ldapmodify,ldapsearch, andldapdeletecome from theldap-utilspackage.slappasswdships with theslapdserver package, so if you run this from a client machine without the server installed, generate the password hash on the LDAP server instead, or install accordingly. - Test first. Read the script, then run it with
DRY_RUN=1(the default) so it only prints the LDIF it would apply. Try it against a test directory or a throwaway user before you point it at production. Never paste-and-run this blind on a live directory. - What it changes: It adds an entry and modifies existing groups. Adding is reversible (delete the entry, remove the
memberUid— shown at the end). But the entry is written to your live directory the momentDRY_RUN=0, so treat it with the same care asuseraddon a shared box. If a target group does not exist, the group modification fails with "No such object" — the script adds users to groups, it does not create them.
Assumptions
I'm assuming a standard Debian 12 / Ubuntu slapd install with:
- Base DN
dc=example,dc=com, people underou=people, groups underou=groups(both containers already exist). - POSIX login groups modelled as
posixGroupwithmemberUidattributes. If instead you usegroupOfNameswith full-DNmemberattributes, the group-modification part differs — you'd add amemberDN, not amemberUid. That's the other common model; I'm writing formemberUid. - You're on the server or a machine that can reach it, using simple bind (
-x).
Replace every example.com / dc=example,dc=com and the sample user details with your own.
The script
#!/usr/bin/env bash
set -euo pipefail
# ---- Directory settings (edit for your environment) ----
BASE_DN="dc=example,dc=com"
PEOPLE_OU="ou=people,${BASE_DN}"
GROUP_OU="ou=groups,${BASE_DN}"
ADMIN_DN="cn=admin,${BASE_DN}"
LDAP_URI="ldap://localhost" # use ldaps://ldap.example.com over a network
LOGIN_SHELL="/bin/bash"
DRY_RUN=1 # 1 = print the LDIF only; 0 = actually apply it
# ---- Details for THIS user (edit each run, or wire up to args) ----
USERNAME="jdoe"
GIVEN_NAME="John"
SURNAME="Doe"
UID_NUMBER="10001" # must be unique in your directory
GID_NUMBER="10001" # the user's primary group id
MAIL="jdoe@example.com"
GROUPS=("developers" "vpn-users") # existing posixGroup cn values
# ---- Generate the password hash (prompts twice, prints {SSHA}...) ----
HASH="$(slappasswd)"
# ---- Build one LDIF: add the user, then modify each group ----
LDIF="$(mktemp)"
trap 'rm -f "$LDIF"' EXIT # never leave the hash lying in /tmp
cat > "$LDIF" <<EOF
dn: uid=${USERNAME},${PEOPLE_OU}
changetype: add
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
uid: ${USERNAME}
cn: ${GIVEN_NAME} ${SURNAME}
sn: ${SURNAME}
givenName: ${GIVEN_NAME}
mail: ${MAIL}
uidNumber: ${UID_NUMBER}
gidNumber: ${GID_NUMBER}
homeDirectory: /home/${USERNAME}
loginShell: ${LOGIN_SHELL}
userPassword: ${HASH}
EOF
# One modify record per group. The blank line before each dn separates records.
for g in "${GROUPS[@]}"; do
cat >> "$LDIF" <<EOF
dn: cn=${g},${GROUP_OU}
changetype: modify
add: memberUid
memberUid: ${USERNAME}
EOF
done
# ---- Apply or preview ----
if [[ "$DRY_RUN" -eq 1 ]]; then
echo "----- DRY RUN: this is the LDIF that would be applied -----"
cat "$LDIF"
echo "----- Set DRY_RUN=0 to apply. Nothing was changed. -----"
exit 0
fi
ldapmodify -x -H "$LDAP_URI" -D "$ADMIN_DN" -W -f "$LDIF"
echo "Done. Verify below."
A few notes on the non-obvious parts:
- I use a single
ldapmodifywith explicitchangetype:lines rather than separateldapaddandldapmodifycalls. That means one bind, one password prompt, and the whole change is described in one LDIF you can read in the dry run.ldapaddis simplyldapmodify -a; usingldapmodifywithchangetype: addis the same operation. - LDIF records are separated by a blank line. That's why each group heredoc starts with an empty line.
slappasswdwith no arguments prompts for the new user's password and prints a salted{SSHA}hash. It does not touch the directory — it only produces the hash string that goes intouserPassword.set -euo pipefailstops the script on the first error, so a failed group add (e.g. a group that doesn't exist) doesn't get silently ignored. Note that the user entry is added first; if a later group modify fails, the user still exists — check the undo section.
Run it:
chmod +x provision-ldap-user.sh
./provision-ldap-user.sh # DRY_RUN=1: prints the LDIF, changes nothing
Read the LDIF. When it's right, set DRY_RUN=0 and run again.
One caution on transport
With -x (simple bind), the admin password and the entry travel to the server as sent. Over ldap://localhost that stays on the box. Over a network, point LDAP_URI at ldaps://ldap.example.com (or use StartTLS) so credentials aren't sent in the clear.
Verify it worked
Confirm the entry exists and has the attributes you expect:
ldapsearch -x -H ldap://localhost \
-b "uid=jdoe,ou=people,dc=example,dc=com"
Confirm the group memberships took:
ldapsearch -x -H ldap://localhost \
-b "ou=groups,dc=example,dc=com" \
"(memberUid=jdoe)" cn
That second search returns the cn of every group jdoe now belongs to. If NSS/PAM on your clients is configured to read from LDAP, id jdoe and getent passwd jdoe on a bound client are a good end-to-end check — but that's client configuration, outside this script.
Undo
Removing a user is a delete plus removing them from any groups. Do the group removals first, then delete the entry.
Remove the memberUid from each group (repeat per group, or script the loop the same way):
ldapmodify -x -H ldap://localhost -D "cn=admin,dc=example,dc=com" -W <<'EOF'
dn: cn=developers,ou=groups,dc=example,dc=com
changetype: modify
delete: memberUid
memberUid: jdoe
EOF
Then delete the user entry:
ldapdelete -x -H ldap://localhost -D "cn=admin,dc=example,dc=com" -W \
"uid=jdoe,ou=people,dc=example,dc=com"
After that, re-run the two ldapsearch checks above — the entry lookup should return "No such object" and the memberUid search should return nothing.
For the exact attribute requirements of each objectClass and the full option list for these tools, see the OpenLDAP Software Administrator's Guide and the ldapmodify, ldapsearch, ldapdelete, and slappasswd man pages on your system.
Runs enterprise networks and security for a living, and writes Shore Up to turn two decades of hands-on Linux, Windows and mail-server work into guides you can actually use.
More about the author →Was this article helpful?
Tap a star — no sign-in needed.
Be the first to rate this article.
Related guides
Provision Linux Users and Groups from a CSV with Bash
Creating one Linux account by hand is fine. Creating thirty from a spreadsheet a manager emailed you is a job for a script. This walks through a small, boring, reliable bash script that reads a CSV and provisions local users and their groups with the standard shadow-utils tools.
Bulk-Create Zimbra Accounts from a CSV with zmprov
This procedure reads a CSV of new users and generates a batch file of createAccount commands that zmprov executes to create real, live mailboxes on your Zimbra server. Creating accounts is a provisioning action that touches your directory and mail store. It is not destructive on its own — but the rollback (deleting an account) is irreversible: deleteAccount removes the mailbox and all of its mail with no undo. Treat the delete step accordingly.
Audit World-Writable Files and SUID Binaries with Bash
This script reads your filesystem and reports two classes of risky files: world-writable files and directories (anyone on the box can modify them) and SUID/SGID binaries (they run with the owner's or group's privileges, often root). It writes a timestamped report and, optionally, a baseline you can diff against later. It does not change any permissions, delete anything, or modify a single file — it only runs find and writes a text report to a directory you choose.
Automate iptables/nftables Backup and Restore
This guide sets up two things: a small script that dumps your host's live packet-filter ruleset to a timestamped, versioned file, and a scheduled systemd timer to run it. It also shows the standard restore path. The backup part is read-only and safe. The restore part is not — reloading a ruleset replaces your firewall's entire running state in one transaction, and a bad ruleset can drop your SSH session and cut the host off the network instantly.




