sssd and OpenLDAP with TLS enabled

If you're looking for a solution to centralize user management and access to Linux servers then sssd might just be for you.

For development we're using docker to build an openldap image and vagrant for creating a virtual machine provisionned with sssd using the ansible provisioner.

sssd running in the virtual machine accesses files from the default /vagrant directory shared between the host and the virtual machine.

The project structure tree:

├── files
│   └── sssd.conf
├── openldap
│   ├── .env
│   ├── docker-compose.yaml
│   ├── docker-entrypoint.sh
│   ├── Dockerfile
│   └── var
│       └── lib
│           └── ldap
│               └── initial.ldif
├── playbook.yaml
└── Vagrantfile

OpenLDAP

Let's build our openldap container with all the requirements for LDAP and LDAP with TLS.

FROM debian:bullseye-slim

ENV DEBIAN_FRONTEND=noninteractive

RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        ca-certificates \
        gnutls-bin \
        ldap-utils \
        slapd \
        ssl-cert \
        sudo-ldap && \ 
    apt-get clean

RUN mv /etc/ldap /var/lib/ldap/config && \
    ln -vs /var/lib/ldap/config /etc/ldap

RUN usermod -aG ssl-cert openldap

COPY ./docker-entrypoint.sh /docker-entrypoint.sh

ENTRYPOINT ["/docker-entrypoint.sh"]

Our docker-entrypoint.sh script creates certificates and configures our database.

#!/bin/sh

set -eux

: LDAP_ROOTPASS=${LDAP_ROOTPASS}
: LDAP_DOMAIN=${LDAP_DOMAIN}
: LDAP_ORGANIZATION=${LDAP_ORGANIZATION}

[ -d "/etc/ssl/templates" ] && echo "/etc/ssl/templates directory exists." || mkdir /etc/ssl/templates
[ -d "/etc/ssl/csr" ] && echo "/etc/ssl/csr directory exists." || mkdir /etc/ssl/csr

# Create ca key and cert and update ca certificates
openssl genrsa -out /etc/ssl/private/ca.key 4096
openssl req -new -x509 \
    -key /etc/ssl/private/ca.key \
    -out /usr/local/share/ca-certificates/ca.crt \
    -subj "/CN=${LDAP_DOMAIN}"
update-ca-certificates

# Create client and server keys and csrs
openssl genrsa -out /etc/ssl/private/client.key 4096
openssl req -new \
    -key /etc/ssl/private/client.key \
    -out /etc/ssl/csr/client.csr \
    -subj "/CN=admin/CN=${LDAP_DOMAIN}"

openssl genrsa -out /etc/ssl/private/server.key 4096
openssl req -new \
    -key /etc/ssl/private/server.key \
    -out /etc/ssl/csr/server.csr \
    -subj "/CN=${LDAP_DOMAIN}"

# Create client and server openssl conf
cat > /etc/ssl/templates/client.cnf <<EOF
basicConstraints = CA:FALSE
nsCertType = client, email
nsComment = "OpenSSL Generated Client Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth, emailProtection
EOF

cat > /etc/ssl/templates/server.cnf <<EOF
basicConstraints = CA:FALSE
nsCertType = server
nsComment = "OpenSSL Generated Server Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer:always
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
EOF

# Create client and server certificates
openssl x509 -req \
    -in /etc/ssl/csr/client.csr \
    -CA /usr/local/share/ca-certificates/ca.crt \
    -CAkey /etc/ssl/private/ca.key \
    -out /etc/ssl/certs/client.crt \
    -CAcreateserial \
    -days 365 \
    -sha256 \
    -extfile /etc/ssl/templates/client.cnf

openssl x509 -req \
    -in /etc/ssl/csr/server.csr \
    -CA /usr/local/share/ca-certificates/ca.crt \
    -CAkey /etc/ssl/private/ca.key \
    -out /etc/ssl/certs/server.crt \
    -CAcreateserial \
    -days 365 \
    -sha256 \
    -extfile /etc/ssl/templates/server.cnf

# Set openldap ownership and permissions on server key
chown :ssl-cert /etc/ssl/private/server.key
chmod 640 /etc/ssl/private/server.key

