Linux

How to Join Linux to Active Directory with SSSD and realmd (Ubuntu 24.04 and RHEL/Rocky 9)

27 min read

The Back Room Tech is reader-supported. We may earn a commission when you buy through links on our site. Learn more.

This guide takes a Linux server from a fresh install to a working Active Directory (AD) member. Domain users log in with their AD credentials, Kerberos handles authentication, and sudo rights follow AD group membership. Every step runs from the Linux side, with real commands, expected output, and fixes for the errors you’re most likely to hit. Tested on Ubuntu 24.04 LTS and RHEL 9/Rocky Linux 9.

Why bother? Without a join, every Linux box has its own local accounts, its own password resets, and its own offboarding checklist that someone forgets. After a join, disabling a user in AD locks them out of every Linux server at once. Adding someone to an AD group like linux-admins gives them sudo everywhere that group is trusted.

Linux Active Directory Integration with SSSD and realmd: How the Pieces Fit

The integration is a small stack of packages from the default Ubuntu and RHEL repositories:

  • realmd provides the realm command. It finds the domain through DNS, installs missing packages, and writes the initial config. It’s a setup tool: after the join, SSSD handles lookups and logins, and realmd only comes back into play when you run realm commands such as permit or leave.
  • adcli does the low-level enrollment work. It creates the computer object in AD and writes the machine’s Kerberos keytab (/etc/krb5.keytab).
  • SSSD (System Security Services Daemon) is the long-running broker. It looks up users and groups over LDAP, authenticates them with Kerberos, and caches results. With cache_credentials = True, which the join sets, users who have already logged in successfully while a DC was reachable can keep logging in through short domain controller outages.
  • Kerberos client tools (krb5-user on Ubuntu, krb5-workstation on RHEL) give you kinit and klist for testing tickets.
  • PAM and NSS are the hooks that let ssh, login, sudo, and id ask SSSD about AD users. On RHEL, authselect manages them. On Ubuntu, pam-auth-update does.

One honest trade-off: a direct join works best for small and mid-size fleets. Say you run hundreds of Linux hosts and want Linux-native policy, such as host-based access control and central sudo rules. FreeIPA with an AD trust scales better there. It also takes much more work to set up.

Prerequisites

  • A Linux server running Ubuntu 24.04 LTS or RHEL 9 / Rocky Linux 9, with root or sudo access
  • An existing, healthy AD domain with at least one reachable domain controller (DC)
  • The DC’s IP addresses. It must also be the DNS server for the AD zone, which is the default for AD-integrated DNS.
  • An AD account that can join computers to the domain. A delegated join account is best. A Domain Admin works for lab testing.
  • Optionally, the distinguished name (DN) of the organizational unit (OU) where Linux computer objects should go
  • An AD security group for Linux admins, such as linux-admins, plus a test user in it
  • Network access from the Linux host to the DCs on the ports listed in Step 4
RequirementDetails
OSUbuntu 24.04 LTS, or RHEL 9 / Rocky Linux 9
SSSD2.9.x (distro package)
Domain controllerWindows Server 2016, 2019, or 2022 (Red Hat’s RHEL 9 docs also list 2012 R2, which is out of Microsoft support; Samba AD DC also works)
HostnameFully qualified, and the short name must be 15 characters or less (NetBIOS limit)
Time syncchrony, synced to the DCs or the same upstream source
PrivilegesAll commands below need sudo unless noted

Test environment: Ubuntu 24.04 LTS and Rocky Linux 9 VMs with SSSD 2.9.x, joined to a Windows Server 2022 domain ad.example.com (realm AD.EXAMPLE.COM) with two DCs: dc1.ad.example.com (10.0.0.10) and dc2.ad.example.com (10.0.0.11). Replace these values with your own throughout. The command output shown below is example output from this lab. Package lists, verbose join lines, cache types, and defaults vary with distro release, realmd/SSSD version, AD policy, and any prior configuration, so match the key lines called out in each step rather than every line.

If you run the DCs in a homelab on a mini PC or a small tower server, put them on a UPS. A DC that loses power hard can come back with a drifted clock or a damaged database. Either one breaks every Linux login that depends on it.

Step-by-Step Guide

Step 1: Set a Fully Qualified Hostname

The hostname becomes the computer object name in AD and the Kerberos service principal (host/web01.ad.example.com). A short or wrong hostname is a common cause of joins that “succeed” and then fail Kerberos checks.

