From CSLabsWiki
Revision as of 03:30, 21 July 2015 by Northug (talk | contribs) (kerb.)
Contact Person: CoreMaintainers
Last Update: Summer 2015
Services: LDAP, DHCP, DNS, Kerberos
Ansible roles: setup, dns, dhcp, ldap, kerberos

Hostname: talos.cslabs.clarkson.edu
Operating system: Debian 8.0 Jessie
NIC 1: Clarkson Network
MAC: ?
CPU: ?
RAM: ?

Talos is our authentication server. It is managed by Ansible.


Start with a minimal installation of Debian Jessie, and proceed to install some useful packages; e.g., htop, vim, sudo. This should be enough to get through the next few steps.

Setting up firewall and other stuff

Setting up LDAP

Before You Begin

If you'd like or are in a hurry, you can skip to Installation below. This may contain some valuable information for setup or troubleshooting, however.

First, a foreword about LDAP: the Lightweight Directory Access Protocol is a protocol designed to provide a speedy, reliable way for networked machines to retrieve small records (that are often ubiquitously needed for various purposes--not the least of which identification across clients). LDAP records (or entries, as I will use interchangeably) are arranged in a hierarchical manner in a one-to-many relationship--much like filesystem hierarchies. These records are identified by a unique string, reminiscent of an absolute file path, called the distinguished name, or DN, which is used consistently to refer to the record throughout the architecture. DNs, as written out, consist of one or more components, each with a key and value separated by an equal sign '=', and joined together with a comma ',' separator. DNs can (and regularly do) have embedded spaces. The base DN for an LDAP database is usually derived from a domain name--a sufficiently unique identifier--as a series of domain components, or DCs, which will look something like dc=example,dc=com for the hypothetical "example.com" domain. Records below the base DN are arranged with more specific components on the right, such as cn=admin,dc=example,dc=com. It's worth mentioning that you will encounter CN (common name), OU (organizational unit), and other more domain-specific keys such as uid and olcDatabase.

Records each possess attributes, a multi-map of string values to a sequence of string values. While it is entirely possible to have arbitrary attributes, records in LDAP normally have properties dictated by a schema. Such a schema consists of attribute types, which define the names of attributes with the kinds of values that can be stored in an attribute (its syntax), its sorting/collation order, its comparison semantics, whether or not it needs to be unique per record, and so forth, as well as classes, which consist of a set of required attribute types (by name), and a set of optional attribute types. For example, an attribute type is something like a "userPassword", which stores an Octet String, is compared via Octet String comparison, and need not be unique to a record, while an object class is something like "posixUser" which MAY have a "userPassword" but MUST have a "uidNumber" and a "gidNumber" (both of which have syntax Integer). Classes have the additional property that they may be structural, auxiliary, or abstract. A record must have one, and only one, structural class, but can possess zero or more auxiliary classes. Classes can participate in inheritance; any may inherit arbitrarily from abstract classes, whereas auxiliary and structural classes may only inherit from their own kind (in addition to abstract). For example, consider the following Python code:

class X(object):

class Mixin(object):

class Y(X, Mixin):

y = Y()

If we consider y to represent a record in the LDAP database, then Y would be its singular structural class, which inherits from the structural class X. Auxiliary classes are not well-represented in this language, except perhaps as well-known attributes defined on the object y itself. Beyond the properties that class Y defines (here absolutely none), the Mixin class can be thought of as an abstract class from which Y also inherits some properties. All of these ultimately inherit from object, whose analogue in LDAP is top, a class which is simultaneously (and confoundingly) abstract and structural--the only such class.

It would be wise to familiarize yourself with some important classes, amongst them "posixAccount", "posixGroup", "organizationalUnit", and "simpleSecurityObject", amongst others. These and many more are defined in various RFCs and included with the OpenLDAP installation (under cn=schema,cn=config, described metareferentially through LDAP access). A web-based tool for browsing local schemas will be installed much later in this tutorial.

Access to the database is controlled through a process called binding. It should be noted that LDAP makes no distinction between users and records; a connecting client can connect to any record it so chooses. Of course, the most obvious things to bind to are user accounts, which are represented as first-class objects in most major LDAP schemas. In the simple case, one can use "simpleSecurityObject" with OpenLDAP to make any object in any part of the database be bindable, but posixAccounts are more likely to be used regularly. A connection that is not yet bound is said to be anonymous.

