/389ds-server

Ansible role to configure 389DS

Primary LanguageHTMLApache License 2.0Apache-2.0

389ds-server

Build Status Ansible Galaxy

This role installs the 389DS server (LDAP server) on the target machine(s).

ansible-galaxy install lvps.389ds_server

Features

  • Install a single LDAP server
  • Configure logging
  • Add custom schema files
  • Enable/disable any plugin
  • Configure DNA plugin for UID/GID numbers
  • Configure TLS
  • Enforce TLS (minimum SSF and require secure binds) or go back to optional TLS
  • Enable/disable LDAPI
  • Enable/disable SASL PLAIN

Replication is managed with another role.

Requirements

  • Ansible 2.7 or newer
  • CentOS 7 or CentOS 8

Role Variables

The variables that can be passed to this role and a brief description about them are as follows.

Variable Default Description Can be changed
dirsrv_suffix dc=example,dc=com Suffix of the DIT. All entries in the server will be placed under this suffix. Normally it's made from the domain components (dc) of your company main domain. E.g. if you're from example.co.uk and the server will be at ldap-server.example.co.uk, set the suffix to dc=example,dc=co,dc=uk, leaving out the subdomain part (ldap-server) since it's irrelevant. No
dirsrv_rootdn cn=Directory Manager Root DN, or "administrator" account username. Bind with this DN to bypass all authorization controls. No
dirsrv_rootdn_password Password for root DN, you must define this variable or the role will fail. No
dirsrv_fqdn {{ansible_nodename}} Server FQDN, e.g. ldap.example.com. If the server hostname is already an FQDN, the default should pick it up. No
dirsrv_serverid default Server ID or instance ID. All the data related to the instance configured by this role will end up in /etc/dirsrv/slapd-default, /var/log/dirsrv/slapd-default, etc... You could use your company name, e.g. for Foo Bar, Inc set the variable to foobar and the directories will be named slapd-foobar. ¹
dirsrv_install_examples false Create example entries under the suffix during installation No
dirsrv_install_additional_ldif [] Install these additional LDIF files, by default none (empty array). This corresponds to the InstallLdifFile directive in the inf installation file. No
dirsrv_listen_host Listen on these addresses/hostnames. If not set (default) does nothing, if set to a string will set the nsslapd-listenhost attribute. Set to [] to delete the attribute. Yes
dirsrv_secure_listen_host Same as dirsrv_listen_host but for LDAPS. If not set (default) does nothing, if set to a string will set the nsslapd-securelistenhost attribute. Set to [] to delete the attribute. Yes
dirsrv_server_uri ldap://localhost Server URI for tasks that connect via LDAP. Since tasks are running on the same server as 389DS, this will be localhost in most cases, no need to customize it. ¹
dirsrv_logging see below see below Yes
dirsrv_plugins_enabled {} Enable or disable plugins, see below for details. By default no plugins are enabled or disabled. Yes
dirsrv_dna_plugin see below Configuration for the DNA (Distributed Numeric Assignment) plugin. Yes
dirsrv_custom_schema [] Paths to custom schema files. They will be dropped into /etc/dirsrv/slapd-{{ dirsrv_serverid }}/schema and a schema reload will be request when anything chages. Yes
dirsrv_allow_other_schema_files false If false (default value), this role will add the specified schema files to /etc/dirsrv/slapd-{{ dirsrv_serverid }}/schema, then delete all other schema files there except 99user.ldif. If your schema files are managed only by this role or dynamically (i.e. from cn=schema, which writes to 99user.ldif), you can leave this variable to its default of false. If you have more schema files in that directory (added manually or by other tasks), set this to true to leave them there. The downside is that if you deploy e.g. 50example.ldif, then you rename it to 50my_example.ldif, when the role runs again it considers it a new file and leaves the previous one there, wreaking havoc on your directory. Yes
dirsrv_tls_enabled false Enable TLS (LDAPS and StartTTLS). All "dirsrv_tls" variables have effect only if this is enabled. Yes
dirsrv_tls_min_version '1.2' Minimum TLS version: 1.0, 1.1 or 1.2. Possibly even 1.3, when support is added to 389DS. SSLv2 and SSLv3 are always disabled by this role. Yes
dirsrv_tls_certificate_trusted true The server certificate is publicly trusted. Set to false only in development (for self-signed certificates)! Yes
dirsrv_tls_enforced false Enforce TLS by requiring secure binds and minimum SSF Yes
dirsrv_tls_minssf 256 Minimum SSF, used only when dirsrv_tls_enforced is true. 128 seems reasonable, 256 should be very secure. Set this to 0 to enforce TLS only with secure binds. Yes
dirsrv_allow_anonymous_binds 'rootdse' Allow anonymous binds: boolean true for Yes, boolean false for No, or 'rootdse'. The Administration Guide suggests to use rootdse instead of No, because it allows anonymous binds to search some data that clients may require before doing a bind. Allowing anonymous binds basically makes your directory public, unless you restrict access with ACIs. Yes
dirsrv_simple_auth_enabled true Enable SIMPLE authentication, probably true unless you want to use SASL PLAIN only or configure other methods manually. Yes
dirsrv_password_storage_scheme [] A single value, possibly the string "PBKDF2_SHA256". Or leave the default, which will delete any custom value and use 389DS default, which should be pretty secure. Yes
dirsrv_ldapi_enabled false Enable LDAPI (connect to the server via a UNIX socket at ldapi:///var/run/dirsrv/slapd-{{ dirsrv_serverid }}.socket). Note that this is subject to TLS enforcing and TLS is not supported, so it's useless if you set dirsrv_tls_enforced to true. Yes
dirsrv_sasl_plain_enabled true Enable SASL PLAIN authentication: if a client tries to authenticate without TLS and TLS is enforced, this kind of authentication should stop it before it sends the plaintext password, while a SIMPLE bind will send the password and then fail because SSF is too low. Yes

Variables exclusive to 389DS version 1.4.X

These variables only affect on installations of 389DS version 1.4.X and have no effect on previous versions even if defined.

Variable Default Description Can be changed
dirsrv_defaults_version 999999999² The defaults configuration values will be the ones of the specified version of 389DS. The format is XXXYYYZZZ, where XXX is the major version, YYY is the minor version and ZZZ is the patch level (all three values are padded with zeros to the length of three). If 999999999 is selected, the latest version of the defaults will be used. No
dirsrv_selfsigned_cert True² Determines wether 389DS will generate a self-signed certificate and enable TLS automatically. No
dirsrv_selfsigned_cert_duration 24² Validity in months of the self-signed certificate generated by 389DS. No
dirsrv_create_suffix_entry False² Determines wether 389DS will generate a suffix entry in the directory with the given suffix: cn={{ dirsrv_suffix }} No

Interoperability between 1.3.X and 1.4.X

To have a playook that behaves in the same way on 1.3 and 1.4 verions of 389DS, the following values should be used:

Variable Value
dirsrv_defaults_version 001004002³
dirsrv_selfsigned_cert False
dirsrv_create_suffix_entry True

Some variables cannot be changed by this role (or at all) after creating an instance of 389DS. If one of them is changed and the role is applied again, undefined behaviour ranging from "nothing" to "the role fails" may happen. Some of them, e.g. the root DN password, can be changed manually: please refer to the Administration Guide for details.

All variables are prefixed with dirsrv because starting a variable name with a number ("389ds") doesn't work that well.

¹ Changing this variable from a previous run will lead to the creation of another instance, another directory completely separated from the previous one. This should work, but it hasn't been tested at all.

² These are the default values as of 389DS version 1.4.2.15 and may change for later versions: run dscreate create-template in your machine to see the effective defaults.

³ This is the version of defaults on top of which this role has been written and validated. Setting the dirsrv_defaults_version is not technically required, but can prevent future updates to the defaults from breaking the playbook by being incompatible with 389DS 1.3. On the other hand, setting the variable will essentially lock the configuration in time and if done for a prolonged period of time might render it obsolete. Use with discrection.

dirsrv_logging

This is the default variable:

dirsrv_logging:
  audit:
    enabled: false
    logrotationtimeunit: day
    logmaxdiskspace: 400
    maxlogsize: 200
    maxlogsperdir: 7
    mode: 600
  access:
    enabled: true
    logrotationtimeunit: day
    logmaxdiskspace: 400
    maxlogsize: 200
    maxlogsperdir: 7
    mode: 600
  error:
    enabled: true
    logrotationtimeunit: day
    logmaxdiskspace: 400
    maxlogsize: 200
    maxlogsperdir: 7
    mode: 600

Ansible doesn't merge dicts by default, i.e. if you want to change only audit > enabled to true you have to define all the other variables too. If you want to change the defaults, it's probably a good idea to copy this entire block into the variables and tweak what you need.

dirsrv_plugins_enabled

If you want to enable the memberof plugin located at cn=MemberOf Plugin,cn=plugins,cn=config, set the variable to:

plugins_enabled:
  MemberOf Plugin: true

If it's enabled and you want to disable it, set it to:

plugins_enabled:
  MemberOf Plugin: false

If you want to enable more plugins:

plugins_enabled:
  MemberOf Plugin: true
  Distributed Numeric Assignment Plugin: true

If a plugin doesn't appear in the list, it's left in its current status.

A plugin named Foo should have an entry under cn=Foo,cn=plugins,cn=config, you can look at the cn=plugins,cn=config tree to see which plugins are available and their status.

dirsrv_dna_plugin

Default value:

dirsrv_dna_plugin:
  gid_min: 2000
  gid_max: 2999
  uid_min: 2000
  uid_max: 2999

Ansible doesn't merge dicts by default, i.e. if you want to change only uid_max and gid_max you have to define the _min variables too. When you define dna_plugin, it replaces this default dict entirely.

This configuration is only applied if "Distributed Numeric Assignment Plugin" is true in plugins_enabled, and is removed when it is false. If it's not mentioned, nothing is done.

Tags

There are some tags available, so can launch e.g.:

ansible-playbook some-playbook.yml --tags dirsrv_schema

and this will only update custom schema files, without changing anything else. some-playbook.yml should apply this role, obviously.

The tags are:

  • dirsrv_schema: custom schema tasks
  • dirsrv_tls: all TLS configuration tasks, including certificates and enforcing
  • dirsrv_cert: TLS certificate tasks, a subset of dirsrv_tls

All the tags also include a few checks at the beginning of the play and a "flush handlers" at the end, since 389DS may need to be restarted or a schema reload may be required.

dirsrv_cert is particularly useful for automated certificate management with ACME: see the "TLS with Let's Encrypt (or other ACME providers)" example below. If the same tag is added to all the ACME related tasks, it will be possible to run ansible-playbook some-playbook.yml --tags dirsrv_cert periodically and automatically to update certificates.

Dependencies

None.

Usage and Examples

Minimum working example

- name: An example playbook
  hosts: example
  roles:
    - role: lvps.389ds_server
      dirsrv_rootdn_password: secret

Bind with DN cn=Directory Manager and password secret on port 389, the suffix will be dc=example,dc=local, everything else is mostly like a clean 389DS install.

Ansible Vault would be a good idea to avoid exposing the root DN password as plaintext in production.

Configure firewall

Not part of this role, but you may need to open the LDAP port (389) to access the server remotely:

- name: Allow ldap port on firewalld
  firewalld: service=ldap permanent=true state=enabled

The same may be needed for the LDAPS port (636), if you enable TLS and want to use that instead of StartTLS.

Example entries and some customization

- name: An example playbook
  hosts: example
  roles:
    - role: lvps.389ds_server
      dirsrv_suffix: dc=custom,dc=example,dc=com
      dirsrv_rootdn: cn=admin
      dirsrv_rootdn_password: secret
      dirsrv_serverid: customized
      dirsrv_install_examples: true
      dirsrv_logging:
        audit:
          enabled: true
          logrotationtimeunit: day
          logmaxdiskspace: 400
          maxlogsize: 200
          maxlogsperdir: 14
          mode: 600
        access:
          enabled: true
          logrotationtimeunit: day
          logmaxdiskspace: 400
          maxlogsize: 200
          maxlogsperdir: 14
          mode: 600
        error:
          enabled: true
          logrotationtimeunit: day
          logmaxdiskspace: 400
          maxlogsize: 200
          maxlogsperdir: 14
          mode: 600
      plugins_enabled:
        MemberOf Plugin: true
      custom_schema:
        - "50example.ldif"
        - "60foobar.ldif"

Bind with DN cn=admin and password secret on port 389, look at the example entries provided by 389DS.

Audit logs are also enabled, and all logs are kept for 14 days (or until they become too large).

MemberOf Plugin is also enabled.

Look into the molecule directory for a custom schema file that is known to work with 389DS, if you want to test that part but you don't have a valid schema file. Delete that part to remove all custom schema files. Schema reload is done automatically.

TLS

- name: An example playbook
  hosts: example
  roles:
    - role: lvps.389ds_server
      dirsrv_suffix: "dc=example,dc=local"
      dirsrv_serverid: example
      dirsrv_rootdn_password: secret
      dirsrv_tls_enabled: true
      dirsrv_tls_cert_file: example_cert.pem
      dirsrv_tls_key_file: example.key
      dirsrv_tls_files_remote: false # True if the files are already on remote host (e.g. provided by certbot)
      dirsrv_tls_certificate_trusted: true # Or false if self-signed
      # If you want to avoid plain LDAP and enforce TLS, also consider these settings:
      dirsrv_tls_enforced: true
      dirsrv_tls_minssf: 256
      # Nothing to do with TLS, but for improved security you may consider:
      dirsrv_password_storage_scheme: "PBKDF2_SHA256"
      # even though the default password storage scheme is already strong enough.

Here you can find a script to generate self-signed certificates that have been repeatedly tested with 389DS. Or look into the molecule directory for an example certificate and key that is used for role testing.

389DS is restarted automatically when needed to apply configuration.

Both LDAPS (port 636) and StartTLS (port 389) are enabled.

If you get tired of having a secure connection, set dirsrv_tls_enabled: false but the certificate will stay in 389DS NSS database. It can be removed manually.

Certificate rollover (replacing certificate and key with a new one, e.g. because old ones are expired) has been tested a few times and seems to work with self signed and Let's Encrypt certificates, but the process is still very complicated and full of hacks and workarounds. If you want to use this in production, it is advisable that you read the relevant parts of section 9.3 of the Administration Guide and the comments in tasks/configure_tls.yml to understand what's happening and why.

TLS with Let's Encrypt (or other ACME providers)

The key point is that you need to feed the "fullchain" (server certificate and all intermediate ones, no root certificate) into the 389ds-server role. Since I couldn't find many other examples on the http-01 challenge with acme_certificate I've added it here to give you a better idea of all the necessary steps.

- name: An example playbook
  hosts: example
  pre_tasks:
    - name: Ensure ACME account exists
      acme_account:
        # acme_directory: "http://..."  # Your provider. Leave this off to use the Let's Encrypt staging directory
        account_key_content: "{{ acme_account_key }}"  # "openssl genrsa 2048" to generate it, but read https://docs.ansible.com/ansible/latest/modules/acme_account_module.html for more up to date information
        acme_version: 2
        state: present
        terms_agreed: true
        contact:
          - mailto:example@example.com

    # You need a CSR (certificate signing request). And a private key.
    # Do *not* reuse the account key, make a new one!
    # Generate them:
    #
    # openssl genrsa 2048 -out example.key
    # openssl req -new -key example.key -out example.csr -subj "/C=/ST=/L=/O=/OU=/CN=your.domain.example.com"
    #
    # Only the domain is important. Both example.key and your account key should be kept secret,
    # you could place them into Ansible Vault and use a template to create example.key from the variable.
    - name: Copy CSR and private key
      copy:
        src: "{{ item }}"
        dest: "/etc/some/secret/directory"
        owner: root
        group: root
        mode: "400"  # The csr could be world-readable, actually, it's not secret
        setype: cert_t
      loop:
        - "path/to/your/example.csr"
        - "path/to/your/example.key"

    - name: Create challenge
      acme_certificate:
        acme_directory: "http://..."
        account_key_content: "{{ acme_account_key }}"
        acme_version: 2
        challenge: "http-01"
        # You'll need the full chain (which contains your certificate and all
        # intermediate ones, but no root certificate). This will be fed into
        # NSS/389DS, which should serve all of them.
        fullchain: "/etc/some/secret/directory/example.fullchain.pem"
        csr: "/etc/some/secret/directory/example.csr"
        # remaining_days: 10
      register: acme_challenge

    # You need an HTTP server running. Imagine there is a NGINX instance that
    # serves pages on example.com from /var/www/html/example.com
    # If you find that an always running HTTP server is annoying, "when: acme_challenge is changed"
    # can be used to start it for the challenge and stop it at the end...
    #
    # You will also need a few directories, or the next task fails because they
    # don't exist...
    - name: Create HTTP directories for ACME http-01 challenge
      file:
        name: "{{ item }}"
        state: directory
        owner: root
        group: root
        # These should not be secret (they're accessible from the Internet),
        # just don't make them writeable by anyone
        mode: "755"
        setype: httpd_sys_content_t  # read-only
      loop:
        - "/var/www/html/example.com"
        - "/var/www/html/example.com/.well-known"
        - "/var/www/html/example.com/.well-known/acme-challenge"

    - name: Fulfill the http-01 challenge
      copy:
        dest: "/var/www/html/example.com/{{ acme_challenge['challenge_data']['example.com']['http-01']['resource'] }}"
        content: "{{ acme_challenge['challenge_data']['example.com']['http-01']['resource_value'] }}"
      when: acme_challenge is changed

    # Same as the previous acme_certificate task, just add "data"
    - name: Do challenge
      acme_certificate:
        acme_directory: "http://..."
        account_key_content: "{{ acme_account_key }}"
        acme_version: 2
        challenge: "http-01"
        fullchain: "/etc/some/secret/directory/example.fullchain.pem"
        csr: "/etc/some/secret/directory/example.csr"
        data: "{{ acme_challenge }}"
      when: acme_challenge is changed

    # Not optimal (for a few moments before this happens the certificate has the
    # wrong permissions)
    # It may be possible to set this task to "state: touch" and place it before
    # the previous one, though.
    - name: Ensure permissions for example certificate
      file:
        state: file
        path: "/etc/some/secret/directory/example.fullchain.pem"
        owner: root
        group: root
        mode: "400"
        setype: cert_t

  # In this example I have used almost no variables for greater clarity
  # (i.e. you see what these strings should look like, instead of an arbitrary
  # name that I invented), but in a real playbook it may be better to use
  # some variables.

  roles:
    - role: lvps.389ds_server
      dirsrv_suffix: "dc=example,dc=local"
      dirsrv_serverid: example
      dirsrv_rootdn_password: secret
      dirsrv_tls_enabled: true
      dirsrv_tls_cert_file: /etc/some/secret/directory/example.fullchain.pem
      dirsrv_tls_key_file: /etc/some/secret/directory/example.key
      dirsrv_tls_files_remote: true  # Both files are on the server
      dirsrv_tls_certificate_trusted: true  # No need to disable certificate checks, yay!

Since certificate rollovers are supported by this role, you just need to run this playbook periodically to update the certificate when it is about to expire.

What about replication?

There's another role for that.

Tests

Tests make use of the docker systemctl replacement script created and distributed by gdraheim under the EUPL license. This script gets downloaded and copied to a local container to allow for the tests to execute correctly. Such distribution happens under the same license and terms upon which gdraheim created and published their work. The script is downloaded as-is and no alteration to it is made whatsoever. By running the tests on their machines the end user agrees to handle the downloaded script under the same terms of the EUPL as intended by its author. Note that the tests themselves (and the role overall) are still licensed under the Apache 2 license.

This role uses molecule for its tests. Install it with pipenv (pip probably works, too) and test all the scenarios:

pipenv install
pipenv shell
molecule test --all

Or to test a single scenario: molecule test -s tls

Future extensions

Could be done, but not planned for the short term

  • Support for Debian/Ubuntu/FreeBSD or any other platform that 389DS supports
  • Support for other plugins that need more than enabled/disabled
  • Support for other DNA attributes

License

Apache 2.0 for the role and and associated tests
EUPL v 1.2 for the "docker systemctl replacement" script by gdraheim (not included but downloaded when running tests)

Author Information

Maintainer: Ludovico Pavesi
Contributor/original author: Colby Prior
Contributor/original author: Artemii Kropachev
Thanks to Firstyear for the comments