Windows Server for beginners. Part 29: Joining Linux to the domain

All parts of the series “Windows Server for beginners”

In all the previous parts Linux came up as a DNS client, as a target for ssh and as a machine that, without Kerberos, is left with nonsecure DNS updates only. Time to join it to the domain for real: so that the user alice logs on to lnx1 with her domain password, is a member of her groups there, gets sudo through membership in a directory group, and all of it works over Kerberos without a single local password. Linux has no Netlogon, no SYSVOL and no group policy in the familiar form, so "domain member" means something different here, and that something is worth taking apart component by component before running realm join.

The distribution in the examples is Ubuntu 26.04 LTS; where Ubuntu 24.04 behaves differently, it is said separately. On RHEL and its derivatives the packages and files are the same, with dnf and authselect instead of pam-auth-update.


What membership is made of

On Windows, joining the domain is the job of one service. On Linux it is five components with separate responsibilities:

 ┌──────────────────────────────────────────────────────────────────────────┐
 │ realmd      discovers the domain and joins it: finds controllers through │
 │             DNS, calls adcli, writes sssd.conf and krb5.conf             │
 ├──────────────────────────────────────────────────────────────────────────┤
 │ adcli       creates the computer account in the directory, gets the keys │
 │             and writes them to /etc/krb5.keytab; changes the machine     │
 │             password when sssd asks                                      │
 ├──────────────────────────────────────────────────────────────────────────┤
 │ sssd        the daemon that does everything at run time: finds DCs by    │
 │             site, reads users and groups over LDAP with Kerberos, checks │
 │             passwords over Kerberos, caches, updates DNS, applies logon  │
 │             rights from GPO                                              │
 ├──────────────────────────────────────────────────────────────────────────┤
 │ NSS         libnss_sss: getpwnam("alice") → sssd → directory: domain     │
 │             users and groups look like ordinary ones to the system       │
 ├──────────────────────────────────────────────────────────────────────────┤
 │ PAM         pam_sss: authentication (Kerberos AS-REQ), authorization     │
 │             (may this user log on to this machine), password change;     │
 │             pam_mkhomedir creates the home directory at first logon      │
 └──────────────────────────────────────────────────────────────────────────┘

A keytab is a file with the long-term keys of the machine's principals: LNX1$@CORP.EXAMPLE.COM, host/lnx1.corp.example.com@CORP.EXAMPLE.COM and a few more. It is the same secret as the computer account password on Windows (part 6), only in the form of a file. Everything sssd does on behalf of the machine — LDAP queries, DNS updates — it does with tickets obtained with these keys, and it authenticates as LNX1$@CORP.EXAMPLE.COM: AD issues a TGT only to the account name, while host/… is the name of a service that other people's tickets come to. The alternative to sssd, Samba winbind, a daemon that ships with Samba, does the same and is needed when the machine is also an SMB file server (the Windows file sharing protocol, part 3); for application servers the standard is sssd.


Preparation

Three things without which the join succeeds and Kerberos does not.

The name. The full one, matching the future DNS record:

$ sudo hostnamectl set-hostname lnx1.corp.example.com
$ hostname; hostname -f; hostname -s
lnx1.corp.example.com
lnx1.corp.example.com
lnx1

/etc/hosts. The Ubuntu installer writes a line with the short name into it:

$ grep 127.0.1.1 /etc/hosts
127.0.1.1 lnx1
$ getent hosts lnx1.corp.example.com
192.168.1.60    lnx1.corp.example.com

As long as the machine name is the full one, this line is harmless: hostname -f looks up lnx1.corp.example.com, it is not in /etc/hosts, and the full name comes from DNS. What breaks is a short machine name together with such a line: then hostname -f finds lnx1 in /etc/hosts and returns it without the domain. adcli gets the full name the same way — gethostname(), and if that is short, getaddrinfo() asking for the canonical name — and will register dNSHostName and the SPNs without the domain; sssd uses the same name to find its principal in the keytab and to register the DNS record. If a short machine name is needed after all, the line is written with the full name first: 192.168.1.60 lnx1.corp.example.com lnx1. That is what the Debian documentation advises for a machine with a permanent address and a name in DNS: the permanent address instead of 127.0.1.1, the full name instead of the short one.

DNS and time. The DNS servers are the controllers, through systemd-resolved or /etc/resolv.conf:

$ resolvectl status eth0
Link 2 (eth0)
    Current Scopes: DNS
         Protocols: +DefaultRoute -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
Current DNS Server: 192.168.1.11
       DNS Servers: 192.168.1.10 192.168.1.11
        DNS Domain: corp.example.com
     Default Route: yes

Time comes from the controllers too, because the five minutes of allowed Kerberos skew apply here as well (part 12). Ubuntu 26.04 installs chrony — an NTP client and server (the time synchronization protocol over UDP 123) — and takes time from Canonical's pools through NTS (Network Time Security, RFC 8915: the keys that sign NTP packets are negotiated over TLS):

