Featured image of post Setting up LLDAP Client with SSSD, PAM, NSS and SSH

Setting up LLDAP Client with SSSD, PAM, NSS and SSH

Introduction

In the previous post, we deployed an LLDAP server for centralized user management. The next step is configuring Linux clients so users stored in LLDAP can log in over SSH without creating local accounts.

This client configuration relies on four components:

  • SSH for public key authentication
  • SSSD (System Security Services Daemon) for LDAP integration and caching
  • NSS (Name Service Switch) for user and group resolution
  • PAM (Pluggable Authentication Module) for session management and home directory creation

Because this modifies the system authentication stack, even small configuration mistakes can prevent users from logging in. For that reason, the role is designed to be predictable and idempotent.

LLDAP Custom User Attributes

Before getting into the technical details, we need to prepare the LLDAP schema. Log in to the LLDAP server, open the User Schema, and add the custom attributes shown in the image:

LLDAP Custom User Attributes

These attributes will later be mapped from LLDAP to the Linux account model.

uidnumber and gidnumber represent the numeric UID and GID values expected by Linux. LLDAP uses UUIDs internally by default, so these attributes allow us to map directory users and groups to traditional Linux identities.

Sequence Diagram

Several components participate in LDAP-backed authentication, so it helps to understand how they interact before we start configuring them. Below is the sequence diagram to help us better understand and visualize the flow:

Sequence Diagram

In a normal SSH scenario, the user connects with a private key and SSHD verifies it against the local authorized_keys file stored on the VM. With LDAP, things are different. The authorized keys are centrally stored instead. In our setup, when a user connects to a VM, SSHD invokes sss_ssh_authorizedkeys, which is a helper utility that will query SSSD to retrieve the public key of the user.

SSSD first checks whether the entry exists in cache. If there is a cache hit, the user’s public key is returned and the normal SSH authentication flow proceeds. Otherwise, SSSD sends the request to the LDAP server, retrieves the authorized_keys entry, and then continues the SSH flow.

Once the user is authenticated, PAM invokes pam_mkhomedir to create the user’s home directory if it does not already exist. That keeps the full login workflow automated and split across the right layers.

Install Required Packages

With the authentication flow in mind, we can start configuring the client. The first step is installing the required packages:

  • sssd: Actual SSSD service and daemon.
  • sssd-tools: Contains helper tools such as SSSD cache.
  • libnss-sss: NSS module that allows Linux applications (id, getent etc.) to resolve users and groups through SSSD.
  • libpam-sss: PAM module that allows PAM to communicate with SSSD.
  • ldap-utils: Optional utility that is useful for testing and debugging.

Our Ansible task will take care of this for us:

1
2
3
4
5
6
7
---
- name: Install required packages (with cache update)
  apt:
    name: "{{ lldap_required_packages }}"
    state: present
    update_cache: true
    cache_valid_time: 3600

PAM Configuration

PAM offers a wide set of tools for handling authentication. For this setup, we only use it for session handling. More specifically, PAM creates the user’s home directory during login. This is done by editing /etc/pam.d/common-session and adding the line:

1
session required pam_mkhomedir.so skel=/etc/skel/ umask=0022

pam_mkhomedir automatically creates a user’s home directory on first login using /etc/skel as the template.

Injection of the PAM configuration is handled through this task:

1
2
3
4
5
6
7
8
---
- name: Ensure mkhomedir is enabled in "{{ lldap_pam_common_path }}"
  ansible.builtin.blockinfile:
    path: "{{ lldap_pam_common_path }}"
    marker: "# {mark} ANSIBLE MANAGED: mkhomedir"
    block: "{{ lldap_pam_mkhomedir_line }}"
    state: present
    create: false

SSH Configuration

Three changes need to be added in /etc/ssh/sshd_config:

1
2
3
4
lldap_sshd_confs:
  - "PubkeyAuthentication yes"
  - "AuthorizedKeysCommand /usr/bin/sss_ssh_authorizedkeys"
  - "AuthorizedKeysCommandUser nobody"

AuthorizedKeysCommand tells SSHD to use SSSD for authorized key retrieval. AuthorizedKeysCommandUser nobody tells SSH which user should run the command to query SSSD. nobody is a low-privilege system account with minimal permissions and is much safer than running the command as root.

Another single Ansible task is enough to take care of that:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
---
- name: Configure SSH public key integration block
  ansible.builtin.blockinfile:
    path: "{{ lldap_sshd_config_path }}"
    marker: "# {mark} ANSIBLE MANAGED: ldap client sshd options"
    insertafter: EOF
    block: |
      {% for conf in lldap_sshd_confs %}
      {{ conf }}
      {% endfor %}
    state: present
    backup: true
  notify: restart ssh

restart ssh is only notified when the block changes. If the settings are already present, the task is a no-op, which makes it safe to run repeatedly without breaking the SSH configuration.

