Extended Brain Storage

OpenBSD: LDAP User Management

Posted on September 20, 2016

A quick introduction of how to utilise a built-in Lightweight Directory Access Protocol daemon (ldapd) server in OpenBSD...

General Theory

In order to authenticate users to access the e-mail services, their credentials need to be stored somewhere. A centralised storage simplifies processes related with creation, deletion and modification of users' details. From e-mail server perspective, users can be differentiated as:

System accounts are stored in the /etc/passwd and /etc/groups files and the authentication process differs based on operating system (Linux, UNIX etc.) Virtual accounts are stored in either a database or a directory. RDBMS is relatively quicker for write operations, while LDDP is generally quicker for read operations.

A short list of several virtual user accounts will be created at the beginning and it is assumed that it will not change (much) throughout the time. Therefore, the LDAP-based approach was selected for this purpose.


LDAP in OpenBSD

There are three options in OpenBSD to implement LDAP-related services:

The YP can be used for basic LDAP functions. However, it is not considered, as it does not provide any sort of security whatsoever.

At the time of writing this introduction, the OpenBSD's openldap-server package came with Berkeley DB (BDB) backend support (considered deprecated) only, i.e. without proper Memory-Mapped DB (MDB) support. Furthermore, it was impossible to stop the slapd daemon using the rcctl stop command. As a result, a crippled database occurs each server's restart.

OpenBSD comes with a built-in LDAP daemon (ldapd), which is not yet fully LDAPv3 compliant (as per RFC3673). However, it can be used for this purpose.


LDAPd Setup

The configuration of ldapd is by default in (/etc/ldapd.conf) and can be initialised by:

$ cp /etc/examples/ldapd.conf /etc/

By default, the list of available schemes is quite limited:

$ ls -plA /etc/ldap | grep -v /
total 68
-rw-r--r--  1 root  wheel  19658 Oct  4 14:13 core.schema
-rw-r--r--  1 root  wheel   2737 Oct  4 14:13 inetorgperson.schema
-rw-r--r--  1 root  wheel   7443 Oct  4 14:13 nis.schema

Therefore, an additional schema (officially registered with IANA) needs to be downloaded to provide extra attributes that are required for e-mail aliases, quotas etc. Schemes provided by openldap.org or ldapadmin.org did not work due to incompatibility errors.

The schema shared by postfix-buch.com (OID registered to Patrick Koetter, State of Mind) worked flawlessly. However, the page is not available anymore. Luckily, the archive.org pages still have it and it can be downloaded as follows:

$ cd /etc/ldap/schemes
$ ftp http://web.archive.org/web/20111105200517/http://www.postfix-buch.com/download/postfix-book.schema.gz
$ gunzip postfix-book.schema.gz

Alternatively, some forks also exist on GitHub, such as: variablenix.

Before the configuration, superuser's (admin's) passphrase needs to be generated. Using unsalted SHA (as follows) is not recommended due to possible rainbow table-based attacks:

$ echo -n PASSPHRASE | openssl dgst -sha1 -binary | openssl enc -base64 | awk '{print "{SHA}"$0}'
{SHA}hashed-passphrase

Therefore, in order to generate superuser's (admin's) PASSPHRASE, a salted SHA is considered as follows:

USER_PASSWRD="passphrase"
RND_SALT=$(openssl rand -base64 6)
PASHASH=$(echo -n "$USER_PASSWRD$RND_SALT" | openssl dgst -sha1 -binary | openssl enc -base64 -A)
LDAP_PASHASH=$({ echo -n "$PASHASH" | openssl base64 -d -A; echo -n "$RND_SALT"; } | openssl enc -base64 -A | awk '{print "{SSHA}"$0}')
echo "$LDAP_PASHASH"

Note: The same results can be achieved using the slappasswd command (part of the openldap-server package):

$ slappasswd -h {SSHA} -s PASSPHRASE
{SSHA}hashed-passphrase
# or, respectively:
$ slappasswd -h {SHA} -s PASSPHRASE
{SHA}hashed-passphrase

Considering that the TLS will be activated only for the egress interface (e.g. vio0), the configuration can be set up as follows:

$ vi /etc/ldapd.conf
#       $OpenBSD: ldapd.conf,v 1.1 2014/07/11 21:20:10 deraadt Exp $
### SCHEMES
schema "/etc/ldap/core.schema"
schema "/etc/ldap/inetorgperson.schema"
schema "/etc/ldap/nis.schema"
schema "/etc/ldap/postfix-book.schema"
### LISTENING INTERFACES AND PORTS
listen on lo0 port 389 secure
listen on vio0 port 389 tls certificate domain.tld
listen on "/var/run/ldapi"
### ACCESS FILTERING
namespace "dc=domain,dc=tld" {
  rootdn "cn=admin,dc=domain,dc=tld"
  rootpw "{SSHA}hashed-passphrase"
  # Any access denied by default.
  deny read,write access to subtree root by any
  # Bind/read/write access allowed by self, i.e. users who previously performed a bind
  allow read,write access to subtree root by self
  # Read access allowed to specific LDAP DNs (or BASEDN) for specific BINDDNs
  allow read access to subtree "ou=people,dc=domain,dc=tld" by "uid=postfix,ou=services,dc=domain,dc=tld"
  allow read access to subtree "ou=domains,dc=domain,dc=tld" by "uid=postfix,ou=services,dc=domain,dc=tld"
  allow read access to subtree "ou=people,dc=domain,dc=tld" by "uid=dovecot,ou=services,dc=domain,dc=tld"
  # Read/write access allowed to the LDAP BASEDN for the LDAP manager (ADMINDN)
  allow read,write access to subtree "dc=domain,dc=tld" by "cn=admin,dc=domain,dc=tld"
}

Since the ldapd runs under a non-privileged user (_ldapd), the TLS certificates needs to be copied as follows:

$ cp /etc/ssl/domain.tld.fullchain.pem /etc/ldap/certs/domain.tld.crt
$ cp /etc/ssl/private/domain.tld.key /etc/ldap/certs/domain.tld.key

Configuration can be checked using the following:

$ ldapd -nf /etc/ldapd.conf
configuration ok

The ldapd service can be enabled and started:

$ rcctl enable ldapd
$ rcctl start ldapd

Service availability check can be performed as follows:

$ netstat -naf inet | grep 389
tcp          0      0  127.0.0.1.389          *.*                    LISTEN
tcp          0      0  EGRESS_IP.389          *.*                    LISTEN

Troubleshooting

Instead of running the daemon as a service (in the background), the ldapd can be run in foreground and log to stderr as follows:

$ ldapd -dv
$ ldapd -dvv

For an already daemonised service, the ldapctl, the LDAP daemon control program, can be used:

$ ldapctl stats

Verbose debug logging can be enabled as follows:

$ ldapctl log verbose

Verbose debug logging can be disabled as follows:

$ ldapctl log brief

Fine-tuning

In order to understand how an LDAP search works, it may be useful to continue to openldap.org pages and read a bit about indexes. As yet, it will not be discussed further.


LDAP DN Structure

The basic idea of the LDAP structure of distinguished names (DN) is as follows:

dc=tld
└── dc=domain
    ├── cn=admin,dc=domain,dc=tld
    ├── ou=people,dc=domain,dc=tld
    │   ├── uid=user1,ou=people,dc=domain,dc=tld
    │   └── uid=user2,ou=people,dc=domain,dc=tld
    ├── ou=groups,dc=domain,dc=tld
    │   ├── cn=group1,ou=groups,dc=domain,dc=tld
    │   └── cn=group2,ou=groups,dc=domain,dc=tld
    ├── ou=services,dc=domain,dc=tld
    │   ├── uid=postfix,ou=services,dc=domain,dc=tld
    │   └── uid=dovecot,ou=services,dc=domain,dc=tld
    └── ou=domains,dc=domain,dc=tld
        └── dc=domain.tld,ou=domains,dc=domain,dc=tld

The domain component (dc) can be chained and will represent the domain name (domain.tld) in the form of dc=domain,dc=tld. Furthermore, it will contain the superuser's credentials (admin) in the form of common name (cn=admin). Finally, it will contain the following organisational units (ou):

Users will have specified the following parameters (mail is the primary e-mail, where mailBox is an e-mail alias):

Groups will have specified the following parameters:

Services will have specified the following parameter:

Domains will have specified the following parameter:

Note: The Groups subtree will not be used by Postfix or Dovecot. However, it can be successfully utilised by self-hosted file share and communication platforms, such as Nextcloud.


LDIF Files Preparation

The LDAP data interchange format (LDIF) is a standard plain text data interchange format defined in RFC2849 and designed for representing LDAP directory content and update requests.

For that purpose, a temporary /tmp/ldapsetup directory will be created.

$ mkdir -p /tmp/ldapsetup

The temporary directory will contain a group of file which will be consequently sent to the LDAP server in order to create the whole LDAP hierarchy.

The Rootdn will be initialised using the following LDIF file:

$ vi /tmp/ldapsetup/00-ldapadd-init-root-dn.ldif
## DEFINE DIT ROOT/BASE/SUFFIX ####
## uses RFC 2377 format

## dcObject is an AUXILIARY objectclass and MUST
## have a STRUCTURAL objectclass (organization in this case)