# Set the FQDN. Keep the first label at 15 characters or less.
sudo hostnamectl set-hostname web01.ad.example.com
hostname -f

Expected output:

web01.ad.example.com

If hostname -f returns only web01, check /etc/hosts. Make sure no line maps the FQDN to 127.0.1.1 ahead of the real IP. Ubuntu cloud images often add that line.

Step 2: Point DNS at Active Directory and Verify SRV Records

This is the step people skip most often. realm discover finds the domain by looking up DNS SRV records: _ldap._tcp.ad.example.com and _kerberos._tcp.ad.example.com. Only AD’s own DNS servers publish these. If the server uses 1.1.1.1, 8.8.8.8, or a router that doesn’t forward the AD zone, discovery fails.

Ubuntu 24.04 (netplan + systemd-resolved). Edit your netplan file. The name varies; it’s often /etc/netplan/50-cloud-init.yaml.

# /etc/netplan/50-cloud-init.yaml (relevant section only)
network:
  version: 2
  ethernets:
    ens18:
      addresses: [10.0.0.50/24]
      routes:
        - to: default
          via: 10.0.0.1
      nameservers:
        addresses: [10.0.0.10, 10.0.0.11]   # AD DCs only, no public resolvers
        search: [ad.example.com]
sudo netplan apply
resolvectl status ens18

RHEL 9 / Rocky 9 (NetworkManager):

# Find your connection name first
nmcli connection show

# ignore-auto-dns stops DHCP from overriding the AD DNS servers
sudo nmcli connection modify "ens18" ipv4.dns "10.0.0.10 10.0.0.11" \
  ipv4.dns-search "ad.example.com" ipv4.ignore-auto-dns yes
sudo nmcli connection up "ens18"

Now check the SRV records. You need dig, from bind9-dnsutils on Ubuntu (sudo apt install bind9-dnsutils) and bind-utils on RHEL (sudo dnf install bind-utils).

dig +short -t SRV _ldap._tcp.ad.example.com
dig +short -t SRV _kerberos._tcp.ad.example.com

Expected output (priority, weight, port, target):

0 100 389 dc1.ad.example.com.
0 100 389 dc2.ad.example.com.

0 100 88 dc1.ad.example.com.
0 100 88 dc2.ad.example.com.

If both queries return nothing, stop here. Every later step will fail. Fix DNS first: point the host at the DCs, or add a conditional forwarder for ad.example.com on your existing resolver.

Tip: If your AD domain ends in .local, the mDNS resolver can capture lookups before they reach the DCs. On Ubuntu, make sure resolvectl status shows the DCs as the DNS servers for your interface. Also confirm that dig _ldap._tcp.yourdomain.local SRV returns answers from 10.0.0.10.

Step 3: Synchronize Time with chrony

Kerberos rejects tickets when the client and DC clocks differ by more than the domain’s allowed skew. The AD default is 5 minutes. Past that, you get an error that’s at least easy to read: Clock skew too great.

Ubuntu 24.04 uses systemd-timesyncd by default. Swap it for chrony, which gives you better diagnostics. RHEL/Rocky already ship chrony.

# Ubuntu 24.04 only. Installing chrony removes systemd-timesyncd.
sudo apt update
sudo apt install -y chrony

Point chrony at your DCs. In AD, the DC holding the PDC Emulator role is the authoritative time source, so syncing to the DCs keeps everything consistent. The config file is /etc/chrony/chrony.conf on Ubuntu and /etc/chrony.conf on RHEL/Rocky. Comment out the default pool line and add:

# Sync against the AD domain controllers
server dc1.ad.example.com iburst
server dc2.ad.example.com iburst
# Service name is "chrony" on Ubuntu, "chronyd" on RHEL/Rocky
sudo systemctl restart chrony   # or: sudo systemctl restart chronyd
chronyc tracking

Expected output (trimmed):

Reference ID : 0A00000A (dc1.ad.example.com)
Stratum : 4
System time : 0.000183421 seconds fast of NTP time
Leap status : Normal

“Leap status: Normal” and a sub-second “System time” offset mean you’re safe. If the reference shows 00000000 or Leap status : Not synchronised, chrony can’t reach the DCs on UDP 123. Check the firewall step next.

Step 4: Confirm the Required Ports Are Reachable

A domain join talks to the DCs over several protocols. Local host firewalls (ufw on Ubuntu, firewalld on RHEL) allow outbound traffic by default. Blocked ports usually sit on a network firewall or a security group between the server and the DCs.