NSS Configuration

The default config file for NSS is located at /etc/nsswitch.conf. On Ubuntu, the default configuration is valid out of the box, without any changes. For completeness, we will provide the default nsswitch.conf:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
passwd:         files systemd sss
group:          files systemd sss
shadow:         files systemd sss
gshadow:        files systemd

hosts:          files dns
networks:       files

protocols:      db files
services:       db files sss
ethers:         db files
rpc:            db files

netgroup:       nis sss
automount:  sss

NSS queries each source in the order listed. In the example above, Linux first checks local files, then the systemd provider, and finally SSSD. If SSSD is reached, it resolves the identity through LDAP.

SSSD Configuration

SSSD is the core of the client configuration because it connects Linux to LDAP. Even small configuration errors can prevent authentication from working.

SSSD Block

1
2
3
4
[sssd]
config_file_version = 2
services = nss, pam
domains = ldap

services defines which SSSD responder services are enabled. The NSS responder allows Linux applications to resolve users and groups through SSSD, while the PAM responder handles session setup, including home directory creation through pam_mkhomedir.

NSS and PAM Blocks

We only need to define the blocks so that SSSD knows they exist. They remain empty:

1
2
3
[nss]

[pam]

These blocks rely on the domain/ldap block defined below. They remain empty, and can even be omitted, since there are no additional configurations to apply.

domain/ldap

The last, and probably most important, section. We need to define:

  • Attribute mapping (the custom user and group attributes that we have created through the web UI)
  • Provider
  • Connection settings
  • TLS settings (disabled for this post)
  • Bind credentials
  • ID mapping and enumeration
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
[domain/ldap]
# Attribute mapping
ldap_user_ssh_public_key = sshPublicKey
ldap_user_home_directory = homeDirectory
ldap_user_shell = loginShell
ldap_user_uid_number = uidNumber
ldap_user_gid_number = gidNumber
ldap_user_name = uid

# Backend providers
id_provider = ldap
auth_provider = ldap

# Connection settings
ldap_uri = {{ lldap_uri }}
ldap_search_base = {{ lldap_base_path }}
ldap_schema = rfc2307

# TLS/SSL settings
ldap_id_use_start_tls = false
ldap_tls_reqcert = never

# Bind credentials
ldap_default_bind_dn = uid=lldap_user_querier,ou=people,{{ lldap_base_path }}
ldap_default_authtok_type = password
ldap_default_authtok = lldap_user_querier

# ID mapping and enumeration
ldap_id_mapping = False
enumerate = False

Bind Credentials

LLDAP provides the read-only lldap_user_querier account for directory lookups. SSSD uses this account to perform LDAP searches by binding with a username and password.

Although the credentials are stored in sssd.conf, the file is readable only by root and the account has read-only permissions. This limits the impact of credential exposure, but the password should still be treated as sensitive.

Ansible Task

First we define the variables that will later be used to render the sssd.conf.j2 template:

1
2
3
4
5
6
lldap_server: lldap-01
lldap_port: 3890

lldap_base_path: "dc=habibi,dc=ops"

lldap_uri: "ldap://{{ lldap_server }}:{{ lldap_port }}"

Finally, here is the sssd.yml Ansible task to bring it all together for setting up SSSD:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
---
- name: Create the sssd.conf file
  ansible.builtin.template:
    src: sssd.conf.j2
    dest: /etc/sssd/sssd.conf
    owner: root
    group: root
    mode: "0600"
  notify: restart sssd

- name: Enable and start SSSD service
  ansible.builtin.systemd:
    name: sssd
    enabled: true
    state: started
    daemon_reload: true

Deployment

All that is left is the playbook that applies the role to the target host:

1
2
3
4
5
6
---
- name: Enable ldap on the ldap_client node
  become: true
  hosts: ldap_client
  roles:
    - ldap_client

After running the palybook, we can ssh with our habibi user:

1
2
3
4
ssh -i habibi_key habibi@ldap-client

Last login: Sun Jul 19 11:53:37 2026 from 192.168.155.244
habibi@ldap-client:~$ 

We have not done any setup on the VM itself, and we did not add any local users or hardcode anything specific to the habibi account. Instead, the VM communicates with the LDAP server when it needs identity data.

If we run the playbook again, it should report 0 changes:

1
2
PLAY RECAP ******************************************************************************************************************************************
ldap-client                : ok=13   changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

Break-Glass Access

Misconfiguration can lock clients out of LDAP-backed login. For that, it is highly recommended to keep one local administrative account with sudo access so you can still log in and fix the client if SSSD, PAM, or SSH is broken.

Conclusion

With the client role in place, the VM can use LLDAP for SSH public keys, user identity lookups, and session setup without any local user provisioning. SSHD, SSSD, NSS, and PAM each handle a small part of the flow, but together they give us a repeatable and centrally managed login path.

The Ansible role is available on GitHub.