How to Setup an OpenLDAP-based PGP Keyserver

This howto will show you how to set up an LDAP-based PGP Keyserver.

LDAP: A Very Short Introduction

~LDAP is a protocol for accessing data in a hierarchical directory.

The fact that ~LDAP defines a protocol means that any ~LDAP client can talk to any ~LDAP server implementation. This is different from, say, ~SQL, which is just a query language. A consequence of this is that although all ~SQL servers understand ~SQL, how ~SQL commands are issued and how results are returned is different for each database implementation.

~LDAP's hierarchical structure is designed for storing and accessing directories, such as, telephone book data. In ~LDAP both the internal nodes and the leaves can store data. The nodes are referred to as entries. Entries contain attributes, which are just key-value pairs.

To look up an entry (or attribute), we need its name or path. This is called a distinguished name (dn) in ~LDAP. A dn consists of components (relative dns or rdns). Working right to left, the server first checks that the first components match the base dn and selects that entry. Then, it finds the child entry that matches the next rdn, visits that entry. The server repeats this process until the path has been completely traversed or an error occurred. This should be straightforward to understand: this is basically how resolving a name in ~DNS works and how a filename is looked up.

In ~LDAP, however, entries don't have names. Instead, any piece or pieces of the content can be used to address the entry as long as those pieces uniquely identify that entry among its siblings. This is both useful (there is no need to come up with names or for users to manage another piece of data) and very flexible (different applications may have different information available and can create names differently). Consider the following very simple directory:

            dc=example,dc=org
                     |
                     v
                 ou=sales               ou: Organizational Unit
           /         |          \           (think: department)
          v          v           v
 gn:    John       John         Jane    gn: Given Name
 sn:    Smith      Doe          Doe     sn: Surname

We can address the entry on the left (John Smith) using the following dn: sn=Smith,ou=sales,dc=example,dc=org. We can't use gn=John for the last component of the dn, since this is not unique among the siblings (the second entry's given name is also John). Note that we can just use the sn when addressing this entry even though not all surnames are unique. Smith, however, is (currently) unique and thus uniquely identifies the entry among its siblings.

Since the second entry doesn't have any attributes whose value is unique among its siblings, we need to use a combined rdn, which uses multiple attributes joined by a +. In this case, we'd use: gn=John+sn=Doe,ou=sales,dc=example,dc=org.

For more information about ~Open~LDAP, the following resources are excellent:

Installing OpenLDAP

~Open~LDAP consists of the server (slapd) and some client utilities (ldap-utils). We need to install the utilities to be able to configure slapd: slapd uses an ~LDAP database for its configuration.

To install on Debian, run:

  $ sudo apt-get install slapd ldap-utils

When installing ~Open~LDAP on Debian, debconf does some initial configuration for you. At normal priority, you'll only have to enter the administrator's password. If you use dpkg-reconfigure slapd, you can change some more settings quite easily. The most important of these are the base distinguished name (bdn), which is the name of the root of the tree managed by your slapd instance. This is normally set to the ~DNS domain name of the machine (Debian defaults to the output of hostname -d). If you use example.org, then slapd will serve requests for the tree rooted at dc=example,dc=org.

To start the server (slapd), run:

  $ sudo /etc/init.d/slapd start

Installing an Additional Schema

New Schema

This section is out of date; installing a new schema is meanwhile much easier. In particular there is no need to convert the configuration file. Please use only the schema described by tehse two files: * Schema * Init

Installing a new schema under ~Open~LDAP is rather painful. First, the schema needs to be converted to ~LDIF (the ~LDAP Data Interchange Format). This can be done using the slaptest utility, which takes an old-school ~Open~LDAP configuration file and converts it to a modern cn=config style. However, since you are already using a cn=config style configuration and there is no easy way to convert it to an old-school style configuration, we need to manually merge the two.

First, we need a temporary directory:

  $ mkdir -p /tmp/ldap-config
  $ cd /tmp/ldap-config

Now, we create an old-school configuration file:

  $ echo 'include pgp-keyserver.schema' > slapd.conf

Add the pgp-keyserver.schema file to /tmp/ldap-config and then run:

  $ mkdir output
  $ /usr/sbin/slaptest -f slapd.conf -F output

