My musings on technology, science, math, and more

LDAP in Containers

Most of the time, connecting to LDAP is pretty straightforward and is just a matter of applying the right configuration to your application. Or maybe it isn’t even something you need to think about; it could be abstracted away behind an API call. This wasn’t always the case though. In several of my previous jobs, authentication wasn’t just a matter of submitting a username and password; I needed to setup and maintain the system that made that work, both for the server and its clients. Thankfully there was a ton of documentation and guides for making Linux work with LDAP. But what about LDAP in containers?

Times have changed and now we’re building containers, not really needing to worry about a lot of the details of Linux configuration. For the most part, we don’t need much from PAM (and even less from sssd) in containers. That said, sometimes you encounter software that just has to rely on your OS for authentication where LDAP sure comes in handy. Here I describe how to configure your Docker container to leverage LDAP via sssd for users and groups.

The LDAP Server

There are lots of options when choosing an LDAP server but most follow open standards. It shouldn’t matter a ton which you use, at least to experiment with LDAP in containers. Active Directory is a fairly obvious exception, though even that is pretty similar. I chose OpenLDAP, both because it is common and because I’ve crafted a demo Docker image out on Dockerhub for it. I think this should suffice as a stand-in for your company’s LDAP solution. I’ll be launching it as a part of a docker-compose file with a mostly-default configuration.

I’ll try to explain the various configuration options as I go, but I’ll be using the RFC2307 LDAP schema (also known as the “NIS” schema) and I’ll assume a pretty standard configuration for the LDAP server (normal port, STARTTLS, typical objectClasses, etc.). If your scenario is different and you get stuck, feel free to ask questions or leave comments.

Here’s a snippet from the docker-compose.yml for starting up the server:

services:
  ldap:
    image: jgnagy/testldap:latest
    mem_limit: 536870912 # 512M
    privileged: true
    ports:
    - "389:389"
    - "636:636"

The LDAP Client

While I’m certain that most of these steps have equivalents for other Linux distributions, I’m going to use CentOS 7 (based on the centos:7 Docker image). Beyond this, I’ll be using a pretty common package for wiring up Linux with LDAP: sssd. Most of the work for configuring this Docker image will be done on your machine.

The Problem

For the most part, the configuration of the LDAP client system will follow what you’ll find on typical guides like this one. In fact, this guide will be pretty simple in comparison. The main difference is that these guides are written from the perspective of a Linux VM or physical host, not for containers.

Linux, like every operating system, needs some mechanism to start and manage services, such as during startup or shutdown. For Linux and other Unix-like operating systems, this is called the init system. What the init system does and how it works has evolved quite a bit over the years, from the rc script (or collection of scripts), to the SysV init process with its runlevels, to systemd. Linux containers, however, work differently. Containers shouldn’t be thought of as full VMs; they are really wrappers around either a single or a very small number of processes. If the process(es) a container contains die, the container itself should die.

Because of this, containers don’t tend to have an init system (with the exception of things like tini but that is really getting into the weeds). Every guide I’ve been able to find for making Linux work with LDAP relies on managing sssd via your init system. I’ll demonstrate how to get sssd working within your container without the need for a proper init system (much less systemd, which most modern guides rely on).

What Is SSSD Anyway?

I’m not going to attempt a full rundown of how authentication works on Linux or all of SSSD’s capabilities, but it is worth describing why sssd exists and why we need it. Software that leverages PAM defers to it to authenticate users. PAM in turn can either leverage its own libraries for talking to LDAP (like the pam_ldap module) or it can work with an intermediary. Providing authentication is usually only one piece of the puzzle though, as most software also needs information about the user, such as full name, group information, and so on. This is what Name Service Switch (NSS) does. NSS can pull this information from LDAP through its own library (such as the nss_ldap module) or it too can work with an intermediary. There is often even a need for PAM to talk to NSS to authorize users based on their group memberships.

The sssd daemon provides that intermediary layer for both PAM and NSS. It can also provide things like caching, multiple domains/sources, supports kerberos, and is pretty flexible. Using sssd is the right way to get Linux working with LDAP, plus it’s actually a lot easier.