PortProtocolServiceWhy it’s needed
53TCP/UDPDNSSRV lookups and host resolution
88TCP/UDPKerberosTickets for the machine and users
123UDPNTPTime sync with the DCs
389TCP/UDPLDAPUser and group lookups, CLDAP discovery
445TCPSMBUsed by some join and GPO operations
464TCP/UDPkpasswdMachine account password changes
636TCPLDAPSLDAP over TLS, if enforced
3268/3269TCPGlobal CatalogMulti-domain forests, universal groups

Test the TCP ports with nc (from netcat-openbsd on Ubuntu, or nmap-ncat on RHEL):

# -z scans without sending data; -v prints the result
for port in 53 88 389 445 464 636 3268; do nc -zv -w 3 dc1.ad.example.com $port; done

Expected output for each port:

Connection to dc1.ad.example.com 88 port [tcp/kerberos] succeeded!

The core join-and-login path is DNS (53), Kerberos (88), LDAP (389), and kpasswd (464). The other rows depend on whether you use LDAPS, the Global Catalog, or GPO-based access control. This loop only tests TCP; DNS, Kerberos, CLDAP, kpasswd, and NTP also use UDP, so make sure the firewall rules cover UDP for those ports too.

A “timed out” or “Connection refused” result points to a firewall between you and the DC. Get it opened before joining. A half-finished join is harder to clean up than a clean failure.

Warning: If you lock down egress with ufw default deny outgoing or custom firewalld policies, add explicit allow rules for these ports to the DC IPs before you reload the firewall. If you manage the server over SSH, confirm your SSH rule is in place before any ufw enable or ufw reset.

Step 5: Install the Domain-Join Packages

Package names differ between distro families. Install everything upfront. Otherwise realmd tries to pull missing packages through PackageKit during the join, which is slow and fails on minimal servers.

Ubuntu 24.04 LTS:

sudo apt update
# sssd-ad pulls in sssd, libnss-sss and libpam-sss
# krb5-user provides kinit/klist for testing (it may prompt for a default realm: enter AD.EXAMPLE.COM)
sudo apt install -y realmd sssd-ad sssd-tools adcli krb5-user

RHEL 9 / Rocky Linux 9:

# oddjob + oddjob-mkhomedir create home dirs on first login
# samba-common-tools is listed as required by realm discover on RHEL
sudo dnf install -y realmd sssd adcli oddjob oddjob-mkhomedir krb5-workstation samba-common-tools

On RHEL, dnf needs an active subscription (or a Satellite/local mirror) to reach these packages. Rocky Linux ships the same package names in its own repositories, so the command is identical.

On RHEL/Rocky, confirm authselect is present. It replaced the legacy authconfig tool, which was removed in RHEL 8.

sudo authselect current

Before the join, you’ll usually see No existing configuration detected. or the minimal/local profile. That’s fine; the join switches it to sssd.

Step 6: Discover the Domain

realm discover resolves the domain’s _ldap._tcp SRV record and queries a DC for the domain’s details (add -v to watch both happen). A clean result confirms that DNS and basic DC connectivity work. It changes nothing on the system.

realm discover ad.example.com

Example output on Rocky 9:

ad.example.com
type: kerberos
realm-name: AD.EXAMPLE.COM
domain-name: ad.example.com
configured: no
server-software: active-directory
client-software: sssd
required-package: oddjob
required-package: oddjob-mkhomedir
required-package: sssd
required-package: adcli
required-package: samba-common-tools

On Ubuntu, the required-package lines list sssd-tools, sssd, libnss-sss, libpam-sss, adcli, and samba-common-bin instead. The exact list can change between realmd versions. Check two lines: server-software: active-directory and client-software: sssd. If the client software shows winbind, Samba’s Winbind is set as the default instead of SSSD.

Output of `realm discover ad.example.com` showing type kerberos, realm-name AD.EXAMPLE.COM, domain-name ad.example.com, configured: no, server-software: active-directory, client-software: sssd, and the required-package list

If you get realm: No such realm found, jump to the DNS troubleshooting entry below.

Step 7: Join the Domain

Run the join with a user that has join rights. The -v flag shows every step, which you’ll want the first time something breaks.

# -U: AD account with join rights (you'll be prompted for its password)
# --computer-ou: optional; without it the object lands in CN=Computers
# -v: verbose output
sudo realm join ad.example.com -U joinadmin \
  --computer-ou="OU=Linux Servers,DC=ad,DC=example,DC=com" -v

