OpenBSD + LDAP

You have a bunch of user accounts that you want to keep synchronized across multiple systems and services. How would you do that?

OpenBSD + LDAP
Photo by Aubrey Odom-Mabey / Unsplash
Update: 2022/11/16
Fixed typo. Noticed by Matto F
Update: 2022/10/17
Negelected to start ypbind(8). This prevents the exchange of account information between the OS and ypldap. Pointed out here.
Original Publish: 2022/07/17

Lets say you have a bunch of user accounts that you want to keep synchronized across multiple systems and services.  How would you do that? We can break out Ansible/Puppet/Chef or some fancy scripting.  Or instead, can turn the problem around and have the systems and services come to one place.  That place is LDAP!

Lightweight Directory Access Protocol (LDAP) a standardized method for accessing data within a database.  (Yes, LDAP isn't actually a database). What the database actually is, doesn't matter for this discussion.  While there are many reasons why you might not want to use LDAP, the one argument for its use is that just about every service you will want to authenticate to, can communicate with an LDAP server.  

In this article I will walk through a simple LDAP server setup and configuration an configure several different services to utilize LDAP for authentication.  For this article I will be using the LDAP daemon that is built-in to OpenBSD. This daemon does not have all the features of OpenLDAP or other larger projects.  If you need those features, then you already know it and this article will, most likely, not provide any new insight. Please note that I am glossing a lot of options and background to make this as simple as possible. My goal to is get a simple LDAP server up and functional with a schema and user accounts so services can authenticate users.

Groundwork

First thing to do is setup the server and database.  And first item to do on the server configuration is to decide on a domain name.   For this example I'll use the ubiquitous example.com.

First config section we will import the schemas we will use.  A schema in LDAP is like the table/field definitions in SQL.  It tells the LDAP server what fields can exist, how to identify and treat each field (is it case sensitive, can it be repeated, etc).  The schemas below are the standard schemas that come with OpenBSD.  Additional schemas can be added if needed.

schema "/etc/ldap/core.schema"
schema "/etc/ldap/inetorgperson.schema"
schema "/etc/ldap/nis.schema"
schema "/etc/ldap/bsd.schema"
Partial ldapd.conf

Next we will tell ldapd(8) what interface(s) to listen on.   We will listen on the loopback interface and mark it as secure.  This allows us the ability to authenticate with plain text passwords. (For testing this might be helpful) We will also listen on the external interface, vio0 in this example, and mandate that LDAPS if required.

listen on lo0 secure
listen on "/var/run/ldapi"
listen on vio0 ldaps certificate <hostname>
Partial ldapd.conf

The certificates are stored in the /etc/ldap/certs directory.  Make sure the directory is not group or world readable.  If there is already a certificate in /etc/ssl then a simple symlink to the cert and key can be made.

# chmod 700 /etc/ldap/certs
# ln -s /etc/ssl/<hostname>.crt /etc/ldap/certs/<hostname>.crt
# ln -s /etc/ssl/private/<hostname>.key /etc/ldap/certs/<hostname>.key

Next we need to have a place to store the information.  We need to "create" the database.  Couple of things to note: A) we specify the domain in LDAP format using the dc=example,dc=com syntax.  This will get very tedious to type out over time. Fortunately I have some scripts to help.  B) the rootpw password can be in plain text but you really shouldn't do that.  Using the script salted_passwd.sh will create a salted SHA1 hash for use.

$ salted_passwd.sh                                      
enter password (will not echo): letmein
{SSHA}vpeFJlj9iz/2UqQ0eGcU9PzeP/lyVG1VQWV3Qg==
namespace "dc=example,dc=com" {
        rootdn          "cn=admin,dc=example,dc=com"
        rootpw          "{SSHA}vpeFJlj9iz/2UqQ0eGcU9PzeP/lyVG1VQWV3Qg=="
        index           sn
        index           givenName
        index           cn
        index           mail
        index           ou
}
Partial ldapd.conf

