Last day I spin up an OpenLDAP server on one of my servers and as usual, to find an updated guide for OpenLDAP is really hard. OpenLDAP has two methods to be configured:

  1. slapd.conf which is deprecated;
  2. olc or cn=config which should be the de facto method.

I won't go too much in details but when you use slapd.conf you have to restart your slapd daemon each time you change the config. While it's ok for a small server, it can be slow with a big directory. One-Line Configuration or olc is a way to store OpenLDAP configuration in the DIT (Directory Information Tree) under cn=config. If you do research be sure to find answers for the olc and not slapd.con (legacy is still highly present). If you want to go further:

NixOS version issue

To the day I'm writing this article NixOS current stable release is 20.09. Between 20.09 and unstable the OpenLDAP module change and I want to use the new module (mostly because it eases a lot of configuration). While it is more or less easy to use the unstable channel to use or update a software (e.g. firefox), I found no documentation or nothing about how to use the unstable channel for a service.

Actually the trick is "basic" but mostly clever:

{ config, pkgs, ... }:
let
  nixpkgs-unstable = fetchTarball
    "https://github.com/nixos/nixpkgs/archive/f217c0ea7c148ddc0103347051555c7c252dcafb.tar.gz";
in {
  imports = [
    (nixpkgs-unstable + "/nixos/modules/services/databases/openldap.nix")
    ...
  ];

  # Cf. above, we use openldap service from unstable
  disabledModules = [ "services/databases/openldap.nix" ];
  
  ...
 }
How to import OpenLDAP service from the unstable channel

We simply disable a module (from the current channel) and import the unstable module from the unstable channel.

Spin up the OpenLDAP server

Once the unstable module is used, you mostly have to copy and paste default settings written in the documentation, and it should work straight forward. If you want to open your LDAP server on the Internet, don't forgot to open the according port(s) in the firewall (TCP).

It should look to something like that:

  services.openldap = {
    enable = true;
    settings = {
      attrs.olcLogLevel = [ "stats" ];
      children = {
        "cn=schema" = {
          includes = [
            "${pkgs.openldap}/etc/schema/core.ldif"
            "${pkgs.openldap}/etc/schema/cosine.ldif"
            "${pkgs.openldap}/etc/schema/inetorgperson.ldif"
          ];
        };
        "olcDatabase={-1}frontend" = {
          attrs = {
            objectClass = "olcDatabaseConfig";
            olcDatabase = "{-1}frontend";
            olcAccess = [
              "{0}to * by dn.exact=uidNumber=0+gidNumber=0,cn=peercred,cn=external,cn=auth manage stop by * none stop"
            ];
          };
        };
        "olcDatabase={0}config" = {
          attrs = {
            objectClass = "olcDatabaseConfig";
            olcDatabase = "{0}config";
            olcAccess = [ "{0}to * by * none break" ];
          };
        };
        "olcDatabase={1}mdb" = {
          attrs = {
            objectClass = [ "olcDatabaseConfig" "olcMdbConfig" ];
            olcDatabase = "{1}mdb";
            olcDbDirectory = "/var/db/ldap";
            olcDbIndex = [
              "objectClass eq"
              "cn pres,eq"
              "uid pres,eq"
              "sn pres,eq,subany"
            ];
            olcSuffix = "dc=locahlo,dc=st";
            olcAccess = [ "{0}to * by * read break" ];
          };
        };
      };
    };
  };
Basic OpenLDAP configuration

The config pasted above is pretty much the same as the default one.

Add TLS to encrypt traffic

By default, OpenLDAP traffic is not encrypted which is not really great to be honest. Let's add TLS!

It needs three olc directives in cn=config (which is the configuration top node). While all guides I found online use ldapmodify to modify OpenLDAP configuration, we won't use it because we are on NixOS and want reproducibility. We rather will edit our NixOS configuration to modify our OpenLDAP DIT (I agree it goes against olc will but eh, not a big deal).

It took me a little while to understand where to put those directives. Actually, in openldap.settings, all directives are under cn=config. If we look at the very first child above, cn=schema, when transformed into LDIF during NixOS evaluation it will be translated/mutated into cn=schema,cn=config.

