Introduction

OpenLDAP is the standard open-source LDAP directory server for centralized authentication and user management. Ansible automates the full stack: install OpenLDAP, configure the DIT (Directory Information Tree), manage users and groups, set up TLS, configure replication, and deploy SSSD clients for Linux authentication.

Deploy OpenLDAP Server

---
- name: Deploy OpenLDAP server
  hosts: ldap_servers
  become: true
  vars:
    ldap_domain: example.com
    ldap_base_dn: "dc=example,dc=com"
    ldap_org: "Example Corp"
    ldap_admin_password: "{{ vault_ldap_admin_password }}"
  tasks:
    - name: Set debconf selections for slapd
      ansible.builtin.debconf:
        name: slapd
        question: "{{ item.question }}"
        value: "{{ item.value }}"
        vtype: "{{ item.vtype }}"
      loop:
        - { question: slapd/internal/generated_adminpw, value: "{{ ldap_admin_password }}", vtype: password }
        - { question: slapd/internal/adminpw, value: "{{ ldap_admin_password }}", vtype: password }
        - { question: slapd/password1, value: "{{ ldap_admin_password }}", vtype: password }
        - { question: slapd/password2, value: "{{ ldap_admin_password }}", vtype: password }
        - { question: slapd/domain, value: "{{ ldap_domain }}", vtype: string }
        - { question: shared/organization, value: "{{ ldap_org }}", vtype: string }
      no_log: true
      when: ansible_os_family == 'Debian'

    - name: Install OpenLDAP
      ansible.builtin.apt:
        name: [slapd, ldap-utils]
        state: present
      when: ansible_os_family == 'Debian'

    - name: Start slapd
      ansible.builtin.service:
        name: slapd
        state: started
        enabled: true

    - name: Allow LDAP through firewall
      ansible.posix.firewalld:
        service: "{{ item }}"
        permanent: true
        state: enabled
        immediate: true
      loop: [ldap, ldaps]

    - name: Create base OUs
      community.general.ldap_entry:
        dn: "ou={{ item }},{{ ldap_base_dn }}"
        objectClass: organizationalUnit
        attributes:
          ou: "{{ item }}"
        server_uri: ldap://localhost
        bind_dn: "cn=admin,{{ ldap_base_dn }}"
        bind_pw: "{{ ldap_admin_password }}"
      loop:
        - People
        - Groups
        - Services

    - name: Create default groups
      community.general.ldap_entry:
        dn: "cn={{ item.name }},ou=Groups,{{ ldap_base_dn }}"
        objectClass: posixGroup
        attributes:
          cn: "{{ item.name }}"
          gidNumber: "{{ item.gid }}"
        server_uri: ldap://localhost
        bind_dn: "cn=admin,{{ ldap_base_dn }}"
        bind_pw: "{{ ldap_admin_password }}"
      loop:
        - { name: developers, gid: 5000 }
        - { name: ops, gid: 5001 }
        - { name: admins, gid: 5002 }

Manage Users

- name: Create LDAP users
  community.general.ldap_entry:
    dn: "uid={{ item.uid }},ou=People,{{ ldap_base_dn }}"
    objectClass:
      - inetOrgPerson
      - posixAccount
      - shadowAccount
    attributes:
      uid: "{{ item.uid }}"
      cn: "{{ item.cn }}"
      sn: "{{ item.sn }}"
      givenName: "{{ item.given_name }}"
      mail: "{{ item.email }}"
      uidNumber: "{{ item.uid_number }}"
      gidNumber: "{{ item.gid_number }}"
      homeDirectory: "/home/{{ item.uid }}"
      loginShell: "{{ item.shell | default('/bin/bash') }}"
      userPassword: "{{ item.password }}"
    server_uri: ldap://localhost
    bind_dn: "cn=admin,{{ ldap_base_dn }}"
    bind_pw: "{{ ldap_admin_password }}"
  loop: "{{ ldap_users }}"
  no_log: true

- name: Add users to groups
  community.general.ldap_attrs:
    dn: "cn={{ item.group }},ou=Groups,{{ ldap_base_dn }}"
    attributes:
      memberUid: "{{ item.uid }}"
    state: present
    server_uri: ldap://localhost
    bind_dn: "cn=admin,{{ ldap_base_dn }}"
    bind_pw: "{{ ldap_admin_password }}"
  loop: "{{ ldap_group_members }}"

User Variables

ldap_users:
  - uid: jdoe
    cn: John Doe
    sn: Doe
    given_name: John
    email: jdoe@example.com
    uid_number: 10001
    gid_number: 5000
    password: "{SSHA}hashed_password_here"
  - uid: jsmith
    cn: Jane Smith
    sn: Smith
    given_name: Jane
    email: jsmith@example.com
    uid_number: 10002
    gid_number: 5001
    password: "{SSHA}hashed_password_here"