Access control is another topic entirely. You should be well aware of OpenLDAP's access control mini-language, which reads fairly straightforward. Amongst other things to be aware of, some important points are:

  • The first matching "to" clause is selected, in the order it is specified in the database. In particular, a "to *" clause will cancel all further searches, because it will immediately match everything. As a rule of thumb, specify more specific "to" clauses first, followed by more generic ones, with "to *" at the end.
  • The "by" clauses match similarly; place "by *" at the end of the list.
  • Giving an access level of "none" (or, equivalently, omitting it entirely) will cause results (when trying to search a record) indistinguishable from that record not existing. You may want to be the root account to be sure you're getting the entire picture.

olcAccess lines are a little cumbersome; you are well advised to Keep It Simple, Stupid.

Finally, LDIF--the LDAP Data Interchange Format. It is standardized as RFC2849, which does a good job of being one of the more concise RFCs I've read (the examples at the end are particularly informative). Basically, the syntax is as follows:

  • As with most script-like languages, lines whose first non-whitespace character is a pound-sign "#" are ignored, and blank lines have no effect on parsing or performance outside of a production.
  • Record indications are headed by a "dn: " field, followed by the DN, on its own line. From there on, all attributes and modifications are applied to that DN. For non-empty LDIF files, this must be the first non-comment, non-blank line.
  • Attributes are indicated similarly, with key, ": ", and value, all strings. If the value is too long, it may wrap onto continuation lines, which begin with exactly one space. If the value contains possibly illegal characters, such as non-ASCII ordinals, control sequences that may disrupt the parser, and so forth, the key may be followed instead by two colons ":: ", and the value will be base64 encoded. Wrapping similarly applies to base64-encoded values. Multi-valued attributes are ordered in the order in which they appear, unless they have a leading integer in {angle brackets} preceding the value, in which case they are first stably sorted by that integer in ascending order. Outputs from ldapsearch will always contain this integer for multi-valued attributes.
  • Some attributes have special behavior when passed through certain tools; ldapmodify is the most prominent of these. This command expects the "changetype: " attribute to be one of "add", "replace", or "delete", which is then to be followed by one or more "add: ", "replace: ", or "delete: " attributes, specifying a data attribute name (which must then follow that specification for all but delete), terminated by a dash "-" on a line of its own. Multiple additions and/or replacements may be specified this way, but (AFAIK) they must consistently be the same operation.
  • While not technically LDIF (and there is an LDIF way of specifying this), ldapmodrdn uses a format without any colon-separated headers in which each pair of non-blank, non-comment lines is, in order:
    • An existent DN.
    • A relative DN (RDN), whose first n components will replace the first n components of the existent DN.


Setting up OpenLDAP

For this purpose, the instructions on the Debian Wiki are invaluable, though somewhat flawed. Presented here is a slightly more instruction-oriented and pedantic version.

First, become root (sudo or su). Though it is not mentioned much, all the commands in the setup assume you are root.

From a root terminal, apt-get install slapd ldap-utils. During this process, you may be prompted for an administrator password for the database. It does not, however, prompt you about certain other things, like the base DN for the main database. After the packages are installed, I recommend running dpkg-reconfigure -p low slapd and verifying the results. It may tell you that it will overwrite the database previously created--that's OK. Remember to set the password to something memorable, but you probably won't need it for a little while.

Using ldap-utils

Now would be a good time to familiarize yourself with the tools in ldap-utils. They mostly take the same options, permitting you to log in in various ways:

  • -H <URI> sets the URI to attempt to contact the server on.
  • -Y <AUTH> sets the SASL authentication method.
  • -D <DN> sets the DN to bind to (very probably "cn=admin,<your_dc_components>" after the setup)
  • -W will instruct the utility to prompt for a bind password, and is best used with -D above.

The exact method you choose will probably heavily depend on the URI in -H. In particular, ldapi:/// represents a Unix domain socket, and allows one to log in using the UIDs present on the local machine. By default, root has access to both the config database (at cn=config) and the regular database (under your dc components). As root, you will generally want to use <ldapcommand> -H ldapi:// -Y EXTERNAL to perform work. **Do not use this as a non-root user, especially on cn=config**; the default privileges restrict anyone but root from even knowing about the existence of the cn=config base DN.

Eventually, for some work (or testing), you might want to log in as the administrator you set up. This is usually accomplished with <ldapcommand> -H ldap://localhost/ -D "cn=admin,<dc_components>" -W. You will be prompted for the password you entered during setup. It should be noted that OpenLDAP, by default, will add this user DN to a special config attribute on the database called "olcRootDN", which means that (1) the password is dictated by "olcRootPW" on this database, and not necessarily by the password in "userPassword", and (2) cn=admin will have unrestricted access regardless of the settings of "olcAccess" on that database. (More on that later.)