We have to edit our services.openldap.settings.attr to add our directives. Currently, there is only one directive: olcLogLevel. According to OpenLDAP TLS documentation we have to add:

  • olcTLSCACertifacetFile;
  • olcTLSCertificateFile;
  • olcTLSCertificateKeyFile.
  services.openldap = {
    settings = {
      attrs = {
        olcLogLevel = [ "stats" ];
        olcTLSCACertificateFile = "";
        olcTLSCertificateFile = "";
        olcTLSCertificateKeyFile = "";
      };
    ...
Which directives to add to support TLS

TLS powered by Let's Encrypt

It would have been too easy to just stop there and to deliver certificates manually or with a provisioning tool. Our encryption will be powered by Let's Encrypt. I agree the way I went is far from perfect, but it's the easier.

  services.nginx = {
    enable = true;
    recommendedTlsSettings = true;
    virtualHosts."ldap.locahlo.st" = {
      enableACME = true;
      forceSSL = true;
      locations."/.well-known" = {
        extraConfig = ''
          proxy_ssl_server_name on;
        '';
      };
      locations."/" = {
        extraConfig = ''
          proxy_ssl_server_name on;
          deny all;
        '';
      };
    };
  };
Activate Nginx to have Let's Encrypt certificates

It activates the Nginx service (if yet activated), allow the /.well-known directory to be accessed (required for ACME challenge validation) and refuses all traffic on the "landing page" (403 everybody). Now just fill certificates' location to OpenLDAP and job's done:

  services.openldap = {
    settings = {
      attrs = {
        olcLogLevel = [ "stats" ];
        olcTLSCACertificateFile = "/var/lib/acme/ldap.locahlo.st/fullchain.pem";
        olcTLSCertificateFile = "/var/lib/acme/ldap.locahlo.st/cert.pem";
        olcTLSCertificateKeyFile = "/var/lib/acme/ldap.locahlo.st/key.pem";
      };
    ...
Fill TLS directives to use Let's Encrypt certificates

nixos-rebuild test aaaaaaand it fails. Why? Because OpenLDAP process don't have rights to read the certificates (actually it lacks rights to read directory's content (740 acme nginx)).

So Nginx can read content? Clearly not perfect, but things are as they are, we will change the group with which OpenLDAP process runs, services.openldap.group = "nginx";.

nixos-rebuild test and it now works. Don't forget to change on which protocol OpenLDAP listens and to open the according port.

TL;DR

{ config, pkgs, ... }:
let
  subdomain = "ldap";
  domain = "locahlo";
  tld = "st";
  fqdn = subdomain + "." + domain + "." + tld;
  nixpkgs-unstable = fetchTarball
    "https://github.com/nixos/nixpkgs/archive/f217c0ea7c148ddc0103347051555c7c252dcafb.tar.gz";
in {
  imports = [
    (nixpkgs-unstable + "/nixos/modules/services/databases/openldap.nix")
  ];

  # Cf. above, we use openldap service from unstable
  disabledModules = [ "services/databases/openldap.nix" ];

  services.nginx = {
    enable = true;
    recommendedTlsSettings = true;
    virtualHosts."${fqdn}" = {
      enableACME = true;
      forceSSL = true;
      locations."/.well-known" = {
        extraConfig = ''
          proxy_ssl_server_name on;
        '';
      };
      locations."/" = {
        extraConfig = ''
          proxy_ssl_server_name on;
          deny all;
        '';
      };
    };
  };
  networking.firewall.allowedTCPPorts = [ 636 ];
  services.openldap = {
    enable = true;
    urlList = [ "ldaps:///" ];
    group = "nginx"; # FIXME workaround to access to Let's Encrypt certificates
    settings = {
      attrs = {
        olcLogLevel = [ "stats" ];
        olcTLSCACertificateFile =
          "/var/lib/acme/${fqdn}/fullchain.pem";
        olcTLSCertificateFile = "/var/lib/acme/${fqdn}/cert.pem";
        olcTLSCertificateKeyFile = "/var/lib/acme/${fqdn}/key.pem";
      };
      children = {
        "cn=schema" = {
          includes = [
            "${pkgs.openldap}/etc/schema/core.ldif"
            "${pkgs.openldap}/etc/schema/cosine.ldif"
            "${pkgs.openldap}/etc/schema/inetorgperson.ldif"
          ];
        };
        "olcDatabase={-1}frontend" = {
          attrs = {
            objectClass = "olcDatabaseConfig";
            olcDatabase = "{-1}frontend";
            olcAccess = [
              "{0}to * by dn.exact=uidNumber=0+gidNumber=0,cn=peercred,cn=external,cn=auth manage stop by * none stop"
            ];
          };
        };
        "olcDatabase={0}config" = {
          attrs = {
            objectClass = "olcDatabaseConfig";
            olcDatabase = "{0}config";
            olcAccess = [ "{0}to * by * none break" ];
          };
        };
        "olcDatabase={1}mdb" = {
          attrs = {
            objectClass = [ "olcDatabaseConfig" "olcMdbConfig" ];
            olcDatabase = "{1}mdb";
            olcDbDirectory = "/var/db/ldap";
            olcDbIndex = [
              "objectClass eq"
              "cn pres,eq"
              "uid pres,eq"
              "sn pres,eq,subany"
            ];
            olcRootDN = "cn=admin,dc=${domain},dc=${tld}";
            olcSuffix = "dc=${domain},dc=${tld}";
            olcAccess = [ "{0}to * by * read break" ];
          };
        };
      };
    };
  };

  ...

}
Always think twice before copy/pasta. Measure twice, cute once.

Side note

If you wish to extend your Config I highly recommend you to only do it via your nix configuration and not by ldapmodify.

Thanks

I'd like to thank @petabyteboy, they gave me the snippet to use OpenLDAP from unstable.