The Solution

We need sssd (and now we know why) and we can’t use an init script to run it. So how do we run it? This actually isn’t very difficult; assuming your configuration is in the default location, your container startup process just needs to include sssd before you run your normal code. Let’s build a working example.

The sssd.conf file

First, we need to prepare an sssd.conf file. This file describes how sssd will communicate with the LDAP server and will describe its behavior for things like caching. Pick a directory to store some files and create sssd.conf, giving it this content:

[sssd]
domains = default
config_file_version = 2
services = nss, pam

[domain/default]
id_provider = ldap
access_provider = simple
ldap_schema = rfc2307
ldap_search_base = dc=planetexpress,dc=com
ldap_id_use_start_tls = True
ldap_tls_reqcert = allow
ldap_uri = ldap://ldap:389
ldap_default_bind_dn = cn=admin,dc=planetexpress,dc=com
ldap_default_authtok_type = password
ldap_default_authtok = GoodNewsEveryone
cache_credentials = False
entry_cache_timeout = 0
enumerate = True
ldap_user_search_base = ou=people,dc=planetexpress,dc=com
ldap_group_search_base = ou=unixgroups,dc=planetexpress,dc=com
ldap_user_name = uid
ldap_user_home_directory = homeDirectory
ldap_user_object_class = posixAccount
ldap_group_object_class = posixGroup
ldap_group_member = memberUid

[nss]
filter_users = root, ldap, named, avahi, haldaemon, dbus, radiusd, news, nscd

I’m not going to go through every line of this configuration since the man pages for sssd and sssd-ldap are pretty complete. Plus, this guide isn’t intended to be a complete primer on LDAP. I will talk about a few though:

  • ldap_schema = rfc2307 – This value needs to match the schema you’re using on your LDAP server. For the test LDAP server image I setup, this is what I’m using. Both rfc2307 and rfc2307bis rely on posixAccount for users, but for groups, rfc2307 uses the memberUid attribute with uid values (meaning the usernames are values). This differs from rfc2307bis which uses the member attribute and user dn values (meaning the user LDAP references are values). This difference is pretty critical as without it, you either need to change the schema of your LDAP server or your group memberships won’t work.
  • ldap_tls_reqcert = allow – This value, as described here, is fairly insecure. This works for development environment as it allows TLS to work, but it doesn’t validate the certificate presented by the server. It would be better to set this to demand, which both validates the certificate provided by the server (including its hostname) and requires that one be presented. If you do set this value to something more strict, be sure to set the ldap_tls_cacert value appropriately as well so the certificate can be verified.

Hopefully the rest of this configuration is pretty easy to follow, though it is pretty likely you’ll need to change some of the values you see in your actual environment. The values as listed above will work with the LDAP test server.

The test Dockerfile

Now let’s setup the test image. Create a file called Dockerfile and give it this content:

FROM centos:7

RUN yum install -y sssd && yum clean all

COPY sssd.conf /etc/sssd/sssd.conf
RUN chmod 600 /etc/sssd/sssd.conf

COPY startup.sh /startup.sh
RUN chmod 744 /startup.sh

CMD ["/startup.sh"]

There isn’t much going on with this. This installs sssd, copies in our sssd.conf file from above, and sets up a startup script.

Configuring PAM

We’ll want to make it so PAM allows SSD users to authenticate. You can do this on an individual service basis through a service-specific PAM config, or you can do it system-wide. In most cases, system-wide is probably what you want. To do this on CentOS/RHEL, you could use authconfig --enablesssdauth --update, though that requires installing another package into our container (and it would make this guide awfully RedHat-specific), so I’d rather avoid it. We really only need to add one line into two files. On CentOS/RHEL, those files are /etc/pam.d/system-auth and /etc/pam.d/password-auth. Both these files require the addition of auth sufficient pam_sss.so use_first_pass at line 6. Thankfully, sed makes this a piece of cake:

sed -i '6iauth        sufficient    pam_sss.so use_first_pass' /etc/pam.d/system-auth
sed -i '6iauth        sufficient    pam_sss.so use_first_pass' /etc/pam.d/password-auth

Afterwards, the first 7 lines of our file should look like:

#%PAM-1.0
# This file is auto-generated.
# User changes will be destroyed the next time authconfig is run.
auth        required      pam_env.so
auth        sufficient    pam_unix.so try_first_pass nullok
auth        sufficient    pam_sss.so use_first_pass
auth        required      pam_deny.so

We’ll make this happen automatically in our startup wrapper script, which we’ll work on now.

The startup script

This script won’t contain much. The point is to be a wrapper around whatever your Docker container would have done before, with the addition of running the sssd daemon.

Create a file called startup.sh with content like the following:

#!/bin/bash

# Wait a sec for OpenLDAP to start
sleep 1

# Run sssd
rm -f /var/run/sssd.pid
sssd

# Allow LDAP users to authenticate
sed -i '6iauth        sufficient    pam_sss.so use_first_pass' /etc/pam.d/system-auth
sed -i '6iauth        sufficient    pam_sss.so use_first_pass' /etc/pam.d/password-auth

# ... continue on with whatever the container was going to do
run_my_awesome_app.sh

Obviously replace run_my_awesome_app.sh with whatever command your container was going to run.

As you can see, we need to make sure that sssd is run before your application. The command will automatically run in the background, meaning it won’t get in the way of the rest of your script. A side effect of running sssd this way is that when the container shuts down, it doesn’t properly stop sssd. This means it will leave its “pid file” laying around. Adding the rm -f line just before launching sssd ensures that the file is gone and allows sssd to start.

You can also see our two sed lines there for configuring PAM.

Testing it all out

Now that we have all the pieces, we can bring them all together. Here’s a docker-compose.yml file that can be used to run this test contain and LDAP:

version: '2'
services:
  ldap:
    image: jgnagy/testldap:latest
    mem_limit: 536870912 # 512M
    privileged: true
    # Uncomment the port to access LDAP from your machine
    # ports:
    # - "389:389"

  testbox:
    build:
      context: ./
      dockerfile: ./Dockerfile
    depends_on:
    - ldap
    links:
    - ldap:ldap

From here, run docker-compose -p ldaptest build, then docker-compose -p ldaptest up and the servers are up and running. This will start the containers in the foreground so you’ll be able to see the logs.

To verify that things are working, open a new terminal and attach to the testbox, then run a few commands to confirm that things are connected. To attach, run docker exec -it ldaptest_testbox_1 /bin/bash. From there, you can run things like getent passwd, which should output something like:

root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/spool/mail:/sbin/nologin
operator:x:11:0:operator:/root:/sbin/nologin
games:x:12:100:games:/usr/games:/sbin/nologin
ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin
nobody:x:99:99:Nobody:/:/sbin/nologin
systemd-network:x:192:192:systemd Network Management:/:/sbin/nologin
dbus:x:81:81:System message bus:/:/sbin/nologin
sssd:x:999:997:User for sssd:/:/sbin/nologin
zoidberg:*:11107:100:John A. Zoidberg:/home/zoidberg:/bin/bash
fry:*:11103:100:Philip J. Fry:/home/fry:/bin/bash
professor:*:11106:100:Hubert J. Farnsworth:/home/professor:/bin/bash
bender:*:11102:100:Bender Bending Rodríguez:/home/bender:/bin/bash
hermes:*:11104:100:Hermes Conrad:/home/hermes:/bin/bash
amy:*:11101:100:Amy Wong:/home/amy:/bin/bash
leela:*:11105:100:Turanga Leela:/home/leela:/bin/bash

You can confirm that LDAP groups work too with getent group, which should include output containing:

linuxusers:*:1101:fry,leela,professor

If you see the above output, you’re all connected to the test LDAP instance!

Further Reading

To see the specifics of how the LDAP server is configured, check out the GitHub repo.

To learn more about LDAP, check out the LDAP Wiki.

Spread the love

One response