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"];
};
};
}