if [ ! -e /var/lib/ldap/docker_bootstrapped ]; then
  cat <<EOF | debconf-set-selections
slapd slapd/internal/generated_adminpw password ${LDAP_ROOTPASS}
slapd slapd/internal/adminpw password ${LDAP_ROOTPASS}
slapd slapd/password2 password ${LDAP_ROOTPASS}
slapd slapd/password1 password ${LDAP_ROOTPASS}
slapd slapd/dump_database_destdir string /var/backups/slapd-VERSION
slapd slapd/domain string ${LDAP_DOMAIN}
slapd shared/organization string ${LDAP_ORGANIZATION}
slapd slapd/backend string HDB
slapd slapd/purge_database boolean true
slapd slapd/move_old_database boolean true
slapd slapd/allow_ldap_v2 boolean false
slapd slapd/no_configuration boolean false
slapd slapd/dump_database select when needed
EOF

  dpkg-reconfigure -f noninteractive slapd

  if [ -f /var/lib/ldap/initial.ldif ]; then
    slapadd -l /var/lib/ldap/initial.ldif
  fi

  touch /var/lib/ldap/docker_bootstrapped
  chown -R openldap:openldap /var/lib/ldap/*
else
  echo "slapd already configured"
fi

set -x
exec /usr/sbin/slapd -h "ldap:// ldapi:// ldaps://" -u openldap -g openldap -d -1

The .env file should contain the required environment variables.

cat > .env <<EOF
LDAP_ROOTPASS=$(openssl rand -base64 32)
LDAP_DOMAIN="lazybit.ch"
LDAP_ORGANIZATION="lazybit.ch"
EOF

We build the image using docker compose.

version: "3"
services:
  openldap:
    image: openldap
    build:
      context: .
    environment:
    - LDAP_ROOTPASS=${LDAP_ROOTPASS}
    - LDAP_DOMAIN=${LDAP_DOMAIN}
    - LDAP_ORGANIZATION=${LDAP_ORGANIZATION}
    container_name: openldap
    hostname: openldap
    domainname: lazybit.ch
    restart: always
    ports:
    - "389:389"
    - "636:636"
    volumes:
    - /var/backups
    - slapd:/var/lib/ldap/config/slapd.d/
    - ./var/lib/ldap/initial.ldif:/var/lib/ldap/initial.ldif
volumes:
  slapd:

The docker compose spec mounts our initial.ldif configuration to bootstrap our LDAP server.

dn: ou=groups,dc=lazybit,dc=ch
objectclass: top
objectclass: organizationalUnit
ou: groups

dn: ou=users,dc=lazybit,dc=ch
objectclass: top
objectclass: organizationalUnit
ou: users

dn: uid=bma,ou=users,dc=lazybit,dc=ch
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
objectClass: posixAccount
cn: Bradley Massey
sn: Massey
uid: bma
userPassword: password123
uidNumber: 10000
gidNumber: 10000
loginShell: /bin/bash
homeDirectory: /home/bma

dn: cn=bma,ou=groups,dc=lazybit,dc=ch
cn: bma
objectClass: posixGroup
gidNumber: 10000
memberUid: bma

Start the openldap server using docker compose then configure TLS on the server.

docker compose build && docker compose up -d
docker exec -i openldap ldapmodify -H ldapi:/// -Y EXTERNAL << EOF
dn: cn=config
changetype: modify
replace: olcTLSCertificateFile
olcTLSCertificateFile: /etc/ssl/certs/server.crt
-
replace: olcTLSCertificateKeyFile
olcTLSCertificateKeyFile: /etc/ssl/private/server.key
-
replace: olcTLSCACertificateFile
olcTLSCACertificateFile: /etc/ssl/certs/ca-certificates.crt
-
replace: olcTLSVerifyClient
olcTLSVerifyClient: demand
EOF

sssd

The sssd virtual machine is created using vagrant.

# -*- mode: ruby -*-
# vi: set ft=ruby :

VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  config.vm.define "ubuntu" do |ubuntu|
    ubuntu.vm.box = "ubuntu/bionic64"
    ubuntu.vm.hostname = "ubuntu.lazybit.ch"
  end
  config.vm.provider "virtualbox" do |v|
    v.memory = 4192
    v.cpus = 2
  end
  config.vm.provision :ansible do |ansible|
    ansible.limit = "all"
    ansible.playbook = "playbook.yaml"
    ansible.compatibility_mode = "2.0"
  end
end

The virtual machine is provisoned with sssd, copies the ca.crt, client.crt and client.key from the openldap container, creates a /etc/hosts entry to resolve lazybit.ch to the hosts IP, and configures sssd.

- hosts: all
  tasks:
  - name: apt install sssd
    apt:
      name: ['sssd-ldap', 'ldap-utils']
      state: present
      update_cache: yes
    become: true

  - name: resolve control host from all hosts
    lineinfile:
      dest: /etc/hosts
      line: "{{ ansible_env['SSH_CLIENT'].split() | first }}    lazybit.ch"
      state: present
    become: true

  - name: copy ca certificate to localhost
    local_action: ansible.builtin.command docker cp openldap:/usr/local/share/ca-certificates/ca.crt files/

  - name: copy client certificate to localhost
    local_action: ansible.builtin.command docker cp openldap:/etc/ssl/certs/client.crt files/

  - name: copy client key to localhost
    local_action: ansible.builtin.command docker cp openldap:/etc/ssl/private/client.key files/

  - name: copy sssd configuration
    ansible.builtin.copy:
      src: "files/ca.crt"
      dest: "/usr/local/share/ca-certificates/"
      owner: root
      group: root
      mode: '0600'
    become: true
    register: ca_pem

  - name: update ca certificates
    ansible.builtin.shell:
      cmd: update-ca-certificates
    become: true
    when: ca_pem.changed

  - name: copy sssd configuration
    ansible.builtin.copy:
      src: "files/sssd.conf"
      dest: "/etc/sssd/sssd.conf"
      owner: root
      group: root
      mode: '0600'
    become: true
    register: sssd_config

  - name: reload and restart sssd
    ansible.builtin.systemd:
      state: restarted
      daemon_reload: yes
      name: sssd
    become: true
    when: sssd_config.changed

  - name: configure ldap.conf
    ansible.builtin.blockinfile:
      path: /etc/ldap/ldap.conf
      block: |
        BASE dc=lazybit,dc=ch
        URI ldaps://lazybit.ch
    become: true

  - name: create ldaprc
    ansible.builtin.blockinfile:
      create: true
      path: /home/vagrant/.ldaprc
      block: |
        TLS_CACERT /vagrant/files/ca.crt
        TLS_CERT /vagrant/files/client.crt
        TLS_KEY /vagrant/files/client.key
        TLS_REQCERT try
        SASL_MECH external

  - name: enable pam mkhomedir
    ansible.builtin.shell:
      cmd: pam-auth-update --enable mkhomedir
    become: true

The sssd.conf configures the connection to openldap, nss to sync users except root, and pam to create the users on the host.

[sssd]
config_file_version = 2
domains = lazybit.ch
services = nss,pam

[nss]
filter_groups = root
filter_users = root

[domain/lazybit.ch]
id_provider = ldap
auth_provider = ldap
ldap_uri = ldaps://lazybit.ch
cache_credentials = False
ldap_search_base = dc=lazybit,dc=ch

debug_level = 10
ldap_schema = rfc2307 
sudo_provider = none
chpass_provider = ldap
enumerate = TRUE

ldap_tls_cacert = /vagrant/files/ca.crt
ldap_tls_cert = /vagrant/files/client.crt
ldap_tls_key = /vagrant/files/client.key
ldap_tls_reqcert = demand
ldap_sasl_mech = EXTERNAL

Conclusion

There we have it, sssd authenticates to openldap using TLS certificates, integrates with nss to map the service identities to the system, and integrates with pam to create the user at login. sssd also integrates with autofs for automatic file mounts (e.g. NFS) which is good if you need to manage UNIX access control at scale as using sssd will give you common UID/GID's across instances in the environment.