dn: dc=domain,dc=tld
objectclass: dcObject
objectclass: organization
dc: domain
o: domain.tld LDAP Server
description: Root entry for domain.tld.

## Superuser's Account

dn: cn=admin,dc=domain,dc=tld
objectclass: organizationalRole
cn: admin
description: Rootdn

The first level hierarchy will be initialised using the following file:

$ vi /tmp/ldapsetup/01-ldapadd-first-level-hierarchy.ldif
## FIRST Level hierarchy - people

dn: ou=people,dc=domain,dc=tld
objectClass: organizationalUnit
ou: people
description: All people in organisation

## FIRST Level hierarchy - groups

dn: ou=groups,dc=domain,dc=tld
objectClass: organizationalUnit
ou: groups
description: All groups in organisation

## FIRST Level hierarchy - services

dn: ou=services, dc=domain,dc=tld
objectClass: organizationalUnit
ou: services
description: All services in organisation

## FIRST Level hierarchy - domains

dn: ou=domains, dc=domain,dc=tld
objectClass: organizationalUnit
ou: domains
description: All domains in organisation

For this e-mail server example, two services need to be defined in LDAP on the second level, i.e. postfix and dovecot:

$ vi /tmp/ldapsetup/02-ldapadd-services.ldif
version: 1

## version not strictly necessary (and some implementations reject it) but generally good practice

### NOTES:
# to generate password hashes, use:
#USER_PASSWRD="passphrase"
#RND_SALT=$(openssl rand -base64 6)
#PASHASH=$(echo -n "$USER_PASSWRD$RND_SALT" | openssl dgst -sha1 -binary | openssl enc -base64 -A)
#LDAP_PASHASH=$({ echo -n "$PASHASH" | openssl base64 -d -A; echo -n "$RND_SALT"; } | openssl enc -base64 -A | awk '{print "{SSHA}"$0}')
#echo "$LDAP_PASHASH"

## SECOND Level hierarchy - services entries

dn: uid=postfix,ou=services,dc=domain,dc=tld
objectClass: account
objectClass: simpleSecurityObject
uid: postfix
userPassword: {SSHA}hashed-passphrase
#PASSPHRASE

dn: uid=dovecot,ou=services,dc=domain,dc=tld
objectClass: account
objectClass: simpleSecurityObject
uid: dovecot
userPassword: {SSHA}hashed-passphrase
#PASSPHRASE

Note: One of the users should be able to receive e-mail for (at least) the following aliases:

User accounts can be created on the second level as follows:

$ vi /tmp/ldapsetup/03-ldapadd-users.ldif
version: 1

## version not strictly necessary (and some implementations reject it) but generally good practice

### NOTES:
# to generate password hashes, use:
#USER_PASSWRD="passphrase"
#RND_SALT=$(openssl rand -base64 6)
#PASHASH=$(echo -n "$USER_PASSWRD$RND_SALT" | openssl dgst -sha1 -binary | openssl enc -base64 -A)
#LDAP_PASHASH=$({ echo -n "$PASHASH" | openssl base64 -d -A; echo -n "$RND_SALT"; } | openssl enc -base64 -A | awk '{print "{SSHA}"$0}')
#echo "$LDAP_PASHASH"

## SECOND Level hierarchy - people entries

dn: uid=user1,ou=people,dc=domain,dc=tld
objectclass: person
objectclass: inetOrgPerson
objectclass: PostfixBookMailAccount
cn: Name1 Surname1
sn: Surname1
givenName: Name1
displayName: Name1
uid: user1
mail: user1@domain.tld
mailAlias: mailalias11@domain.tld
mailAlias: mailalias12@domain.tld
mailStorageDirectory: domain.tld/user1/
mailEnabled: TRUE
mailQuota: 536870900
userPassword: {SSHA}hashed-passphrase
#PASSPHRASE

dn: uid=user2,ou=people,dc=domain,dc=tld
objectclass: person
objectclass: inetOrgPerson
objectclass: PostfixBookMailAccount
cn: Name2 Surname2
sn: Surname2
givenName: Name2
displayName: Name2
uid: user2
mail: user2@domain.tld
mailAlias: mailalias21@domain.tld
mailAlias: mailalias22@domain.tld
mailStorageDirectory: domain.tld/user2/
mailEnabled: TRUE
mailQuota: 536870900
userPassword: {SSHA}hashed-passphrase
#PASSPHRASE

If necessary, group accounts can be created on the second level as follows:

$ vi /tmp/ldapsetup/04-ldapadd-groups.ldif
version: 1

## version not strictly necessary (and some implementations reject it) but generally good practice

## SECOND Level hierarchy - groups entries

dn: cn=group1,ou=groups,dc=domain,dc=tld
objectClass: groupOfNames
cn: group1
member: uid=user1,ou=people,dc=domain,dc=tld
member: uid=user2,ou=people,dc=domain,dc=tld