Example output (trimmed; the exact lines vary by realmd/adcli version and by whether a computer object with this name already exists):

* Resolving: _ldap._tcp.ad.example.com
* Performing LDAP DSE lookup on: 10.0.0.10
* Successfully discovered: ad.example.com
Password for joinadmin:
* Required files: /usr/sbin/oddjobd, /usr/libexec/oddjob/mkhomedir, /usr/sbin/sssd, /usr/sbin/adcli
* LANG=C /usr/sbin/adcli join –verbose –domain ad.example.com –domain-realm AD.EXAMPLE.COM –domain-controller 10.0.0.10 –computer-ou OU=Linux Servers,DC=ad,DC=example,DC=com –login-type user –login-user joinadmin –stdin-password
* Using domain name: ad.example.com
* Calculated computer account name from fqdn: WEB01
* Found computer account for WEB01$ at: CN=WEB01,OU=Linux Servers,DC=ad,DC=example,DC=com
…
* Added the entries to the keytab: host/web01.ad.example.com@AD.EXAMPLE.COM: FILE:/etc/krb5.keytab
* /usr/bin/systemctl enable sssd.service
* /usr/bin/systemctl restart sssd.service
* Successfully enrolled machine in realm

Without -v, a successful join prints nothing at all. No output means it worked.

Verbose output of `sudo realm join --user=joinadmin --verbose ad.example.com` showing realmd discovery, the adcli join, the host/web01 keytab entry, and the final line "Successfully enrolled machine in realm" highlighted

Tip: For automated provisioning, pre-create the computer object and join with a one-time password (realm join --one-time-password), or call adcli join --stdin-password directly, which reads the join account’s password from stdin. Don’t put a Domain Admin password in a cloud-init file.

Step 8: Verify the Join Actually Worked

“Successfully enrolled” means the computer object and keytab exist. It doesn’t prove that users can resolve and log in. Run these four checks.

1. realm list confirms realmd considers the machine configured:

realm list

ad.example.com
type: kerberos
realm-name: AD.EXAMPLE.COM
domain-name: ad.example.com
configured: kerberos-member
server-software: active-directory
client-software: sssd
required-package: oddjob
required-package: oddjob-mkhomedir
required-package: sssd
required-package: adcli
required-package: samba-common-tools
login-formats: %U@ad.example.com
login-policy: allow-realm-logins

configured: kerberos-member is the line you want. login-policy: allow-realm-logins means realmd isn’t restricting logins yet: any AD user who passes SSSD’s own access checks (with the default access_provider = ad, that includes AD account state and GPO logon rights) can log in right now. You’ll tighten that in Step 10.

Output of `realm list` showing configured: kerberos-member, client-software: sssd, login-formats: %U@ad.example.com, and login-policy: allow-realm-logins

2. id confirms SSSD resolves AD users and their groups through NSS:

id jdoe@ad.example.com

uid=1587401103(jdoe@ad.example.com) gid=1587400513(domain users@ad.example.com) groups=1587400513(domain users@ad.example.com),1587401120(linux-admins@ad.example.com),1587401121(web-team@ad.example.com)

The huge UID and GID numbers are normal. SSSD generates them from the AD security identifier (SID) because ldap_id_mapping = True. The same user gets the same UID on every joined host.

Output of `id jdoe@ad.example.com` showing mapped uid/gid and AD group memberships including linux-admins@ad.example.com

3. The machine keytab confirms the host has working Kerberos credentials:

sudo klist -k /etc/krb5.keytab

Keytab name: FILE:/etc/krb5.keytab
KVNO Principal
—- ————————————————————————–
2 WEB01$@AD.EXAMPLE.COM
2 host/WEB01@AD.EXAMPLE.COM
2 host/web01.ad.example.com@AD.EXAMPLE.COM
…

4. sssctl confirms SSSD is online and talking to a DC:

sudo sssctl domain-status ad.example.com

Online status: Online

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

A note on wbinfo: wbinfo -u and wbinfo -t are Winbind tools. They talk to winbindd, which isn’t running on an SSSD-based join. So on this setup, wbinfo failing with “failed to call wbcPing” or similar is expected, and your join is fine. Only rely on wbinfo if you joined with --client-software=winbind, for example on a Samba file server. For SSSD joins, use id, getent passwd user@domain, and sssctl.

Step 9: Enable Automatic Home Directory Creation

AD users don’t have local home directories. Without this step, their first SSH login fails or drops them into /.

RHEL 9 / Rocky 9. realm join sets the authselect profile. Check that the mkhomedir feature is on and that oddjobd is running:

sudo authselect current

Profile ID: sssd
Enabled features:
– with-mkhomedir

If with-mkhomedir is missing, enable it and start the oddjob daemon:

sudo authselect select sssd with-mkhomedir --force
sudo systemctl enable --now oddjobd

Don’t hand-edit /etc/pam.d/system-auth or /etc/nsswitch.conf on RHEL. authselect owns those files and overwrites manual edits the next time it runs.

Ubuntu 24.04. realmd on Ubuntu doesn’t turn on home directory creation for you. Enable the PAM module:

sudo pam-auth-update --enable mkhomedir

This adds pam_mkhomedir.so to /etc/pam.d/common-session. The next AD login creates /home/jdoe@ad.example.com (or whatever fallback_homedir specifies).

Step 10: Restrict Who Can Log In

By default, any enabled AD account can SSH into the server. On most networks that’s too broad. realm permit switches SSSD to the simple access provider and writes an allow list. That is a change of access model, not just an added list: with access_provider = simple, SSSD stops applying AD GPO logon rights (those only apply with access_provider = ad). Pick one model per host rather than expecting both to stack.

# Deny everyone first, then allow specific groups
sudo realm deny --all
sudo realm permit -g linux-admins@ad.example.com
sudo realm permit -g web-team@ad.example.com

# Or allow an individual user
sudo realm permit jdoe@ad.example.com

Check the result:

realm list

…
login-formats: %U@ad.example.com
login-policy: allow-permitted-logins
permitted-logins: jdoe@ad.example.com
permitted-groups: linux-admins@ad.example.com, web-team@ad.example.com

To remove a group later, run sudo realm permit –withdraw -g web-team@ad.example.com.

Tip: For an AD group with spaces in its name, quote it: sudo realm permit -g “linux server admins@ad.example.com”.

Step 11: Grant sudo Rights to an AD Group

Create a drop-in file under /etc/sudoers.d/ rather than editing /etc/sudoers. Always edit it with visudo, which checks syntax before saving. A broken sudoers file can lock you out of root.

sudo visudo -f /etc/sudoers.d/ad-linux-admins

Add:

# /etc/sudoers.d/ad-linux-admins
# Members of the AD group linux-admins get full sudo
%linux-admins@ad.example.com ALL=(ALL:ALL) ALL

# Group names with spaces need a backslash before each space:
# %linux\ server\ admins@ad.example.com ALL=(ALL:ALL) ALL

Write the group name exactly as id prints it in Step 8. With use_fully_qualified_names = True that includes @ad.example.com; if you switch to short names later, the rule must change to match.

Lock the permissions down and validate the file:

sudo chmod 0440 /etc/sudoers.d/ad-linux-admins
sudo visudo -cf /etc/sudoers.d/ad-linux-admins

/etc/sudoers.d/ad-linux-admins: parsed OK

The file /etc/sudoers.d/ad-linux-admins open or printed in the terminal showing the rule %linux-admins@ad.example.com ALL=(ALL:ALL) ALL, followed by visudo -cf output "parsed OK"

How to avoid editing sudoers on every server: the rule names a group, so you write this file once and leave it alone. Adding or removing admins becomes an AD group change. To get the file onto every host, bake it into your VM template or golden image, or ship it with Ansible, Salt, or Puppet alongside the join.

SSSD can also read sudo rules stored in AD (sudo_provider = ad). That requires extending the AD schema with the sudoRole object class. It’s Windows-side work that plenty of teams skip. One static file per host plus AD group membership covers the usual cases.

Warning: Group membership is cached. After adding someone to linux-admins in AD, they may need to log out and back in. On the server, you can flush the cache with sudo sss_cache -E.

Step 12: Test Kerberos Tickets and a Real Login

First, confirm that a user can get a Kerberos ticket from the DC. The realm must be uppercase.

kinit jdoe@AD.EXAMPLE.COM
klist

Expected output:

Password for jdoe@AD.EXAMPLE.COM:

Ticket cache: KCM:1000
Default principal: jdoe@AD.EXAMPLE.COM

Valid starting Expires Service principal
09/26/2026 10:14:02 09/26/2026 20:14:02 krbtgt/AD.EXAMPLE.COM@AD.EXAMPLE.COM
renew until 10/03/2026 10:13:58

RHEL/Rocky use the KCM: cache type by default. Ubuntu usually shows FILE:/tmp/krb5cc_1000. Both are fine. A 10-hour ticket lifetime with 7-day renewal is the AD default policy. Clear the test ticket with kdestroy.