The commands that are most important in this set are:

  • ldapsearch, which takes -b <baseDN> and an optional -s <scope>, which searches for the baseDN and returns results in LDIF format. Scope may be one of:
    • sub (default if omitted): returns the subtree--the baseDN and all of its descendants.
    • one: returns the baseDN and all of its immediate children.
    • children: returns only the children of the baseDN.
    • base: returns only the baseDN.
  • ldapmodify, which takes -f <file> (or input from stdin) and alters the database according to a modification (usually addition, replacement, or deletion of attributes).
  • ldapadd, which takes similar to the above, but adds records to the database.
  • ldapmodrdn, which moves records.
  • ldapdelete, which deletes records.

You may want to read through the search results in entirety to get a feeling for LDIF. Note well that ldapmodify with replacement will replace all values of a multivalue attribute, and that you are responsible for ensuring the correct ordering afterward--it cannot be used for insertion or replacement of a single element. For more on the syntax, I direct you to the man pages--each of which has at least one simple, contrived example that will get you pointed in the right direction. Keep in mind that, returning valid LDIF, ldapsearch may be used to transfer or convert databases in mass as well.


LDAP is a bit tedious to work with on its own, if you can't tell; the LDIF format, while standardized and textual, is quite verbose, and not easy to work with even in fairly powerful text editors (like vim :). More powerful users may prefer to use LDAP bindings in their preferred languages; OpenLDAP is itself a C library, and bindings are available for various scripting languages, including Python and PHP. Web UIs are also available, but many (as FreeIPA and others) impose additional structure on the LDAP database and introduce various schemas that may or may not interoperate well with the base standards. I prefer the direct access affored by phpldapadmin--written with said scripting language bindings--since it doesn't try to parse or specialize any of the data, and likely won't fail in unexpected ways if I end up breaking something.

First things first, in your root console, apt-get install phpldapadmin. This will also install apache2 if you haven't gotten that already. In any case, the apache2 instance will be configured such that phpldapadmin will be mounted on your site at /phpldapadmin/. I recommend changing this, as it is a well known path and likely to be hammered by various automated password crackers (which are probably readily aware of the existence of a cn=admin,<dc_components> account on LDAP anyway). To do so, go into /etc/apache2/conf-enabled and find phpldapadmin.conf, then find within that the following lines (near the top):

# Define /phpldapadmin alias, this is the default
<IfModule mod_alias.c>
    Alias /phpldapadmin /usr/share/phpldapadmin/htdocs

Change the first argument of the Alias as you see fit, then run /etc/init.d/apache2 restart in your root shell.