At least one domain need to be created on the second level:

$ vi /tmp/ldapsetup/05-ldapadd-domains.ldif
version: 1

## version not strictly necessary (and some implementations reject it) but generally good practice

## SECOND Level hierarchy - domains

dn: dc=domain.tld,ou=domains,dc=domain,dc=tld
objectClass: domain
dc: domain.tld
description: 0

LDAP Structure Commit

The openldap-client package will be used to communicate with the LDAP server. It can be installed as follows:

$ pkg_add openldap-client

The following commands will be available:

Having the LDIF file ready, the LDIF files can be uploaded as follows (-v to verbose, -Z to initialise StartTLS):

### LDAP directory initialisation
$ ldapadd -vZWh server.domain.tld -D "cn=admin,dc=domain,dc=tld" -f /tmp/ldapsetup/00-ldapadd-init-root-dn.ldif
### First level hierarchy
$ ldapadd -vZWh server.domain.tld -D "cn=admin,dc=domain,dc=tld" -f /tmp/ldapsetup/01-ldapadd-first-level-hierarchy.ldif
### Second level: Services
$ ldapadd -vZWh server.domain.tld -D "cn=admin,dc=domain,dc=tld" -f /tmp/ldapsetup/02-ldapadd-services.ldif
### Second level: Users
$ ldapadd -vZWh server.domain.tld -D "cn=admin,dc=domain,dc=tld" -f /tmp/ldapsetup/03-ldapadd-users.ldif
### Second level: Users to groups
$ ldapadd -vZWh server.domain.tld -D "cn=admin,dc=domain,dc=tld" -f /tmp/ldapsetup/04-ldapadd-groups.ldif
### Second level: Domains
$ ldapadd -vZWh server.domain.tld -D "cn=admin,dc=domain,dc=tld" -f /tmp/ldapsetup/05-ldapadd-domains.ldif

Note: Unless desired (needs to be configured), the StartTLS (-Z parameter) is not used for localhost access:

$ ldapadd -vWh localhost -D "cn=admin,dc=domain,dc=tld" -f ...

Searching the LDAP branch people by service postfix can be done as follows:

$ ldapsearch -vZWh server.domain.tld -D "uid=postfix,ou=services,dc=domain,dc=tld" -b "ou=people,dc=domain,dc=tld"

Sometimes, it may be necessary to delete the whole LDAP tree. This can be achieved using the following:

$ ldapdelete -rvZWh server.domain.tld -D "cn=admin,dc=domain,dc=tld" "dc=domain,dc=tld"
$ rcctl restart ldapd

phpLDAPadmin

The following configuration steps do not deal with HTTPD or PHP installation, as these have already been described in OpenBSD: HTTPD with PHP Support.

phpLDAPadmin is one of many solutions for administering LDAP servers. It is a web-based application, which requires PHP for its function and which can be installed as follows:

$ pkg_add phpldapadmin
quirks-2.367 signed on 2017-10-03T11:21:28Z
phpldapadmin-1.2.3p3: ok
Look in /usr/local/share/doc/pkg-readmes for extra documentation.

The configuration is straightforward (considering the SSHA algorithm and no TLS encryption, as connecting to localhost only):

$ vi /var/www/phpldapadmin/config/config.php
$servers->newServer('ldap_pla');
$servers->setValue('server','name','LDAP Server domain.tld');
$servers->setValue('server','host','localhost');
$servers->setValue('server','port',389);
$servers->setValue('server','tls',false);
$servers->setValue('server','base',array('dc=domain,dc=tld'));
$servers->setValue('login','bind_id','cn=admin,dc=domain,dc=tld');
$servers->setValue('appearance','password_hash_custom','ssha');

Allowing public access to administrative functions of a server has always been potential security risk. However, there is nothing wrong with accessing the phpLDAPadmin's web interface via SSH tunnel from another machine, as long as the passphrase is "strong" or certificates are used.

$ vi /etc/httpd.conf
### Localhost for phpLDAPadmin
server "localhost" {
  listen on 127.0.0.1 port 80
  log style combined
  ### phpLDAPadmin
  location "/*.php*" {
    directory { index "index.php" }
    root "/phpldapadmin"
    fastcgi socket "/run/php-fpm.sock"
  }
  location "/*" {
    directory { index "index.php" }
    root "/phpldapadmin"
  }
}
### The default host (EGRESS_IP:80)
...
$ rcctl reload httpd
$ ssh -p SSH_PORT -L 1234:localhost:80 user1@server.domain.tld

Tags: #OpenBSD #security #LDAP #ldapd #user management

⏴ Previous Post Next Post ⏵