Running `kinit jdoe@AD.EXAMPLE.COM` with the password prompt, then `klist` showing the ticket cache, default principal jdoe@AD.EXAMPLE.COM, and the krbtgt/AD.EXAMPLE.COM service principal with valid-starting and expiry times

Now do a real end-to-end login from another machine:

ssh -l 'jdoe@ad.example.com' web01.ad.example.com

Passing the username with -l avoids any ambiguity about which @ separates user from host. The two-@ form that Ubuntu’s own SSSD guide uses (ssh jdoe@ad.example.com@web01.ad.example.com) also works with OpenSSH, but -l is the safer form for scripts and copy-paste. After logging in, check the home directory and sudo:

pwd
sudo -l

/home/jdoe@ad.example.com

User jdoe@ad.example.com may run the following commands on web01:
(ALL : ALL) ALL

If all of that works, the integration is complete.

Configuration

Where sssd.conf Lives and Why Permissions Matter

realm join writes the main config to /etc/sssd/sssd.conf. SSSD also reads drop-in files from /etc/sssd/conf.d/, and current Red Hat documentation prefers that directory for your own changes. Drop-in files must end in .conf. SSSD merges them in alphabetical order after the main file. Put custom settings in something like /etc/sssd/conf.d/ad.example.com.conf. The realmd file stays untouched, and your tweaks live in one obvious place.

This is the file realmd generates:

# /etc/sssd/sssd.conf (as written by realm join)
[sssd]
domains = ad.example.com
config_file_version = 2
services = nss, pam

[domain/ad.example.com]
default_shell = /bin/bash
krb5_store_password_if_offline = True
cache_credentials = True
krb5_realm = AD.EXAMPLE.COM
realmd_tags = manages-system joined-with-adcli
id_provider = ad
fallback_homedir = /home/%u@%d
ad_domain = ad.example.com
use_fully_qualified_names = True
ldap_id_mapping = True
access_provider = simple
simple_allow_groups = linux-admins@ad.example.com, web-team@ad.example.com

(access_provider is ad straight after the join. realm permit changed it to simple in Step 10.)

Output of `sudo cat /etc/sssd/sssd.conf` showing the [sssd] and [domain/ad.example.com] sections

SSSD refuses to start if these files are readable by other users. They can contain sensitive settings, so SSSD checks ownership and mode at startup. After any manual edit, run:

sudo chown root:root /etc/sssd/sssd.conf /etc/sssd/conf.d/*.conf
sudo chmod 600 /etc/sssd/sssd.conf /etc/sssd/conf.d/*.conf
sudo sssctl config-check
sudo systemctl restart sssd

sssctl config-check catches typos and unknown options before the restart. Mode 400 also works. 600 is easier because you don’t have to change permissions each time you edit.

Key sssd.conf Settings

SettingDefault after joinWhat it does
id_provideradUses AD for user and group lookups
access_providerad (becomes simple after realm permit)ad honors AD account state and GPO logon rights; simple uses allow lists
ad_domainSet by joinThe AD DNS domain name
ad_hostnameSystem FQDNOverride if the local hostname isn’t the FQDN registered in AD
ad_serverAuto (DNS SRV)Pin specific DCs, e.g. dc1.ad.example.com, dc2.ad.example.com
use_fully_qualified_namesTrueRequires user@domain at login
fallback_homedir/home/%u@%dHome directory template (%u user, %d domain)
ldap_id_mappingTrueGenerates UIDs from SIDs; set False to use AD uidNumber/gidNumber
cache_credentialsTrueAllows offline logins with cached credentials
dyndns_updateTrue for ADRegisters the host’s A/PTR records in AD DNS using the machine account (GSS-TSIG)
ad_gpo_access_controlenforcingApplies GPO logon rights when access_provider = ad

Allowing Short Usernames

Typing jdoe@ad.example.com for every SSH login gets old fast. In a single-domain environment you can drop the suffix:

# /etc/sssd/conf.d/ad.example.com.conf
[domain/ad.example.com]
use_fully_qualified_names = False
fallback_homedir = /home/%u

Then update the sudoers rule to %linux-admins, fix permissions, clear the cache, and restart:

sudo chmod 600 /etc/sssd/conf.d/ad.example.com.conf
# -p stops SSSD first, -o overwrites any earlier local-data backup, -s starts SSSD afterwards.
# This deletes the whole cache (unlike sss_cache -E, which only invalidates entries).
sudo sssctl cache-remove -o -s -p
sudo systemctl restart sssd

The trade-off: short names can collide with local accounts. An AD user called admin or backup will clash with a local account of the same name. In multi-domain forests, short names are also ambiguous. Leave fully qualified names on if either applies.

Using Existing POSIX Attributes

Your AD may already store uidNumber and gidNumber, for example because NFS servers depend on consistent IDs. If so, set ldap_id_mapping = False. Do this before users log in. Changing ID mapping later changes every user’s UID, and existing file ownership will point at the wrong accounts.

Decommissioning: Removing a Host with realm leave

When you retire or rebuild a server, leave the domain cleanly. That avoids stale computer objects, plus duplicate-account problems when a new host reuses the hostname.

Warning: This removes the host’s AD computer object and its Kerberos keytab. AD users will no longer be able to log in. Make sure you have a working local account with sudo, or console access, before you run it.

# --remove deletes the computer object in AD (needs an account with rights, via -U)
sudo realm leave ad.example.com --remove -U joinadmin -v

Expected ending:

* Removing entries from keytab for realm
* /usr/bin/systemctl disable sssd.service
* /usr/bin/systemctl stop sssd.service
* Successfully unenrolled machine from realm

Then clean up what realmd doesn’t own:

sudo rm -f /etc/sudoers.d/ad-linux-admins
realm list                      # should print nothing
sudo klist -k /etc/krb5.keytab  # should fail or show no AD principals

Archive or delete AD users’ home directories under /home/*@ad.example.com according to your retention policy. If you’re only re-imaging, run realm leave on the old install before wiping it. If that’s no longer possible, have the AD team reset or delete the stale object before the new build joins.

Tips and Troubleshooting

“realm: No such realm found”

$ sudo realm discover ad.example.com -v
* Resolving: _ldap._tcp.ad.example.com
* No results: ad.example.com
realm: No such realm found

Why it happens: The host’s resolver can’t find the _ldap._tcp SRV record. Almost always, the host is using a public or ISP DNS server instead of AD DNS.

Fix:

resolvectl status          # Ubuntu: which DNS servers are really in use?
cat /etc/resolv.conf       # RHEL: should list the DC IPs
dig -t SRV _ldap._tcp.ad.example.com @10.0.0.10

If the query against @10.0.0.10 works but the plain query doesn’t, your resolver config is the problem. Go back to Step 2. If even the direct query to the DC fails, the SRV records are missing on the AD side. Ask the AD team to check that the DC’s Netlogon service has registered its DNS records.

Output of `sudo realm discover ad.example.com -v` ending in "realm: No such realm found", followed by `dig -t SRV _ldap._tcp.ad.example.com @10.0.0.10` returning SRV records for dc1 and dc2

“Clock skew too great”

$ kinit jdoe@AD.EXAMPLE.COM
kinit: Clock skew too great while getting initial credentials

During realm join, the same problem shows up as an adcli authentication failure that mentions clock skew.

Why it happens: The Linux clock differs from the DC clock by more than the Kerberos tolerance (5 minutes by default).

How to confirm it: If chrony is already pointed at the DCs (Step 3), check the measured offset per source:

chronyc sources -v

The “Last sample” column shows the offset between the local clock and each DC at the last measurement. A value of several minutes (for example +412s) is well over the 300-second limit.

If chrony isn’t configured yet, chronyd’s documented one-shot mode can measure the offset against a DC without correcting the clock:

# -Q prints the offset and exits without adjusting the clock
sudo chronyd -Q 'server dc1.ad.example.com iburst'

Fix: Step the clock now, then make sure chrony keeps it synced:

sudo chronyc -a makestep
chronyc tracking

Also check the timezone with timedatectl. Kerberos compares UTC, so a wrong timezone alone won’t break it. A clock someone “fixed” by setting local time as UTC will. Hypervisors that sync guest time can also fight chrony. Disable host time sync for the VM, or let chrony own the clock.

Join fails with connection or LDAP errors

! Couldn’t connect to ad.example.com domain: Couldn’t connect to 10.0.0.10: Can’t contact LDAP server
adcli: couldn’t connect to ad.example.com domain: Can’t contact LDAP server
realm: Couldn’t join realm: Failed to join the domain

Why it happens: A firewall between the host and the DC blocks LDAP (389), Kerberos (88), or kpasswd (464). DNS on port 53 often works while the others are blocked. So discovery succeeds, and then the join fails.

Fix: Re-run the port test from Step 4 against every DC the SRV record returns. realmd may pick a different DC than the one you tested. If you enforce local egress rules, allow the ports:

# Ubuntu (ufw) TCP example for one DC. Add matching "proto udp" rules
# (e.g. port 88 proto udp) for DNS, Kerberos, CLDAP, kpasswd, and NTP.
sudo ufw allow out to 10.0.0.10 port 88
sudo ufw allow out to 10.0.0.10 port 389
sudo ufw allow out to 10.0.0.10 port 464

“Insufficient permissions to join the domain”

Why it happens: The account passed with -U can’t create or reset computer objects in the target OU. Or a computer object with the same name already exists and belongs to another account.

Fix: Use an account with delegated join rights on that OU. If an old object is in the way, have it removed or reset in AD, then retry sudo realm join ad.example.com -U joinadmin -v.

SSSD won’t start after editing sssd.conf

Check the journal:

sudo journalctl -u sssd -b --no-pager -n 30

sssd[2231]: File ownership and permissions check failed. Expected root:root and 0600.

Fix: sudo chown root:root /etc/sssd/sssd.conf && sudo chmod 600 /etc/sssd/sssd.conf. Do the same for any file in conf.d/. Then run sudo sssctl config-check and restart SSSD. If the check finds a syntax error, it gives you the file and line number.

id says “no such user” after a successful join

Why it happens: SSSD is stopped, offline, or looking up a name in the wrong format.

Fix:

sudo systemctl status sssd
sudo sssctl domain-status ad.example.com
id jdoe@ad.example.com   # use the full name if use_fully_qualified_names = True

If the domain shows Offline, SSSD can’t reach a DC; recheck DNS and ports. For deeper debugging, add debug_level = 6 under the domain section, restart SSSD, and read /var/log/sssd/sssd_ad.example.com.log.

User resolves but SSH says “Permission denied”

Why it happens: With access_provider = ad, SSSD enforces GPO logon rights by default (ad_gpo_access_control = enforcing). A GPO that restricts “Allow log on through Remote Desktop Services” or “Allow log on locally” can quietly block Linux logins. If you’re using realm permit, the user may also not be in a permitted group.

Fix: Check realm list for the permitted users and groups. Then look for GPO access-denied lines in /var/log/sssd/sssd_ad.example.com.log. For a quick diagnosis, set ad_gpo_access_control = permissive in a conf.d file. Permissive mode logs GPO denials without enforcing them. Use it to confirm the cause, then fix the GPO or switch to realm permit. Don’t leave it permissive.

Login works but there’s no home directory

Fix: On RHEL/Rocky, run sudo systemctl enable --now oddjobd, then sudo authselect select sssd with-mkhomedir --force. On Ubuntu, run sudo pam-auth-update --enable mkhomedir.

Quick Tips

  • Keep a local break-glass account. If both DCs go down and the SSSD cache expires, AD logins stop. A local admin with an SSH key stored somewhere safe is cheap insurance.
  • Harden SSH next. Once AD handles identity, restrict SSH with AllowGroups in sshd_config to match your realm permit groups.
  • Put two DCs in ad_server if you want predictable failover rather than whatever DNS returns first.
  • Flush caches when testing group changes: sudo sss_cache -E invalidates cached entries without deleting the offline credential cache.

Wrapping Up

You now have a Linux server that trusts AD for identity, limits logins to the groups you permitted, and ties sudo to AD group membership. The join itself takes about five minutes. Nearly all the real work is DNS and time sync. Once those are right, realm join is one line in your provisioning script.

My take: for a mixed Windows/Linux shop with an existing domain, SSSD and realmd are the right default. Commercial AD bridges and old Winbind setups are only worth it if you need one of their specific extras.

StepActionApplies To
1Set FQDN hostnameBoth
2Point DNS at DCs, verify SRV recordsBoth
3Sync time with chronyBoth
4Test ports 53/88/389/445/464/636Both
5Install packagesUbuntu: sssd-ad sssd-tools adcli realmd krb5-user; RHEL: sssd adcli oddjob oddjob-mkhomedir krb5-workstation realmd samba-common-tools
6–7realm discover, realm joinBoth
8Verify with realm list, id, klist -k, sssctlBoth
9Enable home directory creationRHEL: authselect; Ubuntu: pam-auth-update
10realm permit allowed groupsBoth
11AD group in /etc/sudoers.d/Both
12kinit/klist and SSH testBoth

Resources