The next next thing is to what fields an index should be created for.  This can be updated and changed after the database has already been created, but may require a re-scan of all data, which in large environment can take a significant amount of time.  (But in our environment this won't be an issue.)

The last item in the config are are the "rules" or ACL.  These lines tell the LDAP server what it's allowed to show and to whom.   (NOTE: for the rootdn user, the rules do not apply.)

# read access allowed to subtrees
allow read access to subtree "dc=example,dc=com" by self
# deny access to users directly reading the userPassword field
deny read access to subtree "ou=people,dc=example,dc=com" attribute "userPassword"
## Allow ourselves to write the attribute
allow read,write access to subtree "ou=people,dc=example,dc=com" attribute "userPassword" by self
allow read,write access to subtree "ou=people,dc=example,dc=com" attribute "description" by self
## Allow SMTP services to read the password
allow read access to subtree "ou=people,dc=example,dc=com" attribute "description" by "cn=smtpd,ou=services,dc=example,dc=com"
## Allow IMAP services to read the password
allow read access to subtree "ou=people,dc=example,dc=com" attribute "userPassword" by "cn=imapd,ou=services,dc=example,dc=com"
Partial ldapd.conf

Additional Packages

As of this article OpenBSD 7.1 ldap(8) command does not support add/modify/delete commands.  To work around this issue the openldap-client package needs to be installed.  To install the package use the pkg_add(8) command.

# pkg_add openldap-client

Start the server

The ldapd.conf file should be stored in the /etc/ directory.  To check syntax run

# ldapd -nv

This will check the syntax of the config for errors and, if errors are found, identify what line the errors are at.

Once the configuration is confirmed, start the service.  This can be done using the rcctl(8) script. First we enable the service, then we start it.

# rcctl enable ldapd
# rcctl start ldapd

Adding the data

As with any database, we need to populate it with data.  We will start by creating the OU structure then we will add the user accounts.  I created some scripts to help make this easier.   This schema is not the only schema that is possible.  It's just a schema that I use and makes sense to me.  Feel free to update as you see fit.

The init_schema.sh script will create an LDIF file.  This LDIF file can then be used to initialize the LDAP schema.

$ ./init_schema.sh -o init.ldif example.com
$ cat init.ldif
#
# Simple LDAP Schema
#
dn: dc=example,dc=com
objectclass: dcObject
objectclass: organization
dc: example
o: example.com LDAP Server
description: Root entry for example.com

# First level
dn: ou=people,dc=example,dc=com
objectclass: organizationalUnit
ou: people
description: All people in organization

dn: ou=groups,dc=example,dc=com
objectclass: organizationalUnit
ou: groups
description: All groups in organization

dn: ou=domains,dc=example,dc=com
objectclass: organizationalUnit
ou: domains
description: All domains in organization

dn: ou=services,dc=example,dc=com
objectclass: organizationalUnit
ou: services
description: All sevices in organization

# Second level
dn: dc=example.com,ou=domains,dc=example,dc=com
objectclass: domain
dc: example.com
description: Main domain

Import the schema into the server.

$ upload_ldif.sh init.ldif

Once the schema is in place we can use the user and group scripts to add user and group accounts as we see fit.

$ create_user.sh -o bob.ldif bob                                       
enter password (will not echo):
First Name [First bob]: bob
Last Name [Last bob]: user

$ cat bob.ldif
#
# Create User bob
#
dn: uid=bob,ou=people,dc=example,dc=com
objectclass: person
objectclass: inetOrgPerson
objectclass: posixAccount
uid: bob
cn: bob
sn: user
uidNumber: 2000
gidNumber: 2000
homeDirectory: /var/mail/vmail
givenName: bob
displayName: bob user
mail: [email protected]
userPassword: {CRYPT}$2b$10$cHzi.KxtfOl1EBr0nBA9Te3WN8p43vTH31FaMkfWvmaG28CsTARxi
description: $2b$10$cHzi.KxtfOl1EBr0nBA9Te3WN8p43vTH31FaMkfWvmaG28CsTARxi

Now add the user to the server.

$ upload_ldif.sh bob.ldif

Show the information.

$ ldapsearch -vv -W -h localhost -D "cn=admin,dc=example,dc=com" -b "dc=example,dc=com" uid=bob
ldap_initialize( ldap://localhost )
Enter LDAP Password: 
filter: uid=bob
requesting: All userApplication attributes
# extended LDIF
#
# LDAPv3
# base <dc=example,dc=com> with scope subtree
# filter: uid=bob
# requesting: ALL
#

# bob, people, example.com
dn: uid=bob,ou=people,dc=example,dc=com
objectclass: person
objectclass: inetOrgPerson
objectclass: posixAccount
uid: bob
cn: bob
sn: user
uidNumber: 2000
gidNumber: 2000
homeDirectory: /var/mail/vmail
givenName: bob
displayName: bob user
mail: [email protected]
userPassword:  {CRYPT}$2b$10$cHzi.KxtfOl1EBr0nBA9Te3WN8p43vTH31FaMkfWvmaG28CsTARxi
description:  $2b$10$cHzi.KxtfOl1EBr0nBA9Te3WN8p43vTH31FaMkfWvmaG28CsTARxi

# search result
search: 2
result: 0 Success

# numResponses: 2
# numEntries: 1

You now have a working LDAP server.  Now create as many users as you need.  But what can you do with it?

Using LDAP

OpenBSD server

Configuring OpenBSD to authenticate a user against LDAP really very easy.  A couple of tweaks to the login.conf(5) and creating a login_ldap.conf file is all you need.  But... you still need to create an account for each user. If that is one or two, then it's no problem.  However, what can we do if it's 10, 20 or 100 across as many systems.  There is an answer, but first a quick aside.

OpenBSD has a very straight forward login process.  While this process may seem limiting at first glance, it is very secure.  As such, user accounts are either kept in the local user store (master.passwd(5)) or in the yp(8) directory store.  YP was written when the network was a kinder and gentler place.  (read: YP is not secure.)  Conveniently, OpenBSD provides a YP to LDAP "bridge" called ypldap(8). This service acts like a YP server for the OpenBSD system, serving the user and group requests with data it queries out of the LDAP database.  This is what allows us communicate with and LDAP backend.

First step is to create an login_ldap.conf file (see login_ldap(8) for description of settings).  This file will take the username and password and try and LDAP bind.  If that bind succeeds, the user is authenticated.

host=ldaps://127.0.0.1
cacert=/etc/ssl/cert.pem
timeout=15
binddn=uid=%u,ou=people,dc=example,dc=com
login_ldap.conf
NOTE: The certificate that the ldap server is using must have the IP address in it's SAN and the cert authority must be trusted, i.e. it must be listed in (or added to) the /etc/ssl/cert.pem file.

Next enable ldap in the login.conf(5) file by modifying the auth-default line as seen below.

# find the line starting with 'auth-defaults' and add ldap

# Default allowed authentication styles
auth-defaults:auth=ldap,passwd,skey:
excerpt from login.conf
NOTE: if there is a problem with testing and you need to login with a local user+password that is not in LDAP, you can login using the username:auth_style format.
# ssh localuser:[email protected]
example authenticating using the login style

Now this much will allow an existing account in the passwd(5) file authenticate against LDAP.  To have the system use any LDAP account we must enable the directory serivces.

First set the directory services domain by creating a /etc/defaultdomain file

echo "example.com" > /etc/defaultdomain

Enable and start the portmap and ypbind processes.

# rcctl enable portmap ypbind
# rcctl start portmap ypbind

When the ypldap(8) service starts up it tries to connect to find the YP server.  It will use the YP server database located in the /var/yp/<domainname>/ypservers.db file.  This file is created using the ypinit(8) program.  That program will ask for server name(s) to add to the database.  If the interface associated with the name of the server does not have an IP address, then the ypserv(8) or ypldap(8) process will hang for 3 or more minutes. This is incredibly annoying for systems that use DHCP because there is no way to guarantee that the device has an IP address before the ypserv/ypldap service starts.  To avoid this we temporarily change the hostname of the system to 'localhost' run the ypinit(8) program to initialize the YP database and then change the hostname back.  This ensures that the ypldap process connects to the localhost interface which should always be up.

# hostname localhost
# hostname
localhost
# ypinit -m
Server Type: MASTER Domain: example.com

Creating an YP server will require that you answer a few questions.
Questions will all be asked at the beginning of the procedure.

Do you want this procedure to quit on non-fatal errors? [y/n: n]  y

At this point, we have to construct a list of this domain's YP servers.
localhost is already known as master server.
Please continue to add any slave servers, one per line. When you are
done with the list, type a <control D>.
        master server   :  localhost
        next host to add:  ^D
The current list of NIS servers looks like this:

localhost

Is this correct?  [y/n: y]  y
Building /var/yp/example.com/ypservers...
localhost has been set up as a YP master server.
Edit /var/yp/example.com/Makefile to suit your needs.
After that, run `make' in /var/yp.

# hostname <servername>

Almost there... Just create a ypldap.conf file.

# 

domain          "example.com"
interval        60 # this can be bumped up once everything is working.
provide map     "passwd.byname"
provide map     "passwd.byuid"
provide map     "group.byname"
provide map     "group.bygid"
provide map     "netid.byname"

directory 127.0.0.1 ldaps {
        basedn "ou=people,dc=example,dc=com"
        bindcred "letmein"
        binddn "cn=auth,ou=services,ddc=example,dc=com"

        # starting point for groups directory search, default to basedn
        #groupdn "ou=Groups,dc=example,dc=com"

        # passwd maps configuration (RFC 2307 posixAccount object class)
        passwd filter "(objectClass=posixAccount)"

        attribute name maps to "uid"
#       attribute passwd maps to "userPassword"
        fixed attribute passwd "*"
        # Can also make uid,gid,home static to place all users 
        # in the same user directory with the same perms.  This
        # will cause other problems depending on situation
        attribute uid maps to "uidNumber"
        attribute gid maps to "gidNumber"
        attribute home maps to "homeDirectory"
        attribute gecos maps to "cn"
        # force login shell to specific shell
#        attribute shell maps to "loginShell"
        fixed attribute shell "/bin/ksh"
        fixed attribute change "0"
        fixed attribute expire "0"
        fixed attribute class ""

        # group maps configuration (RFC 2307 posixGroup object class)
        group filter "(objectClass=posixGroup)"

        attribute groupname maps to "cn"
        fixed attribute grouppasswd "*"
        attribute groupgid maps to "gidNumber"
        # memberUid returns multiple group members
        list groupmembers maps to "memberUid"
}

In the home stretch, we just enable and start the ypldap(8) process.

# rcctl enable ypldap
# rcctl start ypldap

And now "enable" the directory service lookup in the password file by adding the flag in the master.passwd file and rebuilding the db.

# echo '+:*::::::::' >> /etc/master.passwd
# pwd_mkdb -p /etc/master.passwd

Now we can check to see if we see our account.  Use the getent(1) to get the passwd file that the systems sees.

# getent passwd
< service accounts removed>
nobody:*:32767:32767:Unprivileged user:/nonexistent:/sbin/nologin
bob:*:2000:2000:bob:/var/mail/vmail:/bin/ksh
larry:*:2000:2000:larry:/var/mail/vmail:/bin/ksh
test:*:1001:1001:T:/home/test:/bin/ksh

If everything is working, we should see our LDAP users and entries in the output.  Check the /var/log/authlog file for error is a user cannot login.

SMTPD Service

NOTE: These instructions assume that smtpd(8) is already setup and functining normally and the only thing being added is LDAP.  

To enable the SMTPD service to utilize LDAP for authentication of users first create a /etc/mail/ldapd.conf file for smtpd(8).  The filters in this config correspond to the different tables that smtpd(8) uses.  See table(5) for more info.

#
#
url ldap://127.0.0.1
# we will create this users in a moment
username cn=smtpd,ou=services,dc=example,dc=com
password letmein
basedn dc=obtuse,dc=network
# filter to use for alias lookups
alias_filter (&(objectclass=posixAccount)(uid=%s))
alias_attributes mail
# filter to use for user credentials
credentials_filter (&(objectclass=posixAccount)(mail=%s))
credentials_attributes mail,description
# filter to use for domain lookup
domain_filter (&(objectclass=rFC822localPart)(dc=%s))
domain_attributes dc
# filter to use for userinfo
userinfo_filter (&(objectclass=posixAccount)(uid=%s))
userinfo_attributes uidNumber,gidNumber,homeDirectory
# filter to use for mail addresses
mailaddr_filter (&(objectclass=posixAccount)(uid=%s))
mailaddr_attributes mail
/etc/mail/ldapd.conf
NOTE: The astute reader will have noticed that we are also putting the password has in the description attribute and have filtered that description attribute just like the userPassword attribute is filtered.  This is to work around an issue with the table-ldap library.  The library currently does not handle the {type}hash format that the passwords are stored in the LDAP server. Until that update is made the description attribute serves in it's place.
NOTE: The LDAP connection is not running over LDAPS.  As of this article the table-ldap plugin does not support this connection method.

Next we need to create the smtpd service account in ldap.  On the ldap server create the account and password then upload to the ldap server.

# create_service.sh -o smtpd.ldif smtpd
enter password (will not echo): letmein
# upload_ldif.sh smtpd.ldif

Next we need to configure smtpd(8).  The first step is to install the opensmtp-extras package which contains the table-ldap plugin.

# pkg_add opensmtpd-extras

Next update smtpd.conf(5) to use the new table.

table   ldap ldap:/etc/mail/ldapd.conf
# pki and filters should also be part of the listen statement(s)
listen on lo port smtps smtps auth <ldap>
listen on lo port submission auth <ldap>
listen on egress port submission tls-require auth <ldap>

Now restart smtpd(8) and your ready to go.  You can run the smtpd(8) server manually with the -dvvv flags to have the server output debug messages during testing.

DOVECOT Service

NOTE: These instructions assume that dovecot is already configured to utilize either the system passwords or a separate passwd file.

First step is to make sure that the dovecot-ldap package is installed.  To install use the pkg_add(8) command.

# pkg_add dovecot-ldap

Next, edit the /etc/dovecot/conf.d/10-auth.conf file.  At the end of the file there are !include lines.  Comment out any that are in use and uncomment  the !include auth-ldap.conf.ext line.

Now validate the /etc/dovecot/conf.d/auth-ldap.conf.ext file has a lines looking similar to the following

passdb {
  driver = ldap
  args = /etc/dovecot/dovecot-ldap.conf.ext
}
userdb {
  driver = ldap
  args = /etc/dovecot/dovecot-ldap.conf.ext
  # can set some default fields here.  LDAP will override
  #default_fields = home=/home/virtual/%u
  # override the ldap fields. useful for dovecot setups that have virtual users
  override_fields = uid=vmail gid=vmail home=/var/mail/vmail/%d/%n mail=maildir:~/Maildir

}
auth-ldap.conf.ext

Now configure the dovecot-ldap.conf.ext file

uris = ldaps://127.0.0.1
base = ou=people,dc=example,dc=com
dn = "cn=imapd,ou=services,dc=example,dc=com"
dnpass = "letmein
auth_bind = yes
sasl_bind = no
tls = no  # this enables STARTTLS over LDAP. We are using LDAPS
#debug_level = 0  # may be useful if troubleshooting ldap issues

user_attrs = homeDirectory=home,uidNumber=uid,gidNumber=gid
user_filter = (&(objectclass=posixaccount)(mail=%u))
pass_attrs = mail=user,userPassword=password
pass_filter = (&(objectclass=posixaccount)(mail=%u))

iterate_attrs = mail=user
iterate_filter = (objectclass=posixaccount)
# tell dovecot what hash scheme is used for the password
default_pass_scheme = CRYPT
dovecot-ldap.conf.ext

Next add the imapd service account to the ldap server.  On the ldap server create the account and password then upload to the ldap server.

# create_service.sh -o imapd.ldif imapd
enter password (will not echo): letmein
# upload_ldif.sh imapd.ldif

Now restart dovecot.

# rcctl restart dovecot

Check the /var/log/maillog for any connection errors to the ldap server.

If everything looks good try and login using your IMAP mail client.

Conclusion

If you gotten to this point, thank you for sticking with me.  Hopefully you will be able to setup a simple LDAP server that will allow the consolidation of user accounts into a single place.  If you feel I've missed anything glaring obvious or have any other comments, please feel free to send me an email.