This creates a cn=config hierarchy under the output directory with the following files:

  $ find output/
  output/
  output/cn=config
  output/cn=config/cn=schema
  output/cn=config/cn=schema/cn={0}pgp-keyserver.ldif
  output/cn=config/olcDatabase={-1}frontend.ldif
  output/cn=config/olcDatabase={0}config.ldif
  output/cn=config/cn=schema.ldif
  output/cn=config.ldif

Before we can merge the file cn={0}pgp-keyserver.ldif into our configuration tree, we need to modify it a bit. First, at the top of the file, we need to change the dn. It should be:

  cn={###}pgp-keyserver,cn=schema,cn=config

Where ### is the load order of the schema. We want our schema to load last. To avoid figuring out the right number, just choose a large number, like 100, and on import ~Open~LDAP will set this to the maximum number plus one.

A few lines later is a line that looks like:

  cn={0}pgp-keyserver

Remove this line. (~Open~LDAP will automatically fill in the cn attribute.)

Finally, at the bottom of the file we need to remove the lines that start like this:

  structuralObjectClass:
  entryUUID:
  creatorsName:
  createTimestamp:
  entryCSN:
  modifiersName:
  modifyTimestamp:

At last, we can add the schema file:

  $ sudo ldapadd -Y EXTERNAL -H ldapi:/// -f /tmp/ldap-config/output/cn=config/cn=schema/cn={0}pgp-keyserver.ldif
  SASL/EXTERNAL authentication started
  SASL username: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
  SASL SSF: 0
  adding new entry "cn={100}pgp-keyserver,cn=schema,cn=config"

(Using the EXTERNAL authentication mechanism, you just need to be root to gain admin access to slapd; you don't need to use your slapd admin password.)

If you get the following error:

  ldap_add: Server is unwilling to perform (53)
        additional info: operation requires sibling renumbering

Then the number you selected above conflicts with an existing schema.

To see that the schema is really installed, we can list the installed schemas using the following command:

  $ sudo ldapsearch -LLL -Y EXTERNAL -H ldapi:/// -b cn=schema,cn=config cn
  SASL/EXTERNAL authentication started
  SASL username: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
  SASL SSF: 0
  dn: cn=schema,cn=config
  cn: schema
  
  dn: cn={0}core,cn=schema,cn=config
  cn: {0}core
  
  dn: cn={1}cosine,cn=schema,cn=config
  cn: {1}cosine
  
  dn: cn={2}nis,cn=schema,cn=config
  cn: {2}nis
  
  dn: cn={3}inetorgperson,cn=schema,cn=config
  cn: {3}inetorgperson
  
  dn: cn={4}pgp-keyserver,cn=schema,cn=config
  cn: {4}pgp-keyserver

In case you are wondering: yes, this is the recommended way to install new schemas. See, for instance, this section in LDAP for Rocket Scientists.

Setting up the Data Structures

Recall that an ~LDAP server provides access to a hierarchical database. Thus, we need to create some containers for the users and the keys.

Please use the new schema and adjust the provided init file instead

Create a file called /tmp/keyserver.ldif with the following contents and replace dc=EXAMPLE,dc=ORG with your base dn:

  dn: cn=PGPServerInfo,dc=EXAMPLE,dc=ORG
  cn: PGPServerInfo
  objectclass: pgpserverinfo
  pgpSoftware: OpenLDAP
  pgpVersion: 2.2.27
  pgpBaseKeyspaceDN: ou=PGP Keys,dc=EXAMPLE,dc=ORG

  dn: ou=PGP Keys,dc=EXAMPLE,dc=ORG
  objectclass: organizationalUnit
  ou: PGP Keys

  dn: ou=PGP Users,dc=EXAMPLE,dc=ORG
  objectclass: organizationalUnit
  ou: PGP Users

This file describes a change set in ~LDIF format (~LDAP Data Interchange Format). The first stanza creates an entry that ~GPG uses to detect that this is really an ~OpenPGP key server. The next stanza creates an organizational unit (ou) that all keys are added to. And the last stanza creates an organizational unit that all key server users are added to.

To apply the changes, use the following command (as usual, change dc=EXAMPLE,dc=ORG accordingly):

  $ ldapadd -x -D "cn=admin,dc=EXAMPLE,dc=ORG" -W -f /tmp/keyserver.ldif 
  Enter LDAP Password: 
  adding new entry "cn=PGPServerInfo,dc=EXAMPLE,dc=ORG"

  adding new entry "ou=PGP Keys,dc=EXAMPLE,dc=ORG"
  
  adding new entry "ou=PGP Users,dc=EXAMPLE,dc=ORG"

The requested password is the admin password for your ~Open~LDAP installation (on Debian, this was entered when the package was installed).

You'll see that three entries were successfully added.

Adding Users

We can now add some users who will be able to manage the keys in the directory.

Create a file called /tmp/keyserver-user.ldif with the following contents:

  dn: uid=user1,ou=PGP Users,dc=EXAMPLE,dc=ORG
  objectClass: inetOrgPerson
  objectClass: uidObject
  sn: lastname
  cn: firstname lastname
  userPassword: {SSHA}...

Update the fields appropriately. In particular, you can change the value of uid, sn (the surname), cn (the common name). These are the required fields. Other fields, such as gn (given name), are optional and are elided.

You can add more than one user at once. Just use multiple copies of the above entry. Be sure to separate each entry with a blank line.

To add the user, run the following command:

  $ ldapadd -x -D "cn=admin,dc=EXAMPLE,dc=ORG" -W -f /tmp/keyserver-user.ldif 
  Enter LDAP Password: 
  adding new entry "uid=user1,ou=PGP Users,dc=EXAMPLE,dc=ORG"

To list all of the PGP Users, you can use the following command:

  $ ldapsearch  -LLL -x -D "cn=admin,dc=EXAMPLE,dc=ORG" -W -b 'ou=PGP Users,dc=EXAMPLE,dc=ORG'
  Enter LDAP Password: 
  dn: ou=PGP Users,dc=EXAMPLE,dc=ORG
  objectClass: organizationalUnit
  ou: PGP Users
  
  dn: uid=user1,ou=PGP Users,dc=EXAMPLE,dc=ORG
  objectClass: inetOrgPerson
  objectClass: uidObject
  uid: user1
  sn: lastname
  cn: firstname lastname

  ...

Note: the -b selects a base dn and, since there is no filter, ldapsearch lists all of its descendants.

Access Control

We've created the users, but they can't actually access the directory yet. To do this, we need to modify the ~ACLs. In slapd, ~ACLs are applied on a per-database basis. To list the configured databases, use the following command:

  $ sudo ldapsearch -LLL -Y EXTERNAL -H ldapi:/// -b "cn=config"  | grep olcDatabase:
  SASL/EXTERNAL authentication started
  SASL username: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
  SASL SSF: 0
  olcDatabase: {-1}frontend
  olcDatabase: {0}config
  olcDatabase: {1}mdb

Here, you see that I have three databases. This is normal. The frontend database is a special database that provides fallback access control: slapd uses a "first match wins" model for access control. The config database is used for slapd's own configuration and we don't want to mess with it. The last database is where our DIT lies. It is possible to have multiple databases and different formats are common. Currently, mdb is the recommended format.

The database's dn is just it's name combined with the bdn. In my case, it is:

  olcDatabase={1}mdb,cn=config

We now need to get the current set of ~ACLs. On Debian the default set are:

  $ sudo ldapsearch -LLL -Y EXTERNAL -H ldapi:/// -b olcDatabase={1}mdb,cn=config olCaccess
  SASL/EXTERNAL authentication started
  SASL username: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
  SASL SSF: 0
  dn: olcDatabase={1}mdb,cn=config
  olcAccess: {0}to attrs=userPassword,shadowLastChange by self write by anonymous auth by * none
  olcAccess: {1}to dn.base="" by * read
  olcAccess: {2}to * by * read

The first rule allows an authenticated user to change his or her own password, an anonymous user to authenticate against (bind to) the entry and reject any other type of access. The second rule allows anyone to read the base dn. The third is a catch all and allows anyone to read any node. [Note_1]

We now want to insert a rule allowing anyone connected from localhost to add or modify keys as well as authenticated ~PGP users connecting from anywhere. Further, anyone should be able to read the keys. Because ~Open~LDAP uses first match wins when resolving ~ACLs, we need to add the rule before the last entry: the last entry catches everything. Here, we add it immediately prior to the last entry. For the first part of this rule to work---anonymous updates from localhost---we also need to enable anonymous updates. We can do both of these at once. (Note: you can easily disable either of these, if you prefer.) Create /tmp/keyserver-acls.ldif with the following content:

  # userPassword may be written only by users themselves
  dn: olcDatabase={DDD}mdb,cn=config
  changetype: modify
  add: olcAccess
  # Allow access via localhost to add or modify keys.
  # Allow authenticated PGP Users to update keys.
  # Allow anyone else to read the keys.
  olcAccess: {XXX} to dn.subtree="ou=PGP Keys,dc=EXAMPLE,dc=ORG"
    by peername.ip=127.0.0.1 write
    by peername.ip=:: write
    by dn.regex="^uid=([^,]+),ou=PGP Users,dc=EXAMPLE,dc=ORG" write
    by * read
  
  # Allow any connection to localhost to update the PGP keys
  # (including removing them!)  This is only needed if the anonymous
  # updates from localhost are desired.
  dn: cn=config
  add: olcAllows
  olcAllows: update_anon

Be sure to replace DDD with your database index (and change mdb, if necessary). Also, replace XXX with the index of the last entry in the ~ACL. In our example, this was 2. ~Open~LDAP will insert the entry at that position and push any entry that was at that position one forward.

To add the changes, run:

  $ sudo ldapmodify -Y EXTERNAL -H ldapi:/// -f /tmp/keyserver-acls.ldif

Now, enough machinery is in place to actually push some keys! Let's give it a try using an anonymous write:

  $ gpg --keyserver ldap://localhost  --send-keys 8bafcdbd
  gpg: sending key 8BAFCDBD to ldap server localhost

And as an authenticated user:

  $ gpg --keyserver ldap://localhost/uid=user1,ou=PGP%20Users,dc=EXAMPLE,dc=ORG --send-keys 8BAFCDBD
  gpg: sending key 8BAFCDBD to ldap server localhost

Where PASSWORD is the user's password.

The keyserver con be configured in ~/.gnupg/gpg.conf:

  keyserver ldap://localhost/uid=user1,ou=PGP%20Users,dc=EXAMPLE,dc=ORG

Note: We set tls to try. In this howto, we didn't actually configure TLS. If you are going to access the host via an insecure network connection, it makes sense to enable this.

Note: Since your password is in this file, be sure it is not world readable.

Debugging

If something goes wrong, then it is help to enabling logging in slapd. You can do this with the following LDIF file:

  dn: cn=config
  changetype: modify
  replace: olcLogLevel
  olcLogLevel: stats

Then, watch syslog for messages. Before to disable this once you are done.

If you accidentally insert a bad ~ACL, you can remove it using the following ~LDIF file:

  dn: olcDatabase={1}mdb,cn=config
  changetype: modify
  delete: olcAccess
  olcAccess: {XXX}

Where XXX is the number of the rule.

Apply it as follows:

  sudo ldapmodify -Y EXTERNAL -H ldapi:/// -f /tmp/revert.ldif

If you need additional help, please ask on the gnupg-users mailing list.

[Note_1]: In Debian stable (actually, Knoppix), which uses hdb database format, the output is as follows:

$ sudo ldapsearch -LLL -Y EXTERNAL -H ldapi:/// -b olcDatabase={1}hdb,cn=config olCaccess
SASL/EXTERNAL authentication started
SASL username: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
SASL SSF: 0
dn: olcDatabase={1}hdb,cn=config
olcAccess: {0}to attrs=userPassword,shadowLastChange by self write by anonymous
 auth by dn="cn=admin,dc=example,dc=org" write by * none
olcAccess: {1}to dn.base="" by * read
olcAccess: {2}to * by self write by dn="cn=admin,dc=example,dc=org" write
 by * read

LDAPKeyserver (last edited 2020-11-12 15:25:45 by Werner Koch)