Setting up LDAP Authentication with NixOS

2020-07-25

category: tech

tags: NixOS Nix

At work, we manage an OpenLDAP server that handles authentication and authorization for various services that we provide. All machines run Ubuntu with individual service being containerized and run in LXD. I was interested in testing some open source projects out to integrate into our infrastructure.

However, finding out the convoluted installation instructions for software on Ubuntu and turning it into a provisioning script is always a bit of a pain, so I decided to use NixOS this time to quickly try it out. The requirements were fairly involved, though.

I’ll go through the steps I took to meet each requirement, one by one.

Setting up a NixOS LXD container

(If you’re not interested in LXD, you can just skip to LDAP Authentication)

Thanks to nix-community/nixos-generators, this is pretty straightforward. First, we want to start off with a configuration that’s similar to that found in Ubuntu LXD images:

# configuration.nix
{ config, pkgs, ... }:
{
  # https://github.com/NixOS/nixpkgs/issues/9735#issuecomment-500164017
  systemd.services."console-getty".enable = false;
  systemd.services."getty@".enable = false;

  imports = [
    <nixpkgs/nixos/modules/virtualisation/lxc-container.nix>
  ];
  networking.hostName = "nixos";

  services.openssh = {
    enable = true;
    permitRootLogin = "no";
  };

  users.users.nixos = {
    isNormalUser = true;
    extraGroups = [ "wheel" ];
  };

  security.sudo.wheelNeedsPassword = false;

  environment.systemPackages = with pkgs; [
    vim
    htop
    tmux
    wget
  ];

  networking.useDHCP = true;
}

With this, we can create an LXD image with the following command:

nix-shell -p nixos-generators --run \
  'lxc image import --alias nixos \
    $(nixos-generate --format lxc-metadata --configuration ./configuration.nix) \
    $(nixos-generate --format lxc --configuration ./configuration.nix)'

Now that we’ve created an image under the alias nixos, we can launch a NixOS container with lxc launch nixos nixos-test -c security.nesting=true.

Once an alias is defined like this:

lxc alias add nixos 'exec @ARGS@ --mode interactive -- /run/current-system/sw/bin/login -p -f nixos'

Logging into a running NixOS container is as simple as lxc nixos nixos-test. We can now fill in /etc/nixos/configuration.nix and sudo nixos-rebuild switch as needed.

LDAP Authentication

Setting up LDAP authentication was… not as straightforward as I’d hoped.

First Attempt

Let’s forget about group authorization for a moment, and try to get LDAP-based ssh logins working.

{ lib, config, pkgs, ... }:
{
  users.ldap = {
    enable = true;
    base = "dc=example,dc=com";
    server = "ldap://example.com/";
    useTLS = true;
    extraConfig = ''
      ldap_version 3
      pam_password md5
    '';
  };

  security.pam.services.sshd.makeHomeDir = true;
}

Note that the LDAP server we are using is configured to allow anonymous binding and authentication, which may not fit your use-case.

Unfortunately, this doesn’t work; attempting to login as a LDAP user gives errors like the following:

Jul 24 10:51:23 nixos sshd[11992]: Postponed keyboard-interactive for invalid user nixos-user from a.b.c.d port 59828 ssh2 [preauth]
Jul 24 10:51:25 nixos sshd[11994]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=a.b.c.d  user=nixos-user
Jul 24 10:51:25 nixos sshd[11994]: pam_ldap: error trying to bind as user "uid=nixos-user,ou=Users,dc=example,dc=com" (Invalid credentials)
Jul 24 10:51:26 nixos sshd[11992]: error: PAM: Authentication failure for illegal user nixos-user from a.b.c.d

This is frustratingly misleading since the real culprit is this, slightly preceding, message from sshd:

Jul 24 10:51:23 nixos sshd[11992]: User nixos-user not allowed because shell /bin/bash does not exist

This occurs because in our LDAP server, we had set users’ LDAP loginShell attribute to /bin/bash. In hindsight, I don’t think this was such a bad a thing to do, considering that I’ve never dealt with a single system in which /bin/bash doesn’t exist (with the exception of NixOS, of course 😄 ).