$ systemctl is-active chrony systemd-timesyncd
active
inactive
$ ls /etc/chrony/sources.d/
README  ubuntu-ntp-pools.sources
$ grep -h '^pool' /etc/chrony/sources.d/*
pool 1.ntp.ubuntu.com iburst maxsources 1 nts prefer
pool 2.ntp.ubuntu.com iburst maxsources 1 nts prefer
pool 3.ntp.ubuntu.com iburst maxsources 1 nts prefer
pool 4.ntp.ubuntu.com iburst maxsources 1 nts prefer
pool ntp-bootstrap.ubuntu.com iburst maxsources 1 nts certset 1

The controllers answer unsigned NTP to anyone (part 12) and do not speak NTS, so the sources are replaced with plain server lines:

$ sudo rm /etc/chrony/sources.d/ubuntu-ntp-pools.sources
$ sudo tee /etc/chrony/sources.d/corp.sources > /dev/null <<'EOF'
server dc1.corp.example.com iburst
server dc2.corp.example.com iburst
EOF
$ sudo chronyc reload sources

On Ubuntu 24.04, systemd-timesyncd with ntp.ubuntu.com runs instead of chrony; the controllers are set for it with an NTP= line in /etc/systemd/timesyncd.conf.

Packages:

$ sudo apt install realmd sssd sssd-tools adcli krb5-user libnss-sss libpam-sss oddjob oddjob-mkhomedir packagekit samba-common-bin

krb5-user brings kinit, klist and kvno and asks for the default realm during installation — CORP.EXAMPLE.COM, in capitals; realmd itself does not require this package, and without it the machine has no klist. samba-common-bin is there for net ads for rare operations; packagekit lets realmd install whatever is missing by itself.


Joining

First, see what realmd finds:

$ realm discover -v corp.example.com
 * Resolving: _ldap._tcp.corp.example.com
 * Performing LDAP DSE lookup on: 192.168.1.10
 * Performing LDAP DSE lookup on: 192.168.1.11
 * Successfully discovered: corp.example.com
corp.example.com
  type: kerberos
  realm-name: CORP.EXAMPLE.COM
  domain-name: corp.example.com
  configured: kerberos-member
  server-software: active-directory
  client-software: sssd
  required-package: sssd-tools
  required-package: sssd
  required-package: libnss-sss
  required-package: libpam-sss
  required-package: adcli
  required-package: samba-common-bin
  login-formats: %U
  login-policy: allow-realm-logins

This is the output from a machine that is already in the domain; before joining, the configured field says no, and there are no login-formats and login-policy lines. Discovery is the SRV record _ldap._tcp.corp.example.com (a DNS record that names the hosts and ports of a service, RFC 2782; the controllers register it themselves, part 3), then reading the RootDSE of each controller found (part 5), and the LDAP ping — a search for the Netlogon attribute in the RootDSE, which the controller answers with its flags and site; realmd does it over TCP. The join:

$ sudo realm join -v -U alice.admin corp.example.com
 * Resolving: _ldap._tcp.corp.example.com
 * Performing LDAP DSE lookup on: 192.168.1.10
 * Performing LDAP DSE lookup on: 192.168.1.11
 * Successfully discovered: corp.example.com
Password for alice.admin:
 * Unconditionally checking packages
 * Resolving required packages
 * LANG=C /usr/sbin/adcli join --verbose --domain corp.example.com --domain-realm CORP.EXAMPLE.COM --domain-controller 192.168.1.10 --login-type user --login-user alice.admin --stdin-password
 * Using domain name: corp.example.com
 * Calculated computer account name from fqdn: LNX1
 * Using domain realm: corp.example.com
 * Sending NetLogon ping to domain controller: 192.168.1.10
 * Received NetLogon info from: dc1.corp.example.com
 * Wrote out krb5.conf snippet to /var/cache/realmd/adcli-krb5-bZXCpu/krb5.d/adcli-krb5-conf-C1KYf8
 * Authenticated as user: alice.admin@CORP.EXAMPLE.COM
 * Using GSS-SPNEGO for SASL bind
 * Looked up short domain name: CORP
 * Looked up domain SID: S-1-5-21-3623811015-3361044348-30300820
 * Received NetLogon info from: dc1.corp.example.com
 * Using fully qualified name: lnx1.corp.example.com
 * Using domain name: corp.example.com
 * Using computer account name: LNX1
 * Using domain realm: corp.example.com
 * Calculated computer account name from fqdn: LNX1
 * Generated 120 character computer password
 * Using keytab: FILE:/etc/krb5.keytab
 * A computer account for LNX1$ does not exist
 * Found well known computer container at: CN=Computers,DC=corp,DC=example,DC=com
 * Calculated computer account: CN=LNX1,CN=Computers,DC=corp,DC=example,DC=com
 * Encryption type [16] not permitted.
 * Encryption type [23] not permitted.
 * Encryption type [3] not permitted.
 * Encryption type [1] not permitted.
 * Created computer account: CN=LNX1,CN=Computers,DC=corp,DC=example,DC=com
 * Trying to set computer password with Kerberos
 * Set computer password
 * Retrieved kvno '2' for computer account in directory: CN=LNX1,CN=Computers,DC=corp,DC=example,DC=com
 * Checking RestrictedKrbHost/lnx1.corp.example.com
 *    Added RestrictedKrbHost/lnx1.corp.example.com
 * Checking RestrictedKrbHost/LNX1
 *    Added RestrictedKrbHost/LNX1
 * Checking host/lnx1.corp.example.com
 *    Added host/lnx1.corp.example.com
 * Checking host/LNX1
 *    Added host/LNX1
 ! Couldn't authenticate with keytab while discovering which salt to use: LNX1$@CORP.EXAMPLE.COM: Client 'LNX1$@CORP.EXAMPLE.COM' not found in Kerberos database
 * Added the entries to the keytab: LNX1$@CORP.EXAMPLE.COM: FILE:/etc/krb5.keytab
 * Added the entries to the keytab: host/LNX1@CORP.EXAMPLE.COM: FILE:/etc/krb5.keytab
 * Added the entries to the keytab: host/lnx1.corp.example.com@CORP.EXAMPLE.COM: FILE:/etc/krb5.keytab
 * Added the entries to the keytab: RestrictedKrbHost/LNX1@CORP.EXAMPLE.COM: FILE:/etc/krb5.keytab
 * Added the entries to the keytab: RestrictedKrbHost/lnx1.corp.example.com@CORP.EXAMPLE.COM: FILE:/etc/krb5.keytab
 * /usr/sbin/update-rc.d sssd enable
 * /usr/sbin/service sssd restart
 * Successfully enrolled machine in realm

The verbose output (-v) shows what happened:

  • adcli got a TGT for alice.admin and bound to the controller's LDAP through SASL (RFC 4422 — a way to plug a replaceable authentication mechanism into a protocol) with the GSS-SPNEGO mechanism: SPNEGO (RFC 4178) negotiates a GSS-API mechanism, here Kerberos, so the alice.admin password does not travel over LDAP;
  • it created the LNX1$ account in CN=Computers and set a random 120-character password on it with the Kerberos password change protocol (RFC 3244, port 464): the new password travels to the KDC encrypted with the ticket's session key;
  • the Encryption type [n] not permitted lines are encryption types the Kerberos library does not allow: DES (1, 3), 3DES (16) and RC4 (23); in the MIT Kerberos build of Ubuntu 26.04, RC4 is removed from the default list, so the keys are AES only;
  • it registered the host/ and RestrictedKrbHost/ SPNs for the full and the short name and wrote the keys to the keytab;
  • the ! warning about the salt: adcli wanted to check which salt (a string mixed into the password when the key is computed) the KDC uses for the keys, but the request went to a controller that had not yet received the new account through replication; the keys were written without the check, and the machine works fine.

With --computer-ou='OU=Linux,OU=Servers,DC=corp,DC=example,DC=com' the account is created directly in the right OU, and --os-name and --os-version fill in the operating system attributes — without them adcli writes a value set at build time into operatingSystem and leaves the version empty. From the directory side it is an ordinary computer:

PS C:\> Get-ADComputer lnx1 -Properties OperatingSystem, OperatingSystemVersion, dNSHostName, servicePrincipalName, msDS-SupportedEncryptionTypes, msDS-KeyVersionNumber, PasswordLastSet, userAccountControl | Format-List Name, OperatingSystem, OperatingSystemVersion, dNSHostName, servicePrincipalName, msDS-SupportedEncryptionTypes, msDS-KeyVersionNumber, userAccountControl, PasswordLastSet

Name                          : LNX1
OperatingSystem               : pc-linux-gnu
OperatingSystemVersion        :
dNSHostName                   : lnx1.corp.example.com
servicePrincipalName          : {RestrictedKrbHost/lnx1.corp.example.com, RestrictedKrbHost/LNX1, host/lnx1.corp.example.com, host/LNX1}
msDS-SupportedEncryptionTypes : 24
msDS-KeyVersionNumber         : 2
userAccountControl            : 69632
PasswordLastSet               : 19.10.2026 17:38:49

msDS-SupportedEncryptionTypes = 24 is AES128 and AES256 (bits 0x8 and 0x10), that is, only what the library allowed. userAccountControl = 69632 is 0x11000: a workstation account (0x1000) and "password never expires" (0x10000), as with Windows clients; the machine changes the password itself. The keytab:

$ sudo klist -k -e
Keytab name: FILE:/etc/krb5.keytab
KVNO Principal
---- --------------------------------------------------------------------------
   2 LNX1$@CORP.EXAMPLE.COM (aes128-cts-hmac-sha1-96)
   2 LNX1$@CORP.EXAMPLE.COM (aes256-cts-hmac-sha1-96)
   2 host/LNX1@CORP.EXAMPLE.COM (aes128-cts-hmac-sha1-96)
   2 host/LNX1@CORP.EXAMPLE.COM (aes256-cts-hmac-sha1-96)
   2 host/lnx1.corp.example.com@CORP.EXAMPLE.COM (aes128-cts-hmac-sha1-96)
   2 host/lnx1.corp.example.com@CORP.EXAMPLE.COM (aes256-cts-hmac-sha1-96)
   2 RestrictedKrbHost/LNX1@CORP.EXAMPLE.COM (aes128-cts-hmac-sha1-96)
   2 RestrictedKrbHost/LNX1@CORP.EXAMPLE.COM (aes256-cts-hmac-sha1-96)
   2 RestrictedKrbHost/lnx1.corp.example.com@CORP.EXAMPLE.COM (aes128-cts-hmac-sha1-96)
   2 RestrictedKrbHost/lnx1.corp.example.com@CORP.EXAMPLE.COM (aes256-cts-hmac-sha1-96)

KVNO 2 is the same key version number as in msDS-KeyVersionNumber; when the machine password changes, it grows both in the directory and here. The keys in the keytab are the same for all the principals — they are the keys of one account under different names.

On Ubuntu 24.04 the picture is different: MIT Kerberos 1.20 there allows RC4, adcli writes three keys for each principal — arcfour-hmac as well — and sets msDS-SupportedEncryptionTypes to 28 (0x4 + 0x8 + 0x10). After a month of work the keytab of such a machine holds two generations of keys, for example KVNO 6 and 7: when changing the password, adcli keeps the previous key, because the new one is not yet on all controllers and tickets issued with the old one must still decrypt. How to remove RC4 from such a machine is in the postscript.


krb5.conf

The Kerberos library needs to know the default realm and where to look for the KDC. On Ubuntu 24.04 without krb5-user, realmd writes the whole /etc/krb5.conf:

$ cat /etc/krb5.conf

[libdefaults]
udp_preference_limit = 0
default_realm = CORP.EXAMPLE.COM

udp_preference_limit = 0 makes the library start with TCP: AD tickets with a PAC and groups often do not fit into a UDP datagram (part 6). On Ubuntu 26.04, krb5-config is installed together with krb5-user and creates a large sample file with other people's realms as examples; realmd adds the same two lines to it, and the first line of the file is includedir /etc/krb5.conf.d/. Through it comes what sssd writes:

$ grep -v '^#' /etc/krb5.conf.d/enable_sssd_conf_dir
includedir /var/lib/sss/pubconf/krb5.include.d/
$ ls /var/lib/sss/pubconf/krb5.include.d/
domain_realm_corp_example_com  krb5_libdefaults  localauth_plugin
$ cat /var/lib/sss/pubconf/krb5.include.d/domain_realm_corp_example_com
[domain_realm]
.corp.example.com = CORP.EXAMPLE.COM
corp.example.com = CORP.EXAMPLE.COM

[domain_realm] maps host names to the realm; localauth_plugin loads the sssd module that turns a principal into a local user name. Without it the default rule applies: a single-component principal in the default realm becomes the same name, alice@CORP.EXAMPLE.COM becomes alice. That is enough while users have short names; fully qualified names and users of other domains need the sssd module. The short krb5.conf that realmd writes on 24.04 has no includedir line, and these snippets are not read there.

None of the files lists the KDCs: sssd puts the addresses of the controllers it is working with into /var/lib/sss/pubconf/kdcinfo.CORP.EXAMPLE.COM, and the sssd_krb5_locator_plugin module, which libkrb5 loads on its own, hands them to kinit and all other programs.


sssd.conf

realmd wrote /etc/sssd/sssd.conf, and it is worth reading:

[sssd]
domains = corp.example.com
config_file_version = 2
services = nss, pam

[domain/corp.example.com]
default_shell = /bin/bash
krb5_store_password_if_offline = True
cache_credentials = True
krb5_realm = CORP.EXAMPLE.COM
realmd_tags = manages-system joined-with-adcli
id_provider = ad
fallback_homedir = /home/%u@%d
ad_domain = corp.example.com
use_fully_qualified_names = False
ldap_id_mapping = True
access_provider = ad

What matters here:

  • id_provider = ad — users and groups come from the directory. sssd finds the controllers itself through SRV records, taking the site into account like DC Locator, and fails over to another one; ad_server is set only when you need to force it. Which controller and site it would pick right now is shown by adcli info:

    $ adcli info corp.example.com
    [domain]
    domain-name = corp.example.com
    domain-short = CORP
    domain-forest = corp.example.com
    domain-controller = dc1.corp.example.com
    domain-controller-site = Office
    domain-controller-flags = gc ldap ds kdc timeserv closest writable full-secret ads-web
    domain-controller-usable = yes
    domain-controllers = dc1.corp.example.com dc2.corp.example.com dc3.corp.example.com
    [computer]
    computer-site = Office
    
  • ldap_id_mapping = True — UIDs and GIDs are computed from the SID. The range from 200000 to 2000200000 is cut into 10000 slices of 200000 numbers; the domain SID goes through the murmurhash3 hash, the remainder of dividing it by the number of slices picks the domain's slice, and the RID is the offset in it. The result is the same on all machines with sssd and this domain, but has nothing to do with Windows, and uidNumber and gidNumber in the directory are ignored. The alternative is to store them in the directory (RFC 2307 attributes — RFC 2307 describes how to keep UNIX accounts in LDAP, and the attributes are in the AD schema) and set ldap_id_mapping = False, or realm join --automatic-id-mapping=no at join time. This is needed for NFS (the UNIX network file system, which checks permissions by numeric UID and GID) and for compatibility with existing UIDs, and means filling in the attributes for every user and group.

  • use_fully_qualified_names — by default realmd writes True, and the user is called alice@corp.example.com. Here it is False: short names (fully-qualified-names = no in /etc/realmd.conf before joining, or an edit of this line), and realm list shows login-formats: %U. Convenient in one domain; with several domains with clashing names you need the full ones.

  • fallback_homedir = /home/%u@%d — the home directory when the directory has no unixHomeDirectory; this is realmd's default, and for alice it is /home/alice@corp.example.com. The usual /home/%u is set here too. The directory is created by pam_mkhomedir at first logon (sudo pam-auth-update --enable mkhomedir).

  • cache_credentials = True — working without a connection to a controller: sssd keeps a salted SHA-512 hash of the password from the last successful logon and lets the user in with it when no controller is reachable (with no time limit unless offline_credentials_expiration is set). krb5_store_password_if_offline — remember a password entered while offline and get a ticket with it when the connection returns; until then the password sits in plain text in the kernel keyring.

  • access_provider = ad — take disabled and expired accounts into account and, through ad_gpo_access_control, logon rights from group policy (part 24). The default mode is enforcing. sssd reads from SYSVOL the policies that apply to the computer, takes the logon rights from their security settings and maps them to PAM services: sshd and cockpit to "Allow log on through Remote Desktop Services" and "Deny log on through Remote Desktop Services", login, su and graphical logons to "Allow log on locally" and "Deny log on locally". If no policy defines the right, logon is allowed; if one does, only those listed are allowed, and deny beats allow. sssd ignores built-in groups such as Administrators (S-1-5-32-544) in these rules, so you have to allow a domain group. PAM services that are in no list are denied by default (ad_gpo_default_right = deny). permissive computes the decision and logs it but does not deny; disabled turns the check off.

  • dyndns_update — on by default in the ad provider, which is why it is not in the file: when going online and then once a day (dyndns_refresh_interval = 86400) sssd registers A and PTR with the nsupdate utility signed with GSS-TSIG as LNX1$, like the Windows client in part 7; LNX1$ becomes the owner of the record it creates.

  • ad_maximum_machine_account_password_age — also a default, 30 days: once a day sssd checks the age of the machine password and, if it is older, changes it through adcli, like Netlogon on Windows; 0 turns this off.

After editing — sudo systemctl restart sssd, and before that a syntax check:

$ sudo sssctl config-check
Issues identified by validators: 0

Messages generated during configuration merging: 0

Used configuration snippet files: 0

Checking that sssd sees the domain and the users:

$ sudo sssctl domain-status corp.example.com
Online status: Online

Active servers:
AD Global Catalog: dc1.corp.example.com
AD Domain Controller: dc2.corp.example.com

Discovered AD Global Catalog servers:
- dc1.corp.example.com
- dc2.corp.example.com
- dc3.corp.example.com

Discovered AD Domain Controller servers:
- dc2.corp.example.com
- dc1.corp.example.com
- dc3.corp.example.com

$ getent passwd alice
alice:*:1565001104:1565000513:Alice Smith:/home/alice@corp.example.com:/bin/bash
$ id alice
uid=1565001104(alice) gid=1565000513(domain users) groups=1565000513(domain users),1565001145(linux-admins)
$ getent group linux-admins
linux-admins:*:1565001145:alice,bob

UID 1565001104 is the start of this domain's slice, 1565000000, plus RID 1104 from part 3; GID 1565000513 is Domain Users with RID 513. sssd returns group names in lower case: the ad provider is case-insensitive, like the directory itself. Groups with spaces in their names — domain users — stay groups with spaces, and in sudoers they will have to be escaped.

How these users get into the system is visible in nsswitch.conf and the PAM stack — the libnss-sss/libpam-sss packages edit them on installation:

$ grep -E '^(passwd|group|shadow|services|netgroup|automount):' /etc/nsswitch.conf
passwd:         files systemd sss
group:          files systemd sss
shadow:         files systemd sss
services:       db files sss
netgroup:       nis sss
automount:  sss
$ grep -h -E 'pam_(unix|sss|mkhomedir|localuser)' /etc/pam.d/common-auth /etc/pam.d/common-account /etc/pam.d/common-session
auth    [success=2 default=ignore]      pam_unix.so nullok
auth    [success=1 default=ignore]      pam_sss.so use_first_pass
account [success=1 new_authtok_reqd=done default=ignore]        pam_unix.so
account sufficient                      pam_localuser.so
account [default=bad success=ok user_unknown=ignore]    pam_sss.so
session required                        pam_mkhomedir.so umask=0022 skel=/etc/skel
session required        pam_unix.so
session optional                        pam_sss.so

Local files first, then sssd: a local alice in /etc/passwd overrides the domain one. In PAM the password is checked first by pam_unix against /etc/shadow, and if the user is not local, by pam_sss with the same password (use_first_pass); in account, local users pass through pam_localuser without asking sssd.


Logon and access

ssh alice@lnx1 with a password (from the built-in Windows client, ssh -l alice lnx1.corp.example.com, otherwise it substitutes CORP\alice, part 16): PAM calls pam_sss, sssd sends an AS-REQ with pre-authentication to a controller (part 6), gets a TGT and verifies it with a key from the keytab (krb5_validate, on in the ad provider): with this TGT it asks for a ticket for the machine's principal from the keytab and decrypts it with its own key — that is how the machine makes sure the answer came from the real KDC and not a fake one. Then pam_acct_mgmt asks the access_provider, and at first logon pam_mkhomedir creates /home/alice@corp.example.com. The TGT goes into the file /tmp/krb5cc_<UID>_XXXXXX (the suffix is random); on Fedora and RHEL the default cache is different — KCM, the Kerberos Credential Manager, where the sssd-kcm daemon keeps the tickets. With this ticket alice reaches Windows file shares over SMB and other domain machines over SSH without a password — that is the next part.

Who may log on — three ways, from simple to flexible.

realm permit — a list of users or groups:

$ sudo realm permit -g linux-admins
$ sudo grep -E '^(access_provider|simple_allow_groups)' /etc/sssd/sssd.conf
access_provider = simple
simple_allow_groups = linux-admins
$ realm list | tail -3
  login-policy: allow-permitted-logins
  permitted-logins:
  permitted-groups: linux-admins

realm permit -g switches access_provider to simple — and with that the checks for disabled accounts and GPO rights that the ad provider did are gone. realm permit alice bob allows specific users, realm permit --all restores the default after joining — login-policy: allow-realm-logins, logon for all domain users.

An LDAP filter. Stay on access_provider = ad and set ad_access_filter; for nested groups, with the LDAP_MATCHING_RULE_IN_CHAIN rule from part 5, and then sssd requires the full form with the domain name:

ad_access_filter = DOM:corp.example.com:(memberOf:1.2.840.113556.1.4.1941:=CN=linux-admins,OU=Groups,DC=corp,DC=example,DC=com)

Logon rights from GPO with enforcing: "who logs on to the Linux servers" is decided in the same policy as for Windows — a domain group goes into "Allow log on through Remote Desktop Services" of a policy linked to the Linux servers' OU. sshd is in the ad_gpo_map_remote_interactive list by default.

sudo — by directory group, with a file in /etc/sudoers.d/:

$ echo '%linux-admins ALL=(ALL) ALL' | sudo tee /etc/sudoers.d/linux-admins > /dev/null
$ sudo chmod 0440 /etc/sudoers.d/linux-admins
$ sudo visudo -cf /etc/sudoers.d/linux-admins
/etc/sudoers.d/linux-admins: parsed OK

With fully qualified names the group is written %linux-admins@corp.example.com, with a space in the name — %domain\ admins. On Ubuntu 26.04 the sudo command is sudo-rs, a sudo rewritten in Rust with the same sudoers format; it understands sssd groups just the same. sudo rules can also be kept in the directory (the sudoers schema for LDAP, sudo_provider = ad in sssd and sudoers: files sss in nsswitch.conf), and then they reach every machine by themselves, but that needs a schema extension worth doing deliberately (part 5), and classic sudo: sudo-rs does not read rules from LDAP.


Trusted domains

sssd with id_provider = ad finds the other domains of its forest by itself through the global catalog: users of eu.corp.example.com from part 21 are visible as bob@eu.corp.example.com and can log on if allowed, and each domain gets its own UID slice. If there are many domains and not all are needed, ad_enabled_domains = corp.example.com limits the list and speeds up startup.

A forest trust (part 22) is not used this way: the provider documentation says plainly that only the domains of its own forest are recognized.

$ sudo sssctl domain-list
corp.example.com
$ id bob@partner.example.net
id: ‘bob@partner.example.net’: no such user

Leaving

$ sudo realm leave -U alice.admin corp.example.com

-U implies --remove: the computer account is deleted or disabled in the directory. A plain realm leave without credentials removes the configuration only locally — the sssd configuration and the keys — and the account has to be deleted from the Windows side.


Looking at it by hand

Looking up a user who is not in the cache yet, from the machine's side:

$ sudo timeout 6 tcpdump -i eth0 -n -q 'port 88 or port 389 or port 3268' &
$ id bob
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
uid=1565001105(bob) gid=1565000513(domain users) groups=1565000513(domain users),1565001145(linux-admins)
09:47:08.343635 IP 192.168.1.60.46116 > 192.168.1.10.389: tcp 491
09:47:08.344618 IP 192.168.1.10.389 > 192.168.1.60.46116: tcp 1176
09:47:08.344642 IP 192.168.1.60.46116 > 192.168.1.10.389: tcp 0
09:47:08.376274 IP 192.168.1.60.46116 > 192.168.1.10.389: tcp 475
09:47:08.376971 IP 192.168.1.10.389 > 192.168.1.60.46116: tcp 1176
09:47:08.377886 IP 192.168.1.60.46116 > 192.168.1.10.389: tcp 159
09:47:08.378475 IP 192.168.1.10.389 > 192.168.1.60.46116: tcp 429
09:47:08.410932 IP 192.168.1.10.389 > 192.168.1.60.46116: tcp 429
09:47:08.410948 IP 192.168.1.60.46116 > 192.168.1.10.389: tcp 0
9 packets captured
9 packets received by filter
0 packets dropped by kernel

There is no Kerberos here at all: sssd keeps an open LDAP connection to the controller (up to 15 minutes, ldap_connection_expire_timeout), bound at some point with an LNX1$ ticket, and the machine's TGT sits in /var/lib/sss/db/ccache_CORP.EXAMPLE.COM. Three request-response pairs; what is in them, tcpdump -A will not show — the content is sealed by GSSAPI with the Kerberos session key (according to the documentation, the ad provider looks up the user and reads the groups through the single tokenGroups attribute). The last response came twice — a TCP retransmission. sssd goes to the global catalog (3268) for users of other domains of the forest; in a single-domain forest it does not go there. On the controller the machine shows up in the Security log as 4768 (a TGT for LNX1$) and 4769 with DC1$ in the service name field — the ticket for the controller's LDAP, obtained when the connection was opened.

What sssd does in DNS. The lnx1 record may appear before the machine joins the domain: an administrator created it, or a DHCP server that registers records on behalf of clients (part 8). Then the record has another owner, LNX1$ has no rights to it, and the daily update looks like this:

$ sudo journalctl -u sssd --since '-1 day' --no-pager
Oct 30 09:34:16 lnx1.corp.example.com sssd[59860]: ; TSIG error with server: tsig verify failure
Oct 30 09:34:16 lnx1.corp.example.com sssd[59860]: ; TSIG error with server: tsig verify failure
Oct 30 09:34:16 lnx1.corp.example.com sssd[59870]: ; TSIG error with server: tsig verify failure
Oct 30 09:34:16 lnx1.corp.example.com sssd[59870]: ; TSIG error with server: tsig verify failure
Oct 30 09:34:16 lnx1.corp.example.com sssd[59875]: ; TSIG error with server: tsig verify failure
Oct 30 09:34:16 lnx1.corp.example.com sssd[59875]: update failed: REFUSED
$ sudo grep 'Dyndns update' /var/log/sssd/sssd_corp.example.com.log | tail -1
(2026-10-30  9:34:16): [be[corp.example.com]] [be_ptask_done] (0x0040): Task [Dyndns update]: failed with [1432158240]: Dynamic DNS update failed

Each process number is a separate run of nsupdate. TSIG (RFC 8945) signs a DNS message with a shared key, GSS-TSIG is the same mechanism with a key negotiated over Kerberos; tsig verify failure means nsupdate could not verify the signature of the server's response, and the point is in the last line: the server refused (REFUSED). The refusal is about the record LNX1$ has no rights to. In a zone that accepts secure updates only, that is both the A and the PTR. If the forward zone accepts nonsecure updates as well, sssd does overwrite the A — with its own TTL of 3600 (in the DNS audit log on the controller it is a pair of events 520 and 519 with the machine's address), and the DHCP server puts its own back when the lease is renewed; REFUSED then comes from the reverse zone. From the Windows side:

PS C:\> Get-DnsServerResourceRecord -ComputerName dc1 -ZoneName corp.example.com -Name lnx1 | Select-Object HostName, RecordType, Timestamp, TimeToLive, @{n='Data'; e={$_.RecordData.IPv4Address}}

HostName RecordType Timestamp           TimeToLive Data
-------- ---------- ---------           ---------- ----
lnx1     A          30.10.2026 12:00:00 01:00:00   192.168.1.60

PS C:\> (Get-Acl 'AD:\DC=lnx1,DC=corp.example.com,CN=MicrosoftDNS,DC=ForestDnsZones,DC=corp,DC=example,DC=com').Owner
CORP\Domain Admins
PS C:\> Import-Module DhcpServer -SkipEditionCheck
PS C:\> Get-DhcpServerv4Lease -ComputerName dc1 -IPAddress 192.168.1.60 | Select-Object IPAddress, HostName, AddressState, DnsRR, DnsRegistration

IPAddress    HostName              AddressState      DnsRR   DnsRegistration
---------    --------              ------------      -----   ---------------
192.168.1.60 lnx1.corp.example.com ActiveReservation AandPTR Complete

The record is dynamic (it has a Timestamp), belongs to Domain Admins, and the A and PTR for this address are registered by the DHCP server. A record should have one registrar. Either it stays with the DHCP server, and dyndns_update = False goes into sssd.conf; or it goes to sssd: the DHCP server is told not to register the reservation (Set-DhcpServerv4DnsSetting -ComputerName dc1 -IPAddress 192.168.1.60 -DynamicUpdates Never), the old record is deleted, and sssd creates it again at the next update — now owned by LNX1$.

Checking a single user through NSS and PAM authorization without logging on as them:

$ sudo sssctl user-checks alice -s sshd -a acct
user: alice
action: acct
service: sshd

SSSD nss user lookup result:
 - user name: alice
 - user id: 1565001104
 - group id: 1565000513
 - gecos: Alice Smith
 - home directory: /home/alice@corp.example.com
 - shell: /bin/bash

SSSD InfoPipe user lookup result:
 - name: alice
 - uidNumber: 1565001104
 - gidNumber: 1565000513
 - gecos: Alice Smith
 - homeDirectory: not set
 - loginShell: not set

testing pam_acct_mgmt

pam_acct_mgmt: Success

PAM Environment:
 - no env -

NSS returns the home directory and shell from the sssd.conf defaults, InfoPipe (sssd's D-Bus interface) returns what is in the directory, and they are not there. pam_acct_mgmt is authorization: access_provider, GPO rights, a disabled account. With -a auth the command asks for the password and goes through pam_authenticate, that is, gets a TGT. If authorization returns Permission denied, the reason is in /var/log/sssd/sssd_corp.example.com.log with debug_level = 6 in the domain section. The cache, when something changed in the directory and the machine does not see it: sudo sss_cache -E marks all entries as expired, -u alice one user; otherwise an entry lives an hour and a half (entry_cache_timeout = 5400). New groups of a user who is already logged on appear only in a new session, as on Windows.

And the membership state in one command:

$ realm list
corp.example.com
  type: kerberos
  realm-name: CORP.EXAMPLE.COM
  domain-name: corp.example.com
  configured: kerberos-member
  server-software: active-directory
  client-software: sssd
  required-package: sssd-tools
  required-package: sssd
  required-package: libnss-sss
  required-package: libpam-sss
  required-package: adcli
  required-package: samba-common-bin
  login-formats: %U
  login-policy: allow-permitted-logins
  permitted-logins:
  permitted-groups: linux-admins

P.S. RC4 on Ubuntu 24.04 machines

A machine joined with Ubuntu 24.04 keeps RC4: msDS-SupportedEncryptionTypes = 28 and arcfour-hmac keys in the keytab. There is no hurry to fix it. The RC4 key of a computer account is a hash of a random 120-character password, which cannot be guessed, and the KDC issues tickets to the machine with AES anyway: both Windows clients and sssd offer it first. But Microsoft is phasing RC4 out of Kerberos: after the 2026 updates (KB5073381) the controllers assume that an account without this attribute supports AES only, while an explicitly written value is honored — with 28, RC4 stays allowed for the machine until it is removed by hand. To make 24.04 machines no different from 26.04, two commands are enough, and both can be undone.

First make sure nobody asks for RC4 to the machine: in events 4769 for its account the ticket encryption type should be 0x12 (AES256) or 0x11 (AES128), not 0x17 (RC4). Each controller has its own Security log:

PS C:\> $x = "*[System[EventID=4769] and EventData[Data[@Name='ServiceName']='LNX1`$']]"
PS C:\> Invoke-Command -ComputerName dc1, dc2 -ArgumentList $x -ScriptBlock { param($x) Get-WinEvent -LogName Security -FilterXPath $x -ErrorAction SilentlyContinue | ForEach-Object { ([xml]$_.ToXml()).Event.EventData.Data | Where-Object Name -eq 'TicketEncryptionType' | ForEach-Object '#text' } | Group-Object | Select-Object Count, Name } | Format-Table PSComputerName, Count, Name

PSComputerName       Count Name
--------------       ----- ----
dc1.corp.example.com     1 0x12
dc2.corp.example.com     1 0x12

Then leave only AES in the attribute — on the controller adcli will talk to next, so that it reads the new value without waiting for replication:

PS C:\> Set-ADComputer lnx1 -Server dc1 -KerberosEncryptionType AES128,AES256

And change the machine password right away instead of waiting for sssd to do it in 30 days:

$ sudo adcli update --verbose --domain-controller=dc1.corp.example.com --computer-password-lifetime=0 2>&1 | grep -E 'Changed computer password|kvno incremented'
 * Changed computer password
 * kvno incremented to 12
$ sudo klist -k -e
Keytab name: FILE:/etc/krb5.keytab
KVNO Principal
---- --------------------------------------------------------------------------
  11 LNX1$@CORP.EXAMPLE.COM (DEPRECATED:arcfour-hmac)
  11 LNX1$@CORP.EXAMPLE.COM (aes128-cts-hmac-sha1-96)
  11 LNX1$@CORP.EXAMPLE.COM (aes256-cts-hmac-sha1-96)
  11 host/LNX1@CORP.EXAMPLE.COM (DEPRECATED:arcfour-hmac)
  11 host/LNX1@CORP.EXAMPLE.COM (aes128-cts-hmac-sha1-96)
  11 host/LNX1@CORP.EXAMPLE.COM (aes256-cts-hmac-sha1-96)
  11 host/lnx1.corp.example.com@CORP.EXAMPLE.COM (DEPRECATED:arcfour-hmac)
  11 host/lnx1.corp.example.com@CORP.EXAMPLE.COM (aes128-cts-hmac-sha1-96)
  11 host/lnx1.corp.example.com@CORP.EXAMPLE.COM (aes256-cts-hmac-sha1-96)
  11 RestrictedKrbHost/LNX1@CORP.EXAMPLE.COM (DEPRECATED:arcfour-hmac)
  11 RestrictedKrbHost/LNX1@CORP.EXAMPLE.COM (aes128-cts-hmac-sha1-96)
  11 RestrictedKrbHost/LNX1@CORP.EXAMPLE.COM (aes256-cts-hmac-sha1-96)
  11 RestrictedKrbHost/lnx1.corp.example.com@CORP.EXAMPLE.COM (DEPRECATED:arcfour-hmac)
  11 RestrictedKrbHost/lnx1.corp.example.com@CORP.EXAMPLE.COM (aes128-cts-hmac-sha1-96)
  11 RestrictedKrbHost/lnx1.corp.example.com@CORP.EXAMPLE.COM (aes256-cts-hmac-sha1-96)
  12 LNX1$@CORP.EXAMPLE.COM (aes128-cts-hmac-sha1-96)
  12 LNX1$@CORP.EXAMPLE.COM (aes256-cts-hmac-sha1-96)
  12 host/LNX1@CORP.EXAMPLE.COM (aes128-cts-hmac-sha1-96)
  12 host/LNX1@CORP.EXAMPLE.COM (aes256-cts-hmac-sha1-96)
  12 host/lnx1.corp.example.com@CORP.EXAMPLE.COM (aes128-cts-hmac-sha1-96)
  12 host/lnx1.corp.example.com@CORP.EXAMPLE.COM (aes256-cts-hmac-sha1-96)
  12 RestrictedKrbHost/LNX1@CORP.EXAMPLE.COM (aes128-cts-hmac-sha1-96)
  12 RestrictedKrbHost/LNX1@CORP.EXAMPLE.COM (aes256-cts-hmac-sha1-96)
  12 RestrictedKrbHost/lnx1.corp.example.com@CORP.EXAMPLE.COM (aes128-cts-hmac-sha1-96)
  12 RestrictedKrbHost/lnx1.corp.example.com@CORP.EXAMPLE.COM (aes256-cts-hmac-sha1-96)

No need to edit krb5.conf: on update adcli takes the current value of msDS-SupportedEncryptionTypes and creates keys according to it, so the new generation 12 is AES only, and the attribute stays 24:

PS C:\> Get-ADComputer lnx1 -Properties msDS-SupportedEncryptionTypes, KerberosEncryptionType, msDS-KeyVersionNumber | Format-List Name, msDS-SupportedEncryptionTypes, KerberosEncryptionType, msDS-KeyVersionNumber

Name                          : LNX1
msDS-SupportedEncryptionTypes : 24
KerberosEncryptionType        : {AES128, AES256}
msDS-KeyVersionNumber         : 12

adcli kept generation 11 with the RC4 key on purpose — it is needed while the new password spreads to the controllers — and it disappears at the next password change. During those minutes adcli testjoin through a controller that has not received the new password yet answers Couldn't authenticate as machine account: LNX1: Preauthentication failed; after replication (between sites, up to its interval) it passes. To bring RC4 back: -KerberosEncryptionType RC4,AES128,AES256; the AES keys stay in the keytab with any value, so logons are never lost.


Sources

Joining the domain:

  • realm(8) — discover, join with --computer-ou, --os-name, --automatic-id-mapping, leave and --remove, permit;
  • realmd.conf(5) — default-home = /home/%U@%D, fully-qualified-names, automatic-id-mapping, manage-system;
  • adcli(8) — join, update, info, the full name through gethostname() and getaddrinfo(), the DONT_EXPIRE_PASSWORD flag, keeping the previous key, the current msDS-supportedEncryptionTypes value on update, --computer-password-lifetime, --os-name;
  • Ubuntu Server: Join a domain with SSSD — packages, realm join, mkhomedir.

SSSD:

  • sssd-ad(5) — the ad provider: only the domains of its own forest, ad_access_filter and DOM:, ad_gpo_access_control and ad_gpo_map_*, built-in groups, dyndns_*, ad_maximum_machine_account_password_age, the ID mapping algorithm, the krb5_validate and ldap_use_tokengroups defaults;
  • sssd.conf(5) — cache_credentials, offline_credentials_expiration, entry_cache_timeout, debug_level;
  • sssd-krb5(5) — krb5_validate, krb5_ccname_template, krb5_store_password_if_offline;
  • sssd-ldap(5) — ldap_connection_expire_timeout, ldap_use_tokengroups;
  • sssd-simple(5) — simple_allow_groups and simple_allow_users;
  • sssctl(8) — domain-status, domain-list, user-checks, config-check;
  • sss_cache(8) — marking cache entries as expired;
  • pam_sss(8) — the PAM module and use_first_pass;
  • sssd_krb5_locator_plugin(8) — how libkrb5 learns the KDCs from sssd;
  • sssd-sudo(5) — sudo rules from the directory;
  • winbindd(8) — the alternative to sssd that ships with Samba.

Kerberos:

The directory:

DNS and DHCP:

Logon rights:

The system:


The machine is in the domain, users log on with a password, and each of them has a TGT after logon. This TGT can do more than sit in the cache: with it you can log on to the next machine without a password, open a web application without a logon form and let a service on this machine reach the directory on its own behalf.

Next — part 30: Kerberos as single sign-on. GSSAPI in SSH on both sides — Linux and Windows — and ticket delegation; SPNs for services on Linux and keytabs for them; Negotiate authentication in web applications behind Apache and nginx; delegation in practice from the Linux side; and where to look when you get asked for a password again instead of single sign-on.

Only registered users can leave a comment.

Sign in or register to leave a comment.