Using Active Directory for login on NixOS

In my homelab I have Active Directory running to also experiment with some of the Windows side of systems administration. I also run a number of services on NixOS. I centrally manage my NixOS systems from a Git repository.

As a learning exercise to rollout a configuration across all my NixOS machines using a NixOS module I wanted to setup Active Directory based authentication. In the past I have already done something similar at my previous company using SSSD and krb5 on mainly Ubuntu connecting to FreeIPA.

To start I used the Active Directory Client example on the official NixOS wiki. As the example in the wiki might change over time I will copy it here to keep it inline with the rest of the blogpost:

{
  config,
  pkgs,
  ...
}: {
  #
  # Packages
  #
  environment.systemPackages = with pkgs; [
    adcli # Helper library and tools for Active Directory client operations
    oddjob # Odd Job Daemon
    samba4Full # Standard Windows interoperability suite of programs for Linux and Unix
    sssd # System Security Services Daemon
    krb5 # MIT Kerberos 5
    realmd # DBus service for configuring Kerberos and other
  ];

  #
  # Security
  #
  security = {
    krb5 = {
      enable = true;
      settings = {
        libdefaults = {
          udp_preference_limit = 0;
          default_realm = "YOUR_DOMAIN_UPPERCASE";
        };
      };
    };

    pam = {
      makeHomeDir.umask = "077";
      services.login.makeHomeDir = true;
      services.sshd.makeHomeDir = true;
    };

    sudo = {
      extraConfig = ''
        %domain\ admins ALL=(ALL:ALL) NOPASSWD: ALL
        Defaults:%domain\ admins env_keep+=TERMINFO_DIRS
        Defaults:%domain\ admins env_keep+=TERMINFO
      '';

      # Use extraConfig because of blank space in 'domain admins'.
      # Alternatively, you can use the GID.
      # extraRules = [
      #   { groups = [ "domain admins" ];
      #     commands = [ { command = "ALL"; options = [ "NOPASSWD" ]; }  ]; }
      # ];
    };
  };

  #
  # Services
  #
  services = {
    nscd = {
      enable = true;
      config = ''
        server-user nscd
        enable-cache hosts yes
        positive-time-to-live hosts 0
        negative-time-to-live hosts 0
        shared hosts yes
        enable-cache passwd no
        enable-cache group no
        enable-cache netgroup no
        enable-cache services no
      '';
    };

    sssd = {
      enable = true;
      config = ''
        [sssd]
        domains = your_domain_lowercase
        config_file_version = 2
        services = nss, pam

        [domain/your_domain_lowercase]
        override_shell = /run/current-system/sw/bin/zsh
        krb5_store_password_if_offline = True
        cache_credentials = True
        krb5_realm = YOUR_DOMAIN_UPPERCASE
        realmd_tags = manages-system joined-with-samba
        id_provider = ad
        fallback_homedir = /home/%u
        ad_domain = your_domain_lowercase
        use_fully_qualified_names = false
        ldap_id_mapping = false
        auth_provider = ad
        access_provider = ad
        chpass_provider = ad
        ad_gpo_access_control = permissive
        enumerate = true
      '';
    };
  };

  #
  # Systemd
  #
  systemd = {
    services.realmd = {
      description = "Realm Discovery Service";
      wantedBy = ["multi-user.target"];
      after = ["network.target"];
      serviceConfig = {
        Type = "dbus";
        BusName = "org.freedesktop.realmd";
        ExecStart = "${pkgs.realmd}/libexec/realmd";
        User = "root";
      };
    };
  };
}

This uses a combination of tools to achieve Active Directory integration, some if which I have already used in the past (SSSD and krb5), some I am however not familiar with namely realmd and adcli.

Since the a few weeks a new NixOS module has been merged for realmd, with this it is no longer needed to manually configure the realmd systems service. This can be simply be replaced by services.realmd.enable = true;.

Make sure to install polkit as this is required by realmd and will give Not authorized to perform this action , looking at the systemd journal reveals the following error couldn't check polkit authorization: GDBus.Error:org.freedesktop.DBus.Error.ServiceUnknown: The name org.freedesktop.PolicyKit1 was not provided by any .service files. This can be resolved by enabling polkit by adding security.polkit.enable = true; to the NixOS configuration.

Now we have setup the required configuration the next step is to join the NixOS machine into Active Directory. Make sure DNS is functioning properly so the system can discover the Kerberos and LDAP records of Active Directory.

Registering in Active Directory

With the adcli and realmd helper tools we can easily register a computer into Active Directory, this will also create a Kerberos keytab file (in /etc/krb5.keytab). To get started with this we run the following command:

sudo adcli join --domain=your.domain.com --user=administrator

Now restart SSSD with sudo systemctl restart sssd.

With this we can now check if Kerberos is functioning by requesting a Kerberos ticket for our user, after running the command enter your password from Active Directory:

kinit <user>

This should now give you a Kerberos ticket:

klist
Ticket cache: FILE:/tmp/krb5cc_0
Default principal: marlin@mydomain.example.com

Valid starting       Expires              Service principal
01/25/2025 16:12:22  01/26/2025 02:12:22  krbtgt/MYDOMAIN.EXAMPLE.COM@MYDOMAIN.EXAMPLE.COM
        renew until 01/26/2025 16:12:20

UID and GID mapping

By default SSSD will try to generate UIDs and GIDs for all users in Active Directory based on the SID. This behaviour can be toggled with the ldap_id_mapping setting, as I like having control over these values (I use LXC containers and don’t want to deal with UID mapping issues to the host) and because some systems might not have SSSD I left the ldap_id_mapping option disabled like the NixOS wiki example.

This however does require the uidNumber and gidNumber attributes to be defined under each Active Directory user that should be able to login. These can be set by opening Active Directory Users and Computers, enabling the Advanced Features under View and then opening a user. Now you should see a Attribute Editor tab where you can set these attributes.

Now to try and lookup a user in Active Directory use the following command:

id <username>@<domain name>

This should return the uidNumber and gidNumber you have defined in Active Directory. If you get a no such user error then try enabling debugging with sssctl debug-level 6 and then checking the logs in the systemd journal. As SSSD is a caching daemon it might be needed to clear the cache, you can do this with sssctl cache-remove.

Shell selection

The default example in the NixOS wiki overrides the shell that can be defined in the loginShell user attribute to use zsh. Using standard shells like /usr/bin/bash in the loginShell attribute probably won’t work on NixOS as these would be stored in the /nix/store instead, this is why I expect this has been overriden with override_shell = /run/current-system/sw/bin/zsh, I have instead chosen to remove this option so the system falls back to the default shell.

SSH authentication

Now that our user is recognized we should be able to login to the system via SSH.

As the users have their domain as suffix we can login with the following command:

ssh -l <username>@<domain> <hostname of server as in AD>

I would also like to support Kerberos based GSSAPI authentication however I haven’t been able to get this to work.

Kerberos authentication via SSH

To enable Kerberos based GSSAPI authentication via SSH we have to add GSSAPIAuthentication yes. It can also be useful to delegate the Kerberos ticket from the client to the machine by enabling GSSAPIDelegateCredentials as this would allow authentication to further services without requesting a new ticket.

We have to also use the openssh_gssapi package for SSH in NixOS as the default package doesn’t have GSSAPI and Kerberos support compiled in. To configure the SSH service with Kerberos support we have to add the following to the NixOS configuration:

{
  config,
  pkgs,
  ...
}: {
  programs.ssh.package = pkgs.openssh_gssapi;
  services.openssh = {
    enable = true;
    extraConfig = ''
      KerberosAuthentication yes
      GSSAPIAuthentication yes
    '';
  };
}

Now you can try to SSH into the server with Kerberos with (note the -K, this enables GSSAPI on the client), make sure to use the hostname as defined in AD so a Kerberos ticket will be used:

ssh -l <username>@<domain> <hostname of server as in AD> -K -vv

If you see the following in the log then the client should at least be doing the right thing:

debug3: remaining preferred: publickey,keyboard-interactive,password               
debug3: authmethod_is_enabled gssapi-with-mic                                      
debug1: Next authentication method: gssapi-with-mic

If it is not working it can be useful to set services.openssh.options.LogLevel = "DEBUG2";, you might for example see the following in the log which means the OpenSSH server is using the wrong entry from /etc/hosts:

debug1: No credentials were supplied, or the credentials were unavailable or inaccessible\nNo key table entry found matching host/localhost@\n\n

This one I wasn’t really sure how to fix as NixOS now always writes localhost as first hostname in /etc/hostname which as far as I understand is exactly what SSH does not want so I had to overwrite the networking.hostFiles property to write 127.0.0.1 localhost last.

{
  config,
  pkgs,
  ...
}: {
  networking = {
    hostName = "<hostname>";
    domain = "<domain>";
    hosts = {
      "127.0.1.1" = lib.mkForce ["<hostname>.<domain>" "llm"];
      "::1" = lib.mkForce ["<hostname>.<domain>" "llm" "localhost"];
    };
  };
}

>> Home