Second Attempt

The standard approach to solving this problem is configuring the LDAP client to override the relevant attribute:

...
{
  users.ldap = {
    ...
    extraConfig = ''
      ...

      nss_override_attribute_value loginShell /run/current-system/sw/bin/bash
    '';
  };
  ...
}

Unfortunately, this doesn’t seem to solve the issue, and sshd continues to complain about /bin/bash missing.2

This seems to leave us with changing the loginShell attributes to /bin/sh, since that’s the only way to accommodate NixOS systems while maintaining compatibility with the non-NixOS systems also using LDAP. But /bin/sh isn’t a nice shell to work in, so do we really want to force this on everyone across all machines? Well, there is one easy trick to make this all go away; just symlink /bin/bash to /run/current-system/sw/bin/bash :gasp:.

{ lib, config, pkgs, ... }:
{
  users.ldap = {
    enable = true;
    base = "dc=example,dc=com";
    server = "ldap://example.com/";
    useTLS = true;
    extraConfig = ''
      ldap_version 3
      pam_password md5

      # TOFIX: this does not work for some reason
      # # https://serverfault.com/a/137996
      # nss_override_attribute_value loginShell /run/current-system/sw/bin/bash
    '';
  };

  # evil, horrifying hack for dysfunctional nss_override_attribute_value
  systemd.tmpfiles.rules = [
    "L /bin/bash - - - - /run/current-system/sw/bin/bash"
  ];
}

Group-based Authorization and Home Directory Creation

LDAP group-based authorization was also not as straightforward as I’d hoped. Adding pam_groupdn cn=admin,ou=Groups,dc=example,dc=com to users.ldap.extraConfig only seems to work when users solely belong to the admin group, which was not the case for us. Instead, using pam_listfile.so got us what we wanted:

...
{
  ...
  security.pam.services.sshd = {
    makeHomeDir = true;

    # see https://stackoverflow.com/a/47041843 for why this is required
    text = lib.mkDefault (
      lib.mkBefore ''
        auth required pam_listfile.so \
          item=group sense=allow onerr=fail file=/etc/allowed_groups
      ''
    );
  };

  environment.etc.allowed_groups = {
    text = "admins";
    mode = "0444";
  };
  ...
}

LDAP Authentication for Nginx

This is actually just a variation on the example for PAM authentication for Nginx on the NixOS Wiki used along with pam_listfile.so:

...
{
  ...
  services.nginx = {
    enable = true;
    package = (pkgs.nginx.override { modules = [ pkgs.nginxModules.pam ]; });
    virtualHosts."www.example.com" = {
      enableACME = true;
      forceSSL = true;
      ...
      extraConfig = ''
        auth_pam "LDAP Authentication Required";
        auth_pam_service_name "nginx";
      '';
    };
  };
  security.pam.services.nginx.text = ''
    auth    required     pam_listfile.so \
                         item=group sense=allow onerr=fail file=/etc/allowed_groups
    auth    required     ${pkgs.pam_ldap}/lib/security/pam_ldap.so
    account required     ${pkgs.pam_ldap}/lib/security/pam_ldap.so
  '';
  ...
}

With this configuration, accesses to www.example.com will be authenticated with HTTP Basic Authentication backed by the same LDAP group-based policy.

Wrapping Up

After working through a fairly involved example of deploying NixOS into conventional infrastructure, I’m left with the impression that there’s a lot more we can do to make the experience working with mundane things like LDAP much better. I would love to see NixOS becoming the sysadmin’s favorite distribution, as it definitely has the potential to become so.

Thanks to __pandaman64__ for comments and suggestions.


  1. We either expose relevant ports via LXD proxy devices, or directly expose the containers to the network using macvlan. For the purposes of this post, you can follow along with the assumption that the HTTP and HTTPS ports of the containers are accessible from the Internet at www.example.com.↩︎

  2. If you know how to fix this, please let me know!↩︎

index