How to Set Up LDAP Authentication on FreeBSD
Managing user accounts across multiple FreeBSD servers with local passwd files does not scale. Every new hire means updating every server. Every password change means logging into every machine. LDAP solves this by storing user accounts, groups, SSH public keys, and sudo rules in a central directory that all servers query at authentication time.
This guide covers a complete LDAP authentication deployment on FreeBSD 14.x: installing and configuring an OpenLDAP server, defining user and group schemas, configuring FreeBSD clients to authenticate against LDAP via PAM and NSS, storing SSH public keys in LDAP for key-based login, and centralizing sudo authorization rules. Every command and configuration has been tested on FreeBSD 14.
Architecture Overview
The setup involves two roles:
- LDAP server: runs
slapd(the OpenLDAP daemon), stores the directory database, handles authentication queries. Typically one primary server with optional replication. - LDAP clients: FreeBSD servers that query the LDAP server during login. They run
nslcd(name service LDAP client daemon) and use PAM modules to authenticate users against the directory.
A user logs into any client server via SSH. PAM checks the LDAP directory for the user's credentials. NSS resolves the user's UID, GID, home directory, and shell from LDAP. If the user exists in LDAP and the password matches, login succeeds. The user's SSH public key can also be stored in LDAP and fetched by sshd at connection time.
Installing the OpenLDAP Server
Install OpenLDAP server and client tools:
shpkg install openldap26-server openldap26-client
Enable the service:
shsysrc slapd_enable="YES" sysrc slapd_flags='-h "ldapi:/// ldap:/// ldaps:///"'
The flags tell slapd to listen on Unix socket (ldapi), plaintext (ldap, port 389), and TLS-encrypted (ldaps, port 636) connections.
Configuring the LDAP Server
OpenLDAP uses slapd.conf or the newer cn=config (OLC) backend. This guide uses slapd.conf for clarity and auditability.
Generate a password hash for the LDAP admin:
shslappasswd
Copy the hash output. Edit the server configuration:
shvi /usr/local/etc/openldap/slapd.conf
shellinclude /usr/local/etc/openldap/schema/core.schema include /usr/local/etc/openldap/schema/cosine.schema include /usr/local/etc/openldap/schema/inetorgperson.schema include /usr/local/etc/openldap/schema/nis.schema include /usr/local/etc/openldap/schema/openssh-lpk.schema pidfile /var/run/openldap/slapd.pid argsfile /var/run/openldap/slapd.args modulepath /usr/local/libexec/openldap moduleload back_mdb # TLS configuration TLSCACertificateFile /usr/local/etc/openldap/certs/ca.crt TLSCertificateFile /usr/local/etc/openldap/certs/server.crt TLSCertificateKeyFile /usr/local/etc/openldap/certs/server.key # Access control access to attrs=userPassword by self write by anonymous auth by dn="cn=admin,dc=example,dc=com" write by * none access to * by dn="cn=admin,dc=example,dc=com" write by self read by users read by anonymous auth # Database definition database mdb maxsize 1073741824 suffix "dc=example,dc=com" rootdn "cn=admin,dc=example,dc=com" rootpw {SSHA}YOUR_HASHED_PASSWORD_HERE directory /var/db/openldap-data index objectClass eq index uid eq index cn eq index gidNumber eq index uidNumber eq index memberUid eq
Replace dc=example,dc=com with your actual domain and paste the hash from slappasswd into the rootpw line.
Create the data directory:
shmkdir -p /var/db/openldap-data chown ldap:ldap /var/db/openldap-data
SSH Public Key Schema
To store SSH public keys in LDAP, you need the openssh-lpk schema. If it is not included with your OpenLDAP package, create it:
shvi /usr/local/etc/openldap/schema/openssh-lpk.schema
shellattributetype ( 1.3.6.1.4.1.24552.500.1.1.1.13 NAME 'sshPublicKey' DESC 'OpenSSH public key' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 ) objectclass ( 1.3.6.1.4.1.24552.500.1.1.2.0 NAME 'ldapPublicKey' DESC 'OpenSSH public key objectclass' SUP top AUXILIARY MAY ( sshPublicKey $ uid ) )
TLS Certificate Setup
Production LDAP must use TLS. For testing, generate a self-signed certificate. For production, use certificates from Let's Encrypt or your internal CA.
shmkdir -p /usr/local/etc/openldap/certs cd /usr/local/etc/openldap/certs openssl req -new -x509 -nodes -days 3650 \ -keyout server.key -out server.crt \ -subj "/CN=ldap.example.com" cp server.crt ca.crt chown ldap:ldap server.key server.crt ca.crt chmod 400 server.key
Test the configuration and start the server:
shslaptest -f /usr/local/etc/openldap/slapd.conf service slapd start
Verify the server is listening:
shsockstat -l | grep slapd
Populating the Directory
Create the base directory structure. Write an LDIF file:
shvi /tmp/base.ldif
ldifdn: dc=example,dc=com objectClass: top objectClass: dcObject objectClass: organization o: Example Organization dc: example dn: ou=People,dc=example,dc=com objectClass: organizationalUnit ou: People dn: ou=Groups,dc=example,dc=com objectClass: organizationalUnit ou: Groups
Import it:
shldapadd -x -D "cn=admin,dc=example,dc=com" -W -f /tmp/base.ldif
Adding Users
Create a user entry with SSH public key:
shvi /tmp/user-jdoe.ldif
ldifdn: uid=jdoe,ou=People,dc=example,dc=com objectClass: inetOrgPerson objectClass: posixAccount objectClass: shadowAccount objectClass: ldapPublicKey cn: John Doe sn: Doe uid: jdoe uidNumber: 10001 gidNumber: 10001 homeDirectory: /home/jdoe loginShell: /bin/sh userPassword: {SSHA}HASHED_PASSWORD sshPublicKey: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... jdoe@workstation
Generate the password hash and replace:
shslappasswd -s "temporarypassword"
Import the user:
shldapadd -x -D "cn=admin,dc=example,dc=com" -W -f /tmp/user-jdoe.ldif
Adding Groups
Create a POSIX group:
shvi /tmp/group-sysadmin.ldif
ldifdn: cn=sysadmin,ou=Groups,dc=example,dc=com objectClass: posixGroup cn: sysadmin gidNumber: 10001 memberUid: jdoe
shldapadd -x -D "cn=admin,dc=example,dc=com" -W -f /tmp/group-sysadmin.ldif
Verify the entries:
shldapsearch -x -b "dc=example,dc=com" "(uid=jdoe)"
Configuring FreeBSD LDAP Clients
On each FreeBSD server that should authenticate against LDAP, install the required packages:
shpkg install nss-pam-ldapd openldap26-client
nss-pam-ldapd provides nslcd, the daemon that resolves NSS queries (user, group, host lookups) against LDAP, and the PAM module for authentication.
Configure nslcd
Edit the nslcd configuration:
shvi /usr/local/etc/nslcd.conf
shelluid nslcd gid nslcd uri ldaps://ldap.example.com base dc=example,dc=com binddn cn=admin,dc=example,dc=com bindpw your_admin_password ssl on tls_cacertfile /usr/local/etc/openldap/certs/ca.crt scope sub filter passwd (objectClass=posixAccount) filter group (objectClass=posixGroup) filter shadow (objectClass=shadowAccount) map passwd homeDirectory homeDirectory map passwd loginShell loginShell
For production, create a read-only bind account instead of using the admin DN. Set restrictive permissions:
shchmod 600 /usr/local/etc/nslcd.conf chown nslcd:nslcd /usr/local/etc/nslcd.conf
Copy the CA certificate from the LDAP server to the client:
shscp ldap-server:/usr/local/etc/openldap/certs/ca.crt /usr/local/etc/openldap/certs/
Configure NSS
Edit /etc/nsswitch.conf to add LDAP as a source for passwd and group:
shvi /etc/nsswitch.conf
Change the relevant lines:
shellpasswd: files ldap group: files ldap shadow: files ldap
Configure PAM
Edit the PAM configuration for SSH:
shvi /etc/pam.d/sshd
Add the LDAP PAM module. The file should look like:
shellauth sufficient /usr/local/lib/pam_ldap.so auth required pam_unix.so no_warn try_first_pass auth required pam_login_access.so account sufficient /usr/local/lib/pam_ldap.so account required pam_unix.so session required pam_permit.so session optional /usr/local/lib/pam_ldap.so session optional pam_mkhomedir.so skel=/etc/skel umask=0077 password sufficient /usr/local/lib/pam_ldap.so password required pam_unix.so no_warn try_first_pass
The pam_mkhomedir.so module automatically creates the user's home directory on first login.
Also update /etc/pam.d/system for console and su authentication:
shvi /etc/pam.d/system
shellauth sufficient /usr/local/lib/pam_ldap.so auth required pam_unix.so no_warn try_first_pass account sufficient /usr/local/lib/pam_ldap.so account required pam_unix.so session required pam_permit.so session optional /usr/local/lib/pam_ldap.so session optional pam_mkhomedir.so password sufficient /usr/local/lib/pam_ldap.so password required pam_unix.so no_warn try_first_pass
Enable and Start Services
shsysrc nslcd_enable="YES" service nslcd start
Test user resolution:
shid jdoe getent passwd jdoe getent group sysadmin
If id jdoe returns the UID, GID, and group memberships from LDAP, NSS integration is working.
SSH Public Key Lookup from LDAP
Configure sshd to fetch authorized keys from LDAP instead of (or in addition to) ~/.ssh/authorized_keys.
Create a script that queries LDAP for the user's SSH key:
shvi /usr/local/bin/ldap-ssh-keys.sh
sh#!/bin/sh ldapsearch -x -H ldaps://ldap.example.com \ -b "ou=People,dc=example,dc=com" \ -D "cn=admin,dc=example,dc=com" \ -w "your_bind_password" \ "(uid=$1)" sshPublicKey | \ sed -n 's/^sshPublicKey: //p'
shchmod 755 /usr/local/bin/ldap-ssh-keys.sh
Test it:
sh/usr/local/bin/ldap-ssh-keys.sh jdoe
It should output the SSH public key stored in LDAP.
Configure sshd to use this script:
shvi /etc/ssh/sshd_config
Add:
shellAuthorizedKeysCommand /usr/local/bin/ldap-ssh-keys.sh AuthorizedKeysCommandUser nobody
Restart SSH:
shservice sshd restart
Now users can log in with the SSH key stored in their LDAP entry, without needing a local authorized_keys file.
Sudo Authorization via LDAP
Centralize sudo rules so that group membership in LDAP controls who can run privileged commands.
The simplest approach uses local sudoers with LDAP group references. On each client:
shpkg install sudo visudo
Add:
shell%sysadmin ALL=(ALL) ALL
Since sysadmin is a POSIX group in LDAP, and NSS resolves group membership from LDAP, any user whose memberUid appears in the sysadmin group gets sudo access. No per-server configuration needed -- just manage group membership in LDAP.
To add a user to the sysadmin group:
shvi /tmp/add-to-group.ldif
ldifdn: cn=sysadmin,ou=Groups,dc=example,dc=com changetype: modify add: memberUid memberUid: newuser
shldapmodify -x -D "cn=admin,dc=example,dc=com" -W -f /tmp/add-to-group.ldif
The change takes effect on the next sudo invocation -- no restart needed.
Managing Users
Change a user's password:
shldappasswd -x -D "cn=admin,dc=example,dc=com" -W -S "uid=jdoe,ou=People,dc=example,dc=com"
Disable a user (lock the account):
shvi /tmp/lock-user.ldif
ldifdn: uid=jdoe,ou=People,dc=example,dc=com changetype: modify replace: loginShell loginShell: /usr/sbin/nologin
shldapmodify -x -D "cn=admin,dc=example,dc=com" -W -f /tmp/lock-user.ldif
Delete a user:
shldapdelete -x -D "cn=admin,dc=example,dc=com" -W "uid=jdoe,ou=People,dc=example,dc=com"
List all users:
shldapsearch -x -b "ou=People,dc=example,dc=com" "(objectClass=posixAccount)" uid cn
Security Hardening
Use a read-only bind account for clients. Do not use the admin DN in nslcd.conf. Create a dedicated bind user:
ldifdn: cn=readonly,dc=example,dc=com objectClass: organizationalRole objectClass: simpleSecurityObject cn: readonly userPassword: {SSHA}HASHED_PASSWORD
Update access controls in slapd.conf:
shellaccess to attrs=userPassword by self write by anonymous auth by dn="cn=admin,dc=example,dc=com" write by dn="cn=readonly,dc=example,dc=com" read by * none
Require TLS for all connections. Add to slapd.conf:
shellsecurity tls=1
Restrict network access with PF:
shellpass in on egress proto tcp from 10.0.0.0/24 to any port { 389, 636 } block in on egress proto tcp to any port { 389, 636 }
Enable audit logging. Add to slapd.conf:
shellloglevel stats
Configure syslog to capture LDAP logs:
shvi /etc/syslog.conf
Add:
shelllocal4.* /var/log/ldap.log
shservice syslogd restart
Troubleshooting
nslcd cannot connect to the LDAP server:
Check connectivity:
shopenssl s_client -connect ldap.example.com:636 -CAfile /usr/local/etc/openldap/certs/ca.crt
Check nslcd logs:
shtail -50 /var/log/messages | grep nslcd
User exists in LDAP but id does not resolve:
Verify nslcd is running:
shservice nslcd status
Verify NSS configuration:
shgetent passwd | grep jdoe
Test LDAP query directly:
shldapsearch -x -H ldaps://ldap.example.com -b "ou=People,dc=example,dc=com" "(uid=jdoe)"
PAM authentication fails:
Check PAM module load:
shls -la /usr/local/lib/pam_ldap.so
Test authentication with pamtester if installed, or check auth logs:
shtail -50 /var/log/auth.log
FAQ
Can I use LDAP alongside local accounts?
Yes. The nsswitch.conf line passwd: files ldap checks local files first, then LDAP. Local accounts (root, service accounts) always work even if LDAP is unreachable. LDAP users supplement local accounts.
What happens if the LDAP server goes down?
Users already logged in stay logged in. New SSH connections for LDAP-only users will fail because PAM cannot verify credentials. Local accounts are unaffected. For high availability, set up LDAP replication with a second server and list both URIs in nslcd.conf.
Should I use OpenLDAP or 389 Directory Server?
OpenLDAP is the standard on FreeBSD and available as a binary package. 389 Directory Server (formerly Fedora DS) has a richer admin interface but is not as well supported on FreeBSD. For most deployments of under 10,000 users, OpenLDAP is the pragmatic choice.
How do I replicate LDAP for high availability?
OpenLDAP supports syncrepl for replication. Add a syncrepl directive in the consumer's slapd.conf pointing to the provider. The consumer maintains a synchronized copy of the directory. Configure nslcd.conf on clients with multiple uri directives for failover.
Can LDAP users have different shells or home directories per server?
The shell and home directory are stored in the LDAP entry and apply everywhere. If you need per-server overrides, use nslcd.conf map directives to rewrite attributes, or use local /etc/passwd entries to override specific users.
How do I migrate existing local users to LDAP?
Extract user entries from /etc/passwd and /etc/master.passwd, convert them to LDIF format, and import with ldapadd. A script that reads pw usershow -a and generates LDIF entries is the fastest approach. After migration, test thoroughly before removing local accounts.