OpenBSD + LDAP
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.
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.
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==
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.)
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: bob@example.com
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: bob@example.com
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.
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.
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.
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.
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
Now configure the dovecot-ldap.conf.ext file
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.