How to Setup an OpenLDAP-based PGP Keyserver
This howto will show you how to set up an LDAP-based PGP Keyserver.
Contents
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 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