ldap_group_members:
  - { uid: jdoe, group: developers }
  - { uid: jsmith, group: ops }
  - { uid: jsmith, group: admins }

TLS Configuration

- name: Configure LDAP TLS
  community.general.ldap_attrs:
    dn: cn=config
    attributes:
      olcTLSCACertificateFile: /etc/ldap/ssl/ca.crt
      olcTLSCertificateFile: /etc/ldap/ssl/server.crt
      olcTLSCertificateKeyFile: /etc/ldap/ssl/server.key
    state: exact
    server_uri: ldapi:///
    bind_dn: cn=config
    bind_pw: "{{ ldap_config_password }}"

- name: Force TLS
  community.general.ldap_attrs:
    dn: cn=config
    attributes:
      olcSecurity: tls=1
    state: exact
    server_uri: ldapi:///
    bind_dn: cn=config
    bind_pw: "{{ ldap_config_password }}"

SSSD Client Configuration

---
- name: Configure LDAP authentication on clients
  hosts: ldap_clients
  become: true
  tasks:
    - name: Install SSSD and LDAP client
      ansible.builtin.package:
        name: [sssd, sssd-ldap, ldap-utils]
        state: present

    - name: Deploy SSSD config
      ansible.builtin.template:
        src: sssd.conf.j2
        dest: /etc/sssd/sssd.conf
        mode: '0600'
      notify: restart sssd

    - name: Enable SSSD
      ansible.builtin.service:
        name: sssd
        state: started
        enabled: true

    - name: Enable mkhomedir
      ansible.builtin.command: pam-auth-update --enable mkhomedir
      changed_when: true
# templates/sssd.conf.j2
[sssd]
domains = {{ ldap_domain }}
services = nss, pam, ssh

[domain/{{ ldap_domain }}]
id_provider = ldap
auth_provider = ldap
ldap_uri = ldaps://{{ groups['ldap_servers'][0] }}
ldap_search_base = {{ ldap_base_dn }}
ldap_tls_reqcert = demand
ldap_tls_cacert = /etc/ldap/ssl/ca.crt

# User/group mappings
ldap_user_search_base = ou=People,{{ ldap_base_dn }}
ldap_group_search_base = ou=Groups,{{ ldap_base_dn }}

cache_credentials = true
offline_credentials_expiration = 7

enumerate = false

Password Policy

- name: Load password policy overlay
  community.general.ldap_attrs:
    dn: cn=module{0},cn=config
    attributes:
      olcModuleLoad: ppolicy
    state: present
    server_uri: ldapi:///
    bind_dn: cn=config
    bind_pw: "{{ ldap_config_password }}"

- name: Create password policy
  community.general.ldap_entry:
    dn: "cn=default,ou=Policies,{{ ldap_base_dn }}"
    objectClass:
      - pwdPolicy
      - person
    attributes:
      cn: default
      sn: default
      pwdMaxAge: 7776000       # 90 days
      pwdMinLength: 12
      pwdInHistory: 5
      pwdMaxFailure: 5
      pwdLockout: "TRUE"
      pwdLockoutDuration: 1800  # 30 minutes
      pwdFailureCountInterval: 600
    server_uri: ldap://localhost
    bind_dn: "cn=admin,{{ ldap_base_dn }}"
    bind_pw: "{{ ldap_admin_password }}"

Search and Verify

- name: Search for user
  ansible.builtin.command: >
    ldapsearch -x -H ldap://localhost
    -b "{{ ldap_base_dn }}" "(uid=jdoe)" cn mail
  register: ldap_search
  changed_when: false

- name: Test authentication
  ansible.builtin.command: >
    ldapwhoami -x -H ldap://localhost
    -D "uid=jdoe,ou=People,{{ ldap_base_dn }}"
    -w "{{ test_password }}"
  register: auth_test
  changed_when: false
  no_log: true

Troubleshooting

Check slapd Status

- name: Check slapd is listening
  ansible.builtin.wait_for:
    port: 389
    timeout: 5

- name: Test LDAP connectivity
  ansible.builtin.command: ldapsearch -x -H ldap://localhost -b "" -s base namingContexts
  register: base_search
  changed_when: false

Conclusion

OpenLDAP provides centralized identity management for Linux infrastructure — Ansible automates the server deployment, DIT structure, user/group creation, TLS encryption, and SSSD client configuration. Use community.general.ldap_entry and ldap_attrs modules for declarative directory management. Users defined in YAML variables, password policies enforced centrally, and every client configured from inventory. Identity as code.