sssd and OpenLDAP with TLS enabled
Centralize user management using sssd.
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.
- https://ubuntu.com/server/docs/service-sssd-ldap
- https://ubuntu.com/server/docs/service-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 ourinitial.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.