DEBOPS.LDAP(5)

NAME
debops.ldap - Manage system-wide LDAP configuration and directory objects

SYNOPSIS

debops run service/ldap [--limit group,host,...] [--diff] [--check] [--tags tag1,tag2,...] [--skip-tags tag1,tag2,...] [<ansible-playbook options>] ...

debops run site --tags role::ldap [playbook] ... [--limit group,host,...] [--diff] [--check] [--skip-tags tag1,tag2,...] [<ansible-playbook options>] ...

debops run site --tags role::ldap:tasks [playbook] ... [--limit group,host,...] [--diff] [--check] [<ansible-playbook options>] ...

debops run site --skip-tags skip::ldap [playbook] ... [--limit group,host,...] [--diff] [--check] [--tags-tags tag1,tag2,...] [<ansible-playbook options>] ...

DESCRIPTION

Lightweight Directory Access Protocol <https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol> is a popular and standardized protocol that permits applications to access structured data. It can be used to build and manage clustered environments with central authentication and access control based on directory services that allow efficient searching and retrieval of data.

The debops.ldap Ansible role can set up system-wide LDAP configuration on a Debian/Ubuntu host, and provide LDAP-based applications, as well as other Ansible roles, information necessary to connect them to a LDAP directory service. In addition to that, debops.ldap role can be used via Ansible inventory, or as a dependent role by other Ansible roles, to perform tasks in the LDAP directory itself, on behalf of the Ansible user.

If you are looking for a LDAP server management solution, check out the debops.slapd Ansible role that can manage OpenLDAP servers.

GETTING STARTED

  Ansible Controller requirements

If you plan to use this role to perform LDAP tasks in the default configuration, you need to install the python-ldap Python package in the Ansible environment on the Controller host.

By default the role uses pass (Password Store <https://www.passwordstore.org/>) as a password manager to store LDAP user credentials securely using GnuPG. As a fallback, you can also provide the required password using an environment variable on the Ansible Controller, or configure your own password lookup method.

  LDAP directory initialization

The base directory structure used by DebOps roles is defined and managed by the debops.slapd Ansible role. The slapd__structure_tasks variable contains a list of LDAP objects which will be created during the server installation, which conform to the LDAP Access Control List configuration.

The ansible/playbooks/ldap/init-directory.yml Ansible playbook can be used to create an admin account in the LDAP directory and to assign it to the "LDAP Administrator" and "UNIX Administrator" roles. To use it with a newly configured OpenLDAP server, run the command:

debops run ldap/init-directory -l <slapd-server>

The playbook will use the current UNIX account information on the Ansible Controller (username, etc, from the passwd database and SSH public keys from ssh-agent) to create a new user account with administrative privileges in the LDAP directory.

The user will first be asked for a new password for the admin account which will be used in the future to bind to the directory. If no password is provided or Ansible is run in non-interactive mode, a random password will be generated.

Next, the user will be asked whether the password should be stored on the Ansible Controller using the Password Store utility. If not, and the password is randomly generated, it will be stored under the secret/ hierarchy. If the password was not randomly generated and the Password Store is not being used, the password will not be stored (under the assumption that it is memorized) and will have to be provided manually, e.g. using the DEBOPS_LDAP_ADMIN_BINDPW environment variable, in future playbook runs. See LDAP tasks and administrative operations for further details.

The various defaults used in the playbook can also be overridden on the command line using the --extra-vars argument:

debops run ldap/init-directory -l <slapd-server> --extra-vars="admin_user=ansible admin_use_password_store=False"

The playbook will not make any changes to any existing LDAP entries other than the administrative user.

NOTE: For the LDAP access to work, the Ansible Controller needs to trust the Certificate Authority which is used by the OpenLDAP service. If you rely on the debops.pki internal CA, you will have to add the Root CA certificate managed by the role to the operating system certificate store.

  Example inventory

The debops.ldap role is included in the DebOps common playbook, therefore you don't need to do anything special to enable it on a host. However it is deactivated by default.

To enable the role, define in the Ansible inventory, for example in the ansible/inventory/group_vars/debops_all_hosts/ldap.yml file:

ldap__enabled: True

The debops.ldap role is used by many other DebOps roles, and enabling it will affect the environment and configuration of multiple services, including basic things like UNIX system groups used to manage the host. It's best to either not enable LDAP support in a given environment, or enable it at the beginning of a new deployment (but after administrative access has been configured, as described above).

The POSIX integration with the LDAP directory can be controlled using the ldap__posix_enabled variable. If it's set to False, services that are specific to a POSIX environment (nslcd, sshd, sudo and others) will not be configured with LDAP support. In such case only higher-level applications like nullmailer, Postfix, GitLab, etc. will be configured for use with LDAP.

You can of course enable LDAP support in an existing environment, but you should first learn about changes required by other Ansible roles for successful migration. Check the documentation of other DebOps roles for more details.

  Example playbook

If you are using this role without DebOps, here's an example Ansible playbook that uses the debops.ldap role:

---
- name: Manage LDAP basic configuration
  collections: [ 'debops.debops', 'debops.roles01',
                 'debops.roles02', 'debops.roles03' ]
  hosts: [ 'debops_all_hosts', 'debops_service_ldap' ]
  become: True
  environment: '{{ inventory__environment | d({})
                   | combine(inventory__group_environment | d({}))
                   | combine(inventory__host_environment  | d({})) }}'
  roles:
    - role: python
      tags: [ 'role::python', 'skip::python', 'role::ldap' ]
      python__dependent_packages3:
        - '{{ ldap__python__dependent_packages3 }}'
      python__dependent_packages2:
        - '{{ ldap__python__dependent_packages2 }}'
    - role: ldap
      tags: [ 'role::ldap', 'skip::ldap' ]

  Ansible tags

You can use Ansible --tags or --skip-tags parameters to limit what tasks are performed during Ansible run. This can be used after host is first configured to speed up playbook execution, when you are sure that most of the configuration has not been changed.

Available role tags:

role::ldap
    Main role tag, should be used in the playbook to execute all of the role tasks as well as role dependencies.

role::ldap:tasks
    Run the LDAP tasks generated by the role in the LDAP directory.

  Other resources