By now, if you hit the IP address of your LDAP server in your web browser, you should see either the default page (if you just installed apache2 with phpldapadmin) or whatever was mounted on your root before. Navigate to whatever you changed the Alias to (or /phpldapadmin if you didn't), and you should see a login present. (If not, try to fix that before continuing; doing so is beyond the scope of this document.)

Of course, phpldapadmin has some unfortunate defaults, using the well-known example domain "example.com" (dc=example,dc=com) as its default base DN. We'll need to change that. Navigate yourself to /etc/phpldapadmin and open config.php, searching for a line like the following:

/* Array of base DNs of your LDAP server. Leave this blank to have phpLDAPadmin
   auto-detect it for you. */

First off, if the line is commented with either # or //, uncomment it. Then proceed to change the DN in the single quotes to a DN of your desire. If you'd like, you can comma-separate more DNs (in single quotes) to add them. I like to add cn=config to this, and find that commenting the line out (to leave it blank) won't add this base for you.

While we're in here, it would be a good idea to change the URI as well; find:

/* Examples:
           (Unix socket at /usr/local/var/run/ldap) */

and (since we're on the same machine) change the URI to ldapi:///. This will prevent unnecessary network traffic and encodings by using a Unix domain socket for IPC.

Finally, you may want to find this line as well:

/* The DN of the user for phpLDAPadmin to bind with. For anonymous binds or
   'cookie','session' or 'sasl' auth_types, LEAVE THE LOGIN_DN AND LOGIN_PASS
   BLANK. If you specify a login_attr in conjunction with a cookie or session
   auth_type, then you can also specify the bind_id/bind_pass here for searching
   the directory for users (ie, if your LDAP server does not allow anonymous
   binds. */
#  $servers->setValue('login','bind_id','cn=Manager,dc=example,dc=com');

and either comment it out (as the line below it) or change it to a proper account. Note that I do not recommend revealing your cn=admin DN, so I recommend either commenting it out, or replacing it with a "template" that will be used to populate the login name field (like "cn=,ou=users,<dc_components>") so that regular users have an easier time logging in.

Administrating cn=config

If you've added cn=config to your root DNs, you will need to permission them properly. This deserves its own subsection, because it will probably be the first (and hopefully last) time you have to get your hands dirty with the ldap-utils tools. Get back on your root terminal, and run something to the effect of ldapsearch -Y EXTERNAL -H ldapi:/// -b 'olcDatabase={0}config,cn=config'. The following should spit out on your terminal:

# {0}config, config
dn: olcDatabase={0}config,cn=config
objectClass: olcDatabaseConfig
olcDatabase: {0}config
olcRootDN: cn=admin,cn=config
olcAccess: {0}to * by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external
 ,cn=auth manage by * break

This means that only local root can do anything (including even see) this database. We would like to make sure that something we can bind to remotely can do this configuration (we're not running a web browser on our headless server, right?). Luckily, it's pretty easy; retrieve your last command with Up and redirect its output to a file (anywhere--/tmp is a good place); then, edit it until it looks something like this:

# {0}config, config
dn: olcDatabase={0}config,cn=config
changetype: replace
replace: olcAccess

olcAccess: {0}to * by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external
 ,cn=auth manage by dn.exact="cn=admin,<dc_components>" manage by * break

The green sections are bits you'll want to add yourself. And, yes, you'll want to delete all the other cruft as well. Once you're done doing that, write the file back out and run it through ldapmodify -Y EXTERNAL -H ldapi:/// -f <file>. The changes will be commited, and your cn=admin account should have access to cn=config now (for remote configuration).

Setting up SSL

Note: While these instructions suffice for a quick-and-dirty setup, it should be noted that COSI now has its own CA certificate, and it is used properly in the real setup. See that page for more details.

If you're going to be using phpldapadmin over an untrusted or insecure network, it's generally a good idea to setup SSL/TLS on the webserver. In the most recent configuration, it's pretty simple; in your root terminal, run a2enconf ssl to create the relevant symlinks. Then, create a certificate. It's a bit of a process to go through to create a real CA and a real service certificate, or to get it from some other CA, so (for now) we can stick with a simple self-signed certificate, which can be made with the somewhat arduous command line:

openssl req -new -x509 -days 1000 -keyout key/phpldapadmin.key -out crt/phpldapadmin.crt -nodes

This more or less breaks down to the following:

  • -new creates a certificate request or certificate,
  • -x509 makes it into a certificate instead of a request,
  • -days controls its validity period,
  • -keyout is where you want the key to go,
  • -out is where you want the certificate generated to go,
  • -nodes prevents the key from being encrypted--Apache will need to read it plainly. You'll probably want to chmod 600 this key.

Finally, find your default site in /etc/apache2/sites-enabled and add anywhere in the VirtualHost specification:

SSLEngine on
SSLCertificateFile "/path/to/crt/phpldapadmin.crt"
SSLCertificateKeyFile "/path/to/key/phpldapadmin.key"

Restart Apache (/etc/init.d/apache2 restart) and direct your browser to that server with the HTTPS protocol. You will be warned about an invalid certificate (browsers do not like self signed certificates even with all else good), but once you confirm an exception, your communication with the server will be properly encrypted.

Now that you have a key and certificate, you can also take a moment to setup LDAPS--which will be important if you plan on using LDAP authentication for remote services, as this means passwords may traverse the network. The easiest way I've found to do so is to stop the service (/etc/init.d/slapd stop), and find the file /etc/ldap/slapd.d/cn=config.ldif, adding amongst the LDIF (in proper format, which isn't too hard):

olcTLSCertificateFile: /path/to/crt/phpldapadmin.crt
olcTLSCertificateKeyFile: /path/to/key/phpldapadmin.key

Then, in /etc/default/slapd, add "ldaps:///" to the SLAPD_HOSTS. Once you start slapd, you should be able to confirm (e.g., with lsof -i tcp) that it is listening on LDAPS.

Configuring the database

Now that you have the ability to administrate all the important parts of your database from the web interface, you can drop your root shell and grab your web browser. Direct it to the site (HTTPS, preferably) and log in as your cn=admin account. We can move on to the fun part now :)

Presumably, you have access to cn=config now, and it should be in phpldapadmin. Go into olcDatabase={1}whatever,cn=config and make sure that the olcAccess lines look reasonable (they're generally pretty readable). If you want to change something, feel free to do so now or later. If you look down in that record, you'll also see olcRootDN, initialized to your cn=admin account. This means you will never lose administrative access to the database when bound as that DN, so keep in mind that you will be overriding access restrictions while doing this.

Now would be a fun time to start adding users and groups--we'll use posixAccounts and posixGroups, respectively. Since posixAccounts require a mandatory GID, we'll be creating a group first.

It should be noted that, with the posix* accounts, resolving authentication groups is non-trivial; posixAccount objects hold no information as to their group membership--all members are listed in the groups. This means you should keep the number of groups small, as the number of requests needed to resolve a group and its users is constant time (one request), but the time required to resolve a user and its groups is linear with the total number of groups. Keep that in mind.

Before we add a posixGroup, let's get organized. Create an Organizational Unit (OU) under your dc components, and call it "groups" or something of the sort. (Think of an OU as a filesystem directory.) Now, under that OU, create a new (child) entry, and let it be a posixGroup.

From within phpldapadmin, you can click on the "Schema" link to see the schemas of the LDAP classes and attribute types--we can try this now with our posixGroup (don't worry, you can create it later, too). Click "Schemas", where you'll see an exhaustive list of object classes, and select "posixGroup" from the dropdown. It should show us that we REQUIRE "cn" (common name--the name of the group as it will appear everywhere) and gidNumber (its numeric ID), and that the attributes "description", "memberUid" (the usernames of member users--a multivalued attribute), and "userPassword" (see man newgrp) are OPTIONAL. Now that we know what we need, we can do the thing :)

Go back into ou=groups,<dc_components> and create a child entry. You can use either phpldapadmin's automatic Posix Group template, or just use Default--both are just about as easy, though the template for Posix Group will suggest a gidNumber for you. I prefer control over the GID namespace (especially because phpldapadmin chose 500, well within the system block of GIDs), so I'd use Default. Configure away--and try to choose very high numbers for your numeric IDs so they don't conflict with the lower, 1000+ IDs typically used for local accounts :)

Setting up Kerberos

Kerberos is a trusted-third-party PKI which provides cryptographically strong proof of authenticity for both services and users of those services. It is not based in X.509, like OpenSSL, but rather an implementation of a distinct protocol.

In the usual case, Kerberos is pretty elegantly simple; a KDC server runs two services, one for the usual Kerberos protocol (that involves distributing tickets), and one for administration (e.g., things like users changing passwords). The central entities of Kerberos are this KDC, the principals (which are like, but not exactly corresponding to users--a user can have multiple principals with different privileges, and a service can own principals too), and tickets, which are ephemeral certificates of authenticity issued by the KDC.

Kerberos' cryptography is much stronger than LDAP, in the sense that LDAP would gladly send the password for a BIND operation in plaintext if you configured it to do so (which is why setting up LDAPS is a good idea). Furthermore, as bona fide certificates, if the key for a ticket is leaked, it only remains compromised until the expiry of that ticket. Of course, the user's password can still be compromised, which will allow an attacker to get as many tickets as needed...

Configuring Kerberos to work together with LDAP requires two points of contact:

  1. LDAP may be configured as the database backend for Kerberos. Note that the storage for Kerberos principals must, in the most straightforward setup, be distinct from the set of (POSIX) users. They are two different entities.
  2. Kerberos principals will be named to coincide with LDAP users, despite the two being very different entities. Yes, this does result in a little data duplication (of usernames, and particularly grievously, of passwords), but the two systems work together surprisingly well once properly configured; LDAP provides most of the groups and passwd data, aside from the password for authentication, which is provided by Kerberos.

So, why use Kerberos if it takes more work and requires more out of the LDAP database for doing nothing more than replicating one of its present features? In one word: NFS. Kerberos tickets are the only sensible way to secure an NFS share such that an attacker with theoretically arbitrary packet inspection/injection capabilities cannot interfere with (or, if so chosen, exfiltrate) data in a file share. Because local machines are issued one--and only one--ticket per user, compromise of that machine still will only grant enough privileges to modify the share on behalf of all of the users whose tickets are present on that machine. For public machines, such as the lab machines, with which just about every user is trusted with root access, this usually limits the damage to the sole user at the time (usually the one who sudo'd), and thus exposes no additional privileges in that case. Of course, NFS isn't the only service which can be protected thusly; Kerberos tickets can be negotiated through HTTP by various UAs, and a quick search for "kerberos" in the APT repositories shows plenty of software ready to take advantage of that, and theoretically any software that links against the Kerberos libraries can use these security features to their benefit. Nonetheless, at the time of this writing, NFS remains the only (and perhaps most important) user of the Kerberos standards and protocol.