List of other useful resources related to the debops.ldap Ansible role:

•   Manual pages: ldap.conf(5) <https://manpages.debian.org/ldap.conf(5)>, ldif(5) <https://manpages.debian.org/ldif(5)>

•   LDAP for Rocket Scientists <http://www.zytrax.com/books/ldap/>, an excellent book about LDAP and OpenLDAP

•   Debian LDAP Portal <https://wiki.debian.org/LDAP> page in the Debian Wiki

•   Ansible community.general.ldap_entry module <https://docs.ansible.com/ansible/latest/collections/community/general/ldap_entry_module.html>, used to manage LDAP entries.

•   The role does not rely on the Ansible ldap_attr module, instead it uses the ldap_attrs module included in the debops.ansible_plugins role to manage LDAP attributes of an entry.

LDAP - POSIX ENVIRONMENT INTEGRATION

The LDAP directory uses a hierarchical structure to store its objects and their attributes, this structure can be thought of as a N-dimesional object. In contrast to this, POSIX or UNIX environments use a flat UID and GID namespace of entities (users, groups, services, etc.) which can be thought of as a two-dimesional surface. There are different ways of representing a N-dimesional objects on two-dimesional surfaces, unfortunately this cannot be done without compromise.

  LDAP-POSIX support in DebOps

The ldap__posix_enabled default variable controls if the LDAP-POSIX integration should be done on a given host. By default the integration will be enabled, based on the value of the ldap__enabled variable. This choice will also be recorded in the Ansible local facts as ansible_local.ldap.posix_enabled variable, which will preserve the current state of the integration on subsequent Ansible runs.

If the POSIX support is disabled by setting the ldap__posix_enabled variable to False, DebOps roles which manage services in the POSIX environment will not configure LDAP support automatically - the required LDAP accounts will not be created and the service configuration will not rely on LDAP directory. Other, higher level services will be integrated with the directory as usual.

  POSIX attributes

This is a list of the LDAP object attributes that are significant in a POSIX environment, managed via the passwd database:

•   uid

•   uidNumber

•   gidNumber

•   gecos

•   homeDirectory

•   loginShell

    And a similar list, for the group database:

•   gid [1]

•   gidNumber

    These attributes are defined by the posixAccount, posixGroup and posixGroupId LDAP object types. All of them are auxiliary [2], and can be added to any LDAP objects in the directory.

    Group membership should be defined by creating a groupOfNames LDAP object with posixGroup and posixGroupId types and using the member attribute to specify the Distinguished Names of the group members.

    User Private Groups <https://wiki.debian.org/UserPrivateGroups> can be defined by adding the posixAccount, posixGroup and posixGroupId to a LDAP object, for example inetOrgPerson. In this case the uid and gid attributes should define the same name. Check the The posixgroupid schema documentation for more details.

  Reserved UID/GID ranges

LDAP directory is commonly used in large, distributed environments as a global account and group database. Because of the long operational lifetime of these environments, counting in dozens of years or more, and issues with modification of UID and GID values in large environments, good selection of the UID/GID ranges reserved for use in the LDAP directory is a priority.

The systemd <https://www.freedesktop.org/wiki/Software/systemd/> project has an excellent rundown of the UIDs and GIDs used on typical Linux systems in their documentation <https://systemd.io/UIDS-GIDS/>. You can also read the Debian FAQ answer that describes the default UNIX accounts and groups <https://www.debian.org/doc/manuals/securing-debian-manual/ch12.en.html#faq-os-users> present on a Debian system. As an example of production UID/GID range allocation, you can check the UID/GID allocation page <https://wiki.cam.ac.uk/cl-sys-admin/UID/GID_allocation> in the documentation published by the University of Cambridge Computer Laboratory.

For convenience, here's a summary of the UID/GID ranges typically used on Linux hosts, copied from the systemd documentation page:
UID/GID | Purpose | Defined By | Listed in
0 | root user | Linux | /etc/passwd + nss-systemd
1…4 | System users | Distributions | /etc/passwd
5 | tty group | systemd | /etc/passwd
6…999 | System users | Distributions | /etc/passwd
1000…60000 | Regular users | Distributions | /etc/passwd + LDAP/NIS/…
60001…60513 | Human Users (homed <https://www.freedesktop.org/software/systemd/man/systemd-homed.service.html>) | systemd | nss-systemd
60514…61183 | Unused |  |
61184…65519 | Dynamic service users <http://0pointer.net/blog/dynamic-users-with-systemd.html> | systemd | nss-systemd
65520…65533 | Unused |  |
65534 | nobody user | Linux | /etc/passwd + nss-systemd
65535 | 16bit (uid_t) -1 | Linux |
65536…524287 | Unused |  |
524288…1879048191 | Container UID ranges <https://manpages.debian.org/unstable/libnss-mymachines/nss-mymachines.8.en.html> | systemd | nss-mymachines
1879048192…2147483647 | Unused |  |
2147483648…4294967294 | HIC SVNT LEONES |  |
4294967295 | 32bit (uid_t) -1 | Linux |

The factors taken into account during the default UID/GID range selection for the debops.ldap role are:

•   Large number of UNIX accounts, both for normal users and applications, starting with 50 000+ entries, with UID/GID of a given account reserved for a lifetime. Yearly increase in the number of accounts being 1000-5000, for example in a typical university.

•   Support for unprivileged LXC containers, which use their own separate subUID/subGID ranges in the same namespace as the LXC host. This implies that the selected UID/GID range needs to be half of maximum size supported by the operatimg system, or less, to allow for unprivileged UID/GID mapping on the LXC host.

•   Support for User Private Groups <https://wiki.debian.org/UserPrivateGroups> defined in the LDAP directory, which allows easier collaboration between users. This means that each UNIX account requires its own private UNIX group, ideally with the same name as the account, and the same UID/GID number.

•   Avoid collisions with existing UID/GID ranges used on Linux systems for local UNIX accounts and groups, or those reserved by common applications like systemd. This implies that the UID/GID numbers <1100 should be off-limits for LDAP directory to not collide with common desktop PC installations. Ideally the 0-65535 UID/GID range should be avoided altogether to allow for a continuous UID/GID range which makes randomized allocation easier.

    With these parameters in mind, the 1879048192…2147483647 UID/GID range, highlighted in the table above, seems to be the best candidate to contain a reserved LDAP UID/GID range.

  Suggested LDAP UID/GID ranges

The debops.ldap role defines a set of Ansible local facts that specify the UID/GID range reserved for use in the LDAP directory. The range is somewhat arbitrary and users are free to change it or not conform to the selected UID/GID range in their environments, however the selected range affects other applications configured by DebOps roles, for example:

•   the range of subUIDs/subGIDs used for unprivileged containers

•   the minimum and maximum UID/GID from the LDAP directory included in the passwd and group databases

•   the range of UIDs/GIDs allocated randomly by account management applications that support this functionality

    and so on. The Ansible roles that want to conform to the selected UID/GID ranges can access them via Ansible local facts:

•   ldap__uid_gid_min -> ansible_local.ldap.uid_gid_min

•   ldap__uid_gid_max -> ansible_local.ldap.uid_gid_max

    To allow for consistent UID/GID allocation in User Private Groups <https://wiki.debian.org/UserPrivateGroups>, a separate UID/GID range at the start of the allocated namespace has been reserved to contain only groups. The UIDs/GIDs above this range should be used only for personal or service accounts with correspodning private groups of the same name and GID as the account. The group range is defined in Ansible local facts as well:

•   ldap__groupid_min -> ansible_local.ldap.groupid_min

•   ldap__groupid_max -> ansible_local.ldap.groupid_max

    The selected LDAP UID/GID range (2000000000-2099999999) allows for 100 000 000 unique POSIX accounts. The range reserved for groups (2000000000-2001999999) supports 2 000 000 unique groups. Users can increase or decrease the group range inside of the maximum UID/GID range, but going beyond that comes with a risk of exceeding the maximum UID/GID supported by the operating system and Unforeseen Consequences. The UID/GID ranges can be divided further between different purposes, but that's beyond the scope of this role.

    With the selected ranges, a set of subUIDs/subGIDs (210000000-420000000) is also possible, therefore this range should be safe to use inside of the LXC containers. Note however, that the UID/GID range above 2147483648 is considered risky due to issues in some of the kernel subsystems and userspace tools that don't work well with UIDs outside of the signed 32bit range. This puts an upper limit on the normal set of UID/GID numbers to 2047483647 if you want to stay away from that region.

    This unfortunately limits the ability to completely separate containers using private subUID/subGID ranges for each of them, but since the UID/GID numbers inside of the containers will belong to the same "entity" be it a person or a service, the risk in the case of breach between LXC containers should be minimized.

  Next available UID/GID tracking

An important part of the POSIX environment is ensuring that UID and GID values are unique across the entire infrastructure. This is problematic with an LDAP directory due to a lack of the "auto-increment" feature which would allow for easy creation of new accounts with unique uidNumber and gidNumber values. Another risk is the possibility of a collision when two or more entities in a distributed environment are trying to create a new account at the same time.

A solution to this is to track the next available uidNumber and gidNumber values inside of the directory itself, using special objects defined by a separate schema and use an atomic LDAP delete+add operation to ensure that the next available UID or GID is reserved for our purposes. This solution was inspired by the UIDNumber Attribute Auto-Incrementing Method <https://www.rexconsulting.net/ldap-protocol-uidnumber.html/> article.

  Design overview

When initializing a LDAP directory, DebOps creates two LDAP objects to track the next available UID and GID separately:

•   cn=Next POSIX UID,ou=System,dc=example,dc=org

•   cn=Next POSIX GID,ou=System,dc=example,dc=org

    The Next POSIX UID object is meant to track user accounts with their corresponding User Private Groups; it will be initialized by the debops.slapd Ansible role with the next available UID after the admin account is created. The Next POSIX UID object is similarly initialized by the same role after all required groups are created. Users can create additional sets of UID/GID tracking objects for various purposes using the uidNext or gidNext LDAP object classes.

    The uidNumber and gidNumber values can be modified by the members of the cn=UNIX Administrators group. The unique overlay ensures that these values are not repeated anywhere in the LDAP directory, and when they are incremented the specified values will be available for use.

  How to acquire a new UID/GID

The mechanism of acquiring a new UID or GID needs to be implemented in the client applications that manage user accounts. Here you can find an explanation of how to get a new UID; getting a new GID is the same, just involves a different LDAP object.

1.  Search for the next available uidNumber value by checking the contents of the cn=Next POSIX UID,ou=System,dc=example,dc=org LDAP entry. An example CLI command:

    ldapsearch -Z -LLL '(& (objectClass=uidNext) (cn=Next POSIX UID) )' uidNumber

    Store the uidNumber value you found in the application memory for now.

2.  Create a "delete + add" LDAP operation (not "replace", which is not atomic). The operation should tell the LDAP directory to remove the specific uidNumber value we found using the search query and add a new one, incremented by 1. An example LDIF with the operation:

    dn: cn=Next POSIX UID,ou=System,dc=example,dc=org
    changetype: modify
    delete: uidNumber
    uidNumber: 2002000001
    -
    add: uidNumber
    uidNumber: 2002000002

3.  Execute the operation on the LDAP directory. If it fails, the existing value won't be changed, so the operation is safe to use. An example CLI command with the above file:

    ldapmodify -Z -f nextuid.ldif

4.  Check the operation status returned by the server. If the operation succeeded, you can use the UID value you got at the first step and be sure that it is unique and available. If the operation failed, it means that somebody else has got the UID you currently keep in memory and it is reserved. In that case go back to step 1, search for the current available UID and try again.

  Collisions with local UNIX accounts/groups

The POSIX environments permit duplicate entries in the passwd and group databases, that is entries with the same user or group names, or duplicate UID/GID numbers. However, most of the time, only the first entry found in the database is returned. This might cause confusion and hard to debug issues in the environment, or even security breaches if not handled properly.

The various DebOps roles that automatically manage custom UNIX groups or accounts, for example debops.system_groups, will check if the LDAP support is enabled on a given host. If it's enabled, they will automatically prepend _ character to any custom UNIX accounts or UNIX groups created by them, which will affect the user or group names, home directory names, sudo rules, group membership, etc. The names of UNIX groups or accounts present by default on Debian or Ubuntu systems (adm, staff, or other such cases) that are managed by these Ansible roles will not be changed. For example, the local equivalent of the LDAP admins group will be changed to _admins. Local UNIX accounts of the administrators (user) will be renamed to _user, and so on.

These changes will not be performed on already configured hosts if the LDAP support is enabled later on, to not create duplicate entries in the local user and group databases. In these cases, administrators are advised to either apply the desired modifications by themselves, or rebuild the hosts with LDAP support enabled from scratch.

Other DebOps or Ansible roles can also implement similar modifications to UNIX user or group names of the applications they manage, but that's not strictly required. LDAP administrators and editors should take care that the user (uid) and group (gid) names don't clash with the UNIX user and group names of different applications installed locally, to not cause collisions.

FOOTNOTES

[1] The gid attribute is defined by the custom posixgroupid LDAP schema, included in the debops.slapd Ansible role.

[2] The posixGroup class is changed from STRUCTURAL to AUXILIARY via the rfc2307bis LDAP schema, installed by the debops.slapd Ansible role.

LDAP TASKS AND ADMINISTRATIVE OPERATIONS

In addition to maintaining the system-wide LDAP client configuration on a host, the debops.ldap role can be used to perform tasks in the LDAP directory itself, using ldap_entry or ldap_attrs [1] Ansible modules. The LDAP tasks are performed via Ansible task delegation functionality, on the Ansible Controller. This behaviour can be controlled using the ldap__admin_* default variables. Check the ldap__tasks documentation for syntax and examples of usage.

  Authentication to the LDAP directory

The role will use the username of the current Ansible user (from the Ansible Controller host) as the value of the uid= attribute to bind to the LDAP directory. This is done to avoid sharing passwords between users of a single administrator account in the LDAP directory.

By default LDAP connection will be bound as a Distinguished Name:

uid=<user>,ou=People,dc=example,dc=org

The DN can be overridden in the ldap__admin_binddn variable, either via the Ansible inventory (this should be avoided if the inventory is shared between multiple administrators), on the command line (using the --extra-vars argument), or using an environment variable on the Ansible Controller:

export DEBOPS_LDAP_ADMIN_BINDDN="cn=ansible,ou=Services,dc=example,dc=org"

How the bind password is obtained is described in the next section. If the bind password is not provided (the ldap__admin_bindpw variable is empty), the LDAP tasks will be skipped. This allows the debops.ldap role to be used in a playbook with other roles without the fear that lack of LDAP credentials will break execution of said playbook.

  Secure handling of LDAP admin credentials

The LDAP password of the current Ansible user is defined in the ldap__admin_bindpw inventory variable.

  Environment variable

By default, the role first checks if the DEBOPS_LDAP_ADMIN_BINDPW environment variable is defined on the Ansible Controller and uses its value as the password during connections to the LDAP directory.

  Plaintext file

Next, the role will look for credentials in the secret/ldap/credentials/ directory. The files in this directory are named based on the UUID of the current user's Distinguished Name (see the previous section).

The UUID conversion is done because LDAP Distinguished Names can contain spaces, and the Ansible lookups don't work too well with filenames that contain spaces. You can use the ldap/get-uuid.yml playbook to convert user account DNs or arbitrary LDAP Distinguished Names to an UUID value you can use to look up the passwords manually, if needed.

  Password Store

Finally, the role will try and lookup the password using the passwordstore <https://docs.ansible.com/ansible/latest/collections/community/general/passwordstore_lookup.html> Ansible lookup plugin. The plugin uses the pass password manager <https://www.passwordstore.org/> as a backend to store credentials encrypted using the GPG key of the user.

The path in the pass storage directory where the debops.ldap will look for credentials is defined by the ldap__admin_passwordstore_path, by default it's debops/ldap/credentials/. The actual encrypted files with the password are named based on the UUID, like for the plaintext password.

You can store new credentials in the pass password manager using the ansible/playbooks/ldap/save-credential.yml Ansible playbook included in the DebOps monorepo. All you need to do is run this playbook against one of the LDAP servers by following this steps:

1.  Make sure you have GPGv2 and pass installed, ie. apt-get install gpgv2 pass

2.  Make sure you have a GPG key pair <https://alexcabal.com/creating-the-perfect-gpg-keypair/>

3.  Initialize the password store: pass init <your-gpg-id>. Example: pass init admin@example.com

4.  Run the playbook debops run ldap/save-credential -l <host>

5.  Re-run the playbook for each user you want to store a password for

    The playbook will ask interactively for the uid= username, and if not provided, for the full LDAP Distinguished Name, and after that, for a password to store encrypted using your GPG key. If you don't specify one, a random password will be automatically generated, saved in the password store, and displayed for you to use in the LDAP directory. The encrypted passwords will be stored by default under ~/.password-store.

  Different modes of operation

The role acts differently depending on the current configuration of the remote host and its own environment:

•   If the debops.ldap role configuration was not applied on the host, the role will set up system-wide LDAP configuration file, and perform the default LDAP tasks, tasks defined in the Ansible inventory, and any tasks provided via role dependent variables which are usually defined by other roles (see Use as a dependent role for more details).

•   If the debops.ldap role configuration was already applied on the host, and there are no LDAP tasks defined by other Ansible roles, the debops.ldap role will apply the default LDAP tasks and the tasks from Ansible inventory (standalone mode).

•   If the debops.ldap role configuration was already applied on the host, and the role is used as a dependency for another role, the default LDAP tasks and the tasks from Ansible inventory will be ignored, and only those provided via the ldap__dependent_tasks variable by other Ansible roles will be executed in the LDAP directory (dependent mode).

    This ensures that the list of LDAP tasks is short, and tasks defined by default in the role, and those defined in the Ansible inventory, which are presumed to be done previously, are not unnecessarily repeated when dependent role LDAP tasks are performed.

    Because the debops.ldap role relies on the LDAP credentials of the current Ansible user, the person that executes Ansible does not require full access to the entire LDAP directory. The role can perform tasks only on specific parts of the directory depending on the Access Control List of the LDAP directory server and permissions of the current user.

FOOTNOTES

[1] Currently a custom ldap_attrs module, included in the debops.ansible_plugins role is used instead of the ldap_attr plugin included in Ansible.

LDAP ACCESS CONTROL

The Lightweight Directory Access Protocol is commonly used to implement access control policies in organizations. Various methods are available, from Mandatory Access Control (MAC) <https://en.wikipedia.org/wiki/Mandatory_access_control> policy which can define directly what entities have access to which services, through the Role-Based Access Control (RBAC) <https://en.wikipedia.org/wiki/Role-based_access_control> scheme which can be used to grant different levels of access to different entities.

This document describes various mechanisms which are available in the DebOps LDAP environment supported by the debops.ldap and debops.slapd Ansible roles. These mechanisms can be used in different services to implement access control to a varying degree, based on the application.

NOTE: Not all rules defined here are implemented in various DebOps roles at the moment.

  Controlling access to LDAP objects in the directory

The debops.slapd role implements a default LDAP Access Control List which can be used to define which LDAP objects have access to data and at what level. By default, read access is granted to almost entire LDAP directory by authorized users; role-based and group-based access control is used to limit read and/or write access to specific LDAP attributes.

  Account-based access control

Applications can use the LDAP bind operation to check if a given username and password combination is valid. To accomplish that, applications can utilize either a Distinguished Name provided by the user, match the username to a personal LDAP entry with the uid attribute stored in ou=People,dc=example,dc=org directory subtree, or use a search query to find the LDAP entry of a person or a service account in the LDAP directory using their username (in the uid attribute) or the provided e-mail address (in the mail attribute). After finding the correct Distinguished Name, applications need to provide the plaintext password over the TLS connection to the LDAP directory which will then verify it and confirm the validity. Successful bind operations should grant access to the application.

This access method is good for services and applications which should be available to all legitimate users in an organization. Anonymous and external users will not be granted access without authenticating first.

Various applications also require their own account objects in the LDAP directory to access its contents. These accounts are usually stored under the host objects in the ou=Hosts,dc=example,dc=org LDAP subtree, or if the applications are external to the organization or are implemented as a cluster, under the ou=Services,dc=example,dc=org LDAP subtree. Application accounts are subject to the LDAP Access Control List rules defined by the OpenLDAP service and may not have access to all of the LDAP entries and/or attributes.

This authorization type is global - any LDAP entry with userPassword attribute can be used to authorize access to a resource.

  Examples of LDAP search queries

Directly check existence of a LDAP entry:

ldapsearch -Z -b "uid=$value,ou=People,dc=example,dc=org" uid

Search for personal Distinguished Name based on username or e-mail address. Esure that only one LDAP entry is returned, more entries result in an error code from LDAP which needs to be handled by the application:

ldapsearch -Z -z 1 -b ou=People,dc=example,dc=org \
           "(& (objectClass=inetOrgPerson) (| (uid=$value) (mail=$value) ) )" dn

Search for service account Distinguished Name based on username and FQDN of the host. Only one LDAP entry is allowed, more entries should result in an error:

ldapsearch -Z -z 1 -b dc=example,dc=org \
           "(& (objectClass=account) (uid=$username) (host=$fqdn) )" dn

  Access control based on group membership

The group LDAP objects, defined under the ou=Groups,dc=example,dc=org LDAP subtree, can be used to control access to resources. These objects usually use the groupOfNames object class with the member attribute which defines the group members. Optionally, these objects can define a corresponding POSIX group using the posixGroup and posixGroupId object classes which can then be used to define access control in an UNIX environment.

The groupOfNames object class enforces at least one group member at all times. Groups can also have defined owners or managers using the owner attribute; in the default LDAP Access Control List configuration group owners have the ability to add or remove group members from the groups they own.

Applications can check the member attribute of one or more groups to determine if a given user or application account belongs to a group and with that information grant or revoke access to resources. Alternatively, the memberOf attribute of the user or account LDAP object can be used to determine group membership and control resource access based on that information.

This authorization type can be either global, or scoped to a particular application with group entries located under the ou=Groups subtree under the application LDAP entry.

  Examples of LDAP search queries

Get the Distinguished Names of LDAP entries which are members of the UNIX Administrators group:

ldapsearch -Z -b "cn=UNIX Administrators,ou=Groups,dc=example,dc=org" member

Get the list of group Distinguished Names a given user belongs to:

ldapsearch -Z -b "uid=$username,ou=People,dc=example,dc=org" memberOf

Find all members of the UNIX Administrators group:

ldapsearch -Z "(memberOf=cn=UNIX Administrators,ou=Groups,dc=example,dc=org)" dn

  Role-based access control

The role LDAP objects, defined under the ou=Roles,dc=example,dc=org LDAP subtree, are similar to the group objects described above. They are usually defined using the organizationalRole object class, and use the roleOccupant attribute to determine the people and accounts which are granted a given role.

The organizationalRole object class does not require any particular members to be present, unlike the groupOfNames object class. This is a good choice to create various roles which don't have existing role occupants - different roles can then be granted to different people or accounts at a later date.

This authorization type can be either global, or scoped to a particular application with role entries located under the ou=Roles subtree under the application LDAP entry.

  Examples of LDAP search queries

Get the Distinguished Names of LDAP entries which are included in the LDAP Administrator role:

ldapsearch -Z -b "cn=LDAP Administrator,ou=Roles,dc=example,dc=org" roleOccupant

  Attribute-based access control

LDAP entries can include the authorizedServiceObject object class which provides the authorizedService attribute. This attribute is a multi-valued string which can be used to define the access permissions to a particular resource. Only "equal" match for this attribute is defined in the LDAP schema, which limits its capabilities to a degree - searching for partial string matches is not supported.

This authorization type is scoped to an LDAP entry, which results in less LDAP queries needed to find out particular access permissions. It can be used to implement Attribute-Based Access Control (ABAC) <https://en.wikipedia.org/wiki/Attribute-based_access_control> authorization scheme.

In DebOps, applications should standardize on a structured format of the attribute values, either all, <service>, <system>, or <system>:<type>.

  Global permissions

The all value grants access to all services and systems and if present, should be the only value of the authorizedService attribute. Any additional values present are nullified by it, therefore if more fine-grained access control is desired, the all value should be removed from the LDAP entry entirely. Client applications are free to implement the meaning of the all value as they choose, however usually the usage in the LDAP search filter will most likely be either all or some specific set of values.

  Service permissions

The <service> value usually means a specific network service daemon, for example sshd, slapd, vsftpd and so on. Since web applications are accessed via a web server, they should use their own separate service or system names to allow more fine-grained access control to each web application. The value grants blanket access to a particular service without fine-grained control over capabilities of the user.

  System permissions

The <system> value is an agnostic name for a set of various services that work together as a whole to accomplish a task. For example, mail would define an access control parameter for the SMTP server, IMAP server, mail filtering software, and the shell string would define access control parameter for the SSH service, sudo access, NSS database service, etc.

Similarly to the <service> value, this value grants blanket access to a particular system as a whole. It means that the system cannot define "global" access and "partial" access at the same time (see below). It might be hard to convert a "global" access permissions to "partial" access permissions, therefore the choice of how to define the access should be selected early on during development.

  Partial system permissions

The <system>:<type> value is a definition of a system access permissions which are split into "parts" of the whole, each part defined by the permission <type>. The partial permissions shouldn't overlap (two or more permissions controlling the same resource access) or be additive (a permission type implying presence of another permission type). There shouldn't be a <system>:all permission as well, since it would nullify partial permissions for a given system.

Each system can define its own set of permission types, however the type names should be as precise and descriptive as possible. A good example is the "mail" system, with the mail:receive permission allowing incoming messages to be received by the e-mail account, the mail:send permission allowing outgoing messages to be sent by the e-mail account, and the mail:access permission granting read-write access to the e-mail account by its user.

It's easy to create additional permission types once the system is implemented, therefore in larger systems this should be a preferred method of access control. The partial permissions shouldn't be mixed with the "global" permission for a given system because that would nullify the partial permissions.

  Examples of LDAP search queries

Get list of access control values of a given user account:

ldapsearch -Z -b 'uid=$username,ou=People,dc=example,dc=org' authorizedService

Find all personal accounts which have shell access or global access:

ldapsearch -Z -b "ou=People,dc=example,dc=org" \
           "(& (objectClass=inetOrgPerson) (| (authorizedService=all) (authorizedService=shell) ) )" dn

Find all LDAP entries which can send e-mail messages or have global access:

ldapsearch -Z -b "dc=example,dc=org" \
           "(| (authorizedService=all) (authorizedService=mail:send) )" dn

  Known access controls

This paragraph lists various authorizedService values which are used by different DebOps roles to grant access to services. You can check the LDAP directory structure organized by DebOps to find what DebOps roles use which access control.
Access control | Description
all | Grants access to all services supported by DebOps. Mutually exclusive with other access controls.
mail:access | Grants read/write access to mail account contents through a service, for example IMAP or POP3.
mail:receive | Allows a given mail account to receive e-mail messages via a service, for example SMTP - the mail account is present in alias and mailbox lookup tables.
mail:send | Allows a given mail account to send e-mail messages via a service, for example SMTP - the mail account is allowed to authenticate to the MTA.
shell | Grants access to the UNIX environment through a service, for example SSH. To be fully effective, a given LDAP entry also needs to have a corresponding host attribute with the posix: value which specifies the hosts on which the accounts and groups are present, as well as required object classes (posixAccount, posixGroup, posixGroupId). See "Host-based access control" below for more details.
web:public | Grants access to various web services which are reachable over public Internet. Different services can also limit access using the host attribute, consult the specific services for details.

Apart from these access controls, different services implement their own access controls based usually on the daemon name (slapd, sshd, etc.). See the corresponding LDAP DIT documentation pages of these roles for more details.

  Host-based access control

The hostObject LDAP object class gives LDAP entries access to the host attribute which is used to store hostnames and Fully Qualified Domain Names of the LDAP entries. The attribute type supports substring (wildcard) matches and can be used to create host-based access rules.

Various services and systems can check for the presence of the host attribute with specific value patterns. The preferred value format in this case should be: <service|system>:<host>, where the <host> can be a FQDN hostname, or a woldcard domain (*.example.org), or the value all for all hosts in the cluster.

A separate URN-like (RFC 8141 <https://datatracker.ietf.org/doc/html/rfc8141.html>) scheme is also available to allow for definition of POSIX accounts or groups that is independent from the DNS database, for example to distinguish hosts by their role like "production", "development", etc. This can be defined using the format: <service|system>:urn:<pattern>. Ansible roles are free to match any number of URN-like patterns in LDAP filters defined in the services they manage.

For example, POSIX accounts and groups can be split into "development" and "production" servers using separate URN-like names:

•   posix:urn:dev:app1

•   posix:urn:dev:app2

•   posix:urn:prod:app1

•   posix:urn:prod:app2

    Then, hosts that should support app1 for both development and production, can look for the URN: posix:urn:*:app1. hosts which are meant only for development, can look for URN: posix:urn:dev:*, and so on.

    The glob patterns used by DebOps are defined in the ldap__*_urn_patterns variables and can be accessed by other Ansible roles via ansible_local.ldap.urn_patterns local fact. The default URN pattern defined by the debops.ldap role is:

•   deploy:<deployment>

    The <deployment> is set using debops.machine role Ansible local facts and can be set as development, integration, staging or production. POSIX accounts or groups which define a specific URN-like string will be present on a specific set of hosts. For example, to ensure that a given UNIX account is present on production hosts, add the host attribute with value:

•   posix:urn:deploy:production

    By default any URN-like pattern that matches a given LDAP entry will be included in search result. To change that and, for example, match both URN pattern and specific host/domain, you need to edit the LDAP search filters in the roles that use them.

  Examples of LDAP search queries

Get list of POSIX accounts which should be present on a given host and have access to shell services:

ldapsearch -Z -b "dc=example,dc=org" \
           "(& (objectClass=posixAccount) (| (host=posix:host.example.org) (host=posix:all) ) )"

Get list of POSIX accounts which should be present on any host in a specific domain. This uses the substring match to get all entries with a specific domain:

ldapsearch -Z -b "dc=example,dc=org" \
           "(& (objectClass=posixAccount) (| (host=posix:*.example.org) (host=posix:all) ) )"

Get list of POSIX accounts which should be present on all hosts in a specific domain. This query looks for all entries with a wildcard (*.example.org) domain defined as the value:

ldapsearch -Z -b "dc=example,dc=org" \
           "(& (objectClass=posixAccount) (| (host=posix:\2a.example.org) (host=posix:all) ) )"

  Known access controls

This paragraph lists various host values which are used by different DebOps roles to grant access on a per-host basis. You can check the LDAP directory structure organized by DebOps to find what DebOps roles use which access control.
Access control | Description
posix:all | A given POSIX account or POSIX group will be present on all hosts in the cluster.
posix:<fqdn> | A given POSIX account or POSIX group will be present on a specific host defined by its FQDN name.
posix:*.<domain> | A given POSIX account or POSIX group will be present on a specific host defined by its domain name (*. prefix is required).
posix:urn:<pattern> | A given POSIX account or POSIX group will be present on hosts which look for a defined Uniform Resource Name pattern.

USE AS A DEPENDENT ROLE

The debops.ldap role is designed to be used as an API between Ansible roles and the LDAP directory. Roles can define a list of LDAP tasks which are passed to the debops.ldap role using role dependent variables on the playbook level. These LDAP tasks will be executed using the LDAP tasks and administrative operations interface in the LDAP directory.

This API allows the LDAP integration to be focused in a single, specific role (debops.ldap), so that other Ansible roles don't have to implement different ways of accessing and manipulating the LDAP directory by themselves. The LDAP data like passwords, names of objects and attribute values can be defined by the "parent" role in its own variables, and passed to the debops.ldap role to create or change LDAP objects as needed.

NOTE: Examples of usage and integration between roles will be provided in the future using existing DebOps roles.

  LDAP directory structure organized by DebOps

LDAP directory has a hierarchical structure defined by objects and their attributes. Various Ansible roles included in DebOps can interface with the LDAP directory to create or maintain their own set of objects and attributes. The hierarchy of these objects is called the Directory Information Tree <https://en.wikipedia.org/wiki/Directory_information_tree>.

Each DebOps role that is integrated with the debops.ldap role defines a special page in its documentation section, ldap-dit.rst. In this file, users can find a human-readable description of the LDAP objects and their attributes, which are linked to their corresponding role default variables and Ansible local facts. This can be used to quickly locate the relevant source of a particular LDAP object or attribute.

The objects and attributes have example values and Distinguished Names, real objects in the LDAP directory will be named differently depending on the configuration, but the general structure of the LDAP directory should be accurately reflected in the documentation.

The DIT documentation of different roles is cross-referenced between the roles that interact with each other at the LDAP directory level. This allows travelsal between the documentation pages of different roles to quickly find parent and child nodes, and see the relation between them.

The root of the DebOps DIT is defined by the debops.ldap Ansible role. Other roles will rely on it as a dependency, and will use the Ansible local facts defined by this role to create their own LDAP objects. Some of these roles will have their own "child" roles that depend on them, and so on.

DEFAULT VARIABLE DETAILS

Some of the debops.ldap default variables have more extensive configuration than simple strings or lists, here you can find documentation and examples for them.

  ldap__configuration

The ldap__*_configuration variables define the contents of the /etc/ldap/ldap.conf configuration file. The variables are merged in order defined by the ldap__combined_configuration variable, which allows modification of the default configuration through the Ansible inventory. See ldap.conf(5) <https://manpages.debian.org/ldap.conf(5)> for possible configuration parameters and their values.

  Examples

See ldap__default_configuration variable for an example of existing configuration.

  Syntax

The variables contain a list of YAML dictionaries, each dictionary can have specific parameters:

name
    Required. Name of the ldap.conf(5) <https://manpages.debian.org/ldap.conf(5)> configuration option. The configuration options with the same name parameter will be merged in order of appearance.

    In the generated configuration file, the option name will be automatically converted to uppercase.

value
    Required. The value of a given configuration option. It can be either a string, a YAML list (elements will be joined with spaces).

state
    Optional. If not defined or present, a given configuration option will be included in the generated configuration file. If absent, a given configuration option will be removed from the generated file. If comment, the option will be included, but commented out and inactive. If ignore, the role will not evaluate the configuration entry during template generation, this can be used for conditional activation of ldap.conf(5) <https://manpages.debian.org/ldap.conf(5)> configuration options.

comment
    Optional. String or YAML text block that contains comments about a given configuration option.

separator
    Optional, boolean. If True, and additional empty line will be added before a given configuration option to separate it from the other options for readability.

  ldap__tasks

The ldap__*_tasks variables define a custom set of ldap_entry and ldap_attrs Ansible module tasks that will be executed against the LDAP directory, in the specified order. This also requires that the role can access the LDAP credentials of the Ansible user, on the Ansible Controller. See the LDAP tasks and administrative operations for more details; this section describes the syntax of the LDAP tasks themselves.

NOTE: Remember, these are not "Ansible tasks", they are "LDAP tasks" performed in the LDAP directory itself, via Ansible Controller.

  Examples

See the ldap__default_tasks for an example of the default LDAP tasks used by the role.

Create the ou=People branch of the LDAP directory, add a new user account, and set its password, and some other attributes. This example assumes that LDAP directory will hash the provided password after checking its quality. The ldap__* variables are defined as the debops.ldap default variables, but can be overridden via the inventory.

ldap__tasks:
  - name: 'Create the ou=People object'
    dn: '{{ [ ldap__people_rdn ] + ldap__base_dn }}'
    objectClass: [ 'organizationalUnit' ]
    attributes:
      ou: '{{ ldap__people_rdn.split("=")[1] }}'
  - name: 'Create the uid={{ ansible_user }} object'
    dn: '{{ [ 'uid=' + ansible_user, ldap__people_rdn ] + ldap__base_dn }}'
    objectClass: [ 'inetOrgPerson' ]
    attributes:
      cn: 'Ansible User'
      sn: 'User'
      uid: '{{ ansible_user }}'
      userPassword: 'secret'

Remove the default cn=admin,dc=example,dc=org LDAP object created in the directory by the Debian slapd APT package. It's not needed after an admin account has been created.

ldap__tasks:
  - name: 'Remove the default admin account'
    dn: 'cn=admin,{{ ldap__basedn }}'
    state: 'absent'
    entry_state: 'absent'

  Syntax

The ldap__default_tasks, ldap__tasks, ldap__group_tasks and slapd__host_tasks define a list of YAML dictionaries, each list entry defines a ldap_entry or ldap_attrs task to perform in the LDAP directory. The variables are merged together in the order specified by the ldap__combined_tasks variable.

When the debops.ldap role is used as a dependency, only the ldap__dependent_tasks variable will be included in the ldap__combined_tasks list, the default tasks or the ones specified in the Ansible inventory will not be evaluated. See Use as a dependent role for more details.

The entries with the same name parameter will affect each other, replacing the previously defined "instance" of a given task - this can be used to change previously defined tasks conditionally.

The list of task parameters supported by the role:

name
    Required. The name of a given task, displayed during Ansible execution. It's an equivalent of the name keyword in Ansible tasks lists. Its value does not affect the actions performed in the LDAP directory. Entries with the same name are merged together.

dn  Required. The Distinguished Name of the LDAP directory object which will be configured by a given entry. The value can be specified as a string or a YAML list, which will be joined by commas.

    This parameter is case-sensitive, if you use a wrong case here, the LDAP directory will still most likely accept the configuration, but the task list will not be idempotent. When that happens, check the case of the DN value.

    This parameter can contain LDAP object names that use the X-ORDERED type syntax. The LDAP directory will accept new objects that omit the X-ORDERED syntax prefix, but subsequent executions of the role can cause errors due to incorrect DN name. It's best to specify the object prefix number directly from the start. Remember that the LDAP directory can modify the X-ORDERED prefix number on any modification of the list of objects; you should verify the current prefix numbering before applying any changes.

objectClass
    Optional. Specify a name or a YAML list of the LDAP Object Classes which should be used to define a new LDAP directory object.

    If this parameter is specified, the ldap_entry module will be used to perform the operation instead of ldap_attrs module. The ldap_entry Ansible module will not modify the attributes of any existing LDAP directory objects, you need to use a separate configuration entry to do that, which does not specify this parameter.

    This parameter is case-sensitive, if you use a wrong case here, the LDAP directory will still most likely accept the configuration, but the task list will not be idempotent. When that happens, check the case of the objectClass value(s). The parameter name is case-sensitive as well.

attributes
    Required. YAML dictionary which defines the attributes and their values of a given LDAP object. Each dictionary key is a case-sensitive name of an attribute, and the value is either a string, or a list of strings, or a list of YAML text blocks. If list is used for the values, multiple attribute entries will be created automatically.

    If you create configuration entries with the same name parameter, the attributes parameter will replace entirely the same parameter defined in previous entries on the list. This is not the case in the LDAP directory itself, where multiple separate configuration entries can define the same objects and their attributes multiple times, as long as the state is not specified or is set as present. To ensure that a given set of attributes is specified only once in the LDAP directory, you MUST define the state parameter with the exact value.

    The attribute names are case-sensitive, if you use a wrong case here, the LDAP directory will still most likely accept the configuration, but the task list will not be idempotent. When that happens, check the case of the attribute names.

    The attributes can contain lists that use the X-ORDERED type syntax. The LDAP directory will accept new attribute values that omit the X-ORDERED syntax prefix and a new prefix number will be assigned to them automatically by the LDAP directory. Subsequent executions of the role can create duplicate attribute values, if the prefix number is not specified. It's best to specify the attribute prefix number directly from the start. Remember that the LDAP directory can modify the X-ORDERED prefix number on any modification of the list of attributes; you should verify the current prefix numbering before applying any changes.

ordered
    Optional, boolean. If defined and True, the ldap_attrs Ansible module will automatically add the X-ORDERED index numbers to lists of values in all attributes of a current task. This extension is used in the OpenLDAP cn=config configuration database to define order of object attributes which are normally unordered.

    The most prominent use of the X-ORDERED extension is in the olcAccess attribute, which defines the LDAP Access Control List. This attribute should be defined in a separate LDAP task, so that only its values will have the X-ORDERED index numbers inserted. Existing index values will be removed and replaced with the correct ordering defined by the YAML list.

state
    Optional. Possible values:
    State | Description
    present | Default. The role will ensure that a given configuration entry is present in the LDAP directory. There might be more more than one copy of a given entry present at the same time. To avoid creating duplicate entries, use exact instead of present.
    exact | The role will ensure that only the specified set of attributes of a given LDAP object is defined in the LDAP directory. You MUST use this parameter when X-ORDERED type attributes are configured, otherwise the role cannot guarantee that only the specified set of attribute values, as well as their specified order, is defined in a given LDAP object.
    absent | The specified attributes of a given LDAP object will be removed.
    init | The role will prepare a task entry configuration but it will not be active - this can be used to activate prepared entries conditionally.
    ignore | A given configuration entry will not be evaluated by the role. This can be used to conditionally enable or disable entries.

entry_state
    Optional. This parameter should be present only if the entire LDAP object entry is to be removed. Set the entry state to absent to remove it.

no_log
    Optional, boolean. If True, a given task output will not be recorded to avoid emitting sensitive information like passwords. If not specified or False, the task will be recorded and logged.

run_once
    Optional, boolean. If defined and True, a given LDAP task will be executed only one time when the role is applied on multiple remote hosts at once. This might be important in cases where the LDAP directory is replicated, or values from different remote hosts can result in the same LDAP objects, e.g. objects with X-ORDERED index numbers, like LDAP schemas.

AUTHOR
Maciej Delmanowski

COPYRIGHT
2014-2024, Maciej Delmanowski, Nick Janetakis, Robin Schneider and others