← field notes

Joining Linux Servers to Active Directory with MFA — Offline

How realmd, adcli, SSSD, Kerberos, PAM and RADIUS work together so engineers can log in to Linux with their domain credentials and a second factor, even on air-gapped servers

Sarooj Zawahir21 October 202619 min read

  • Linux
  • Active Directory
  • Security

1. Introduction

Most enterprises run identity on Microsoft Active Directory (AD), but many of their critical systems run Linux. Without integration, every Linux server keeps its own list of local users and passwords. That means accounts nobody remembers to remove, passwords shared between engineers, and no single place to see who has access to what.

This article explains, end to end, how a Red Hat-family Linux server (RHEL, Rocky Linux, AlmaLinux and similar) is joined to an AD domain so that people log in with their domain username and password, followed by a second factor (MFA) delivered through RADIUS. Access is controlled through AD group membership, both for SSH logins and for sudo.

The focus is on understanding, not just copying commands. You will see what each software package is for, what happens on the network when the server joins the domain, and what happens inside the server during every single login. The configuration steps come at the end, written for servers with no internet access, which is the norm in operational technology, security and regulated environments.

What you will learn

  • The role of each component: realmd, adcli, SSSD, Kerberos, NSS, PAM, oddjob, chrony and the pam_radius module.

  • What the domain join actually changes in Active Directory and on the Linux server.

  • How an AD user becomes a valid Linux user, including how Windows security identifiers (SIDs) turn into Linux UIDs.

  • The exact sequence of a login: password, MFA challenge, group check, account check and home folder creation.

  • How to build an offline installation bundle and configure the whole solution step by step.

  • The trade-offs and security points you should consider before rolling it out.

Who this is for

System and network engineers, infrastructure administrators and security engineers who manage mixed Windows/Linux estates. You should be comfortable with a Linux shell and basic AD concepts (users, groups, organisational units). No deep Kerberos or PAM knowledge is assumed; Section 4 covers what you need.

Scope and assumptions

Item Assumption in this article
Linux distribution RHEL-family release 8 or 9 (RHEL, Rocky, AlmaLinux). Differences for release 7 are noted where they matter.
Directory A single Active Directory domain with one or more domain controllers (DCs).
MFA Any MFA product that exposes a RADIUS interface and can validate AD passwords (for example Microsoft NPS with an MFA extension, or a third-party MFA server).
Network The Linux server has no internet access. It can reach the DCs and the RADIUS server on the internal network.
Login paths covered SSH and sudo. The same pattern applies to console and graphical logins.

Naming conventions used

All names below are placeholders. Replace them with your own values.

Placeholder Meaning
example.com / EXAMPLE.COM AD DNS domain / Kerberos realm (the realm is the domain in upper case)
srv01.example.com The Linux server being joined
dc1, dc2 (10.0.0.10, 10.0.0.11) Domain controllers, which also act as DNS and time servers
mfa1, mfa2.example.com RADIUS / MFA servers
svc-domainjoin AD account with permission to create computer objects in the target OU
lnx-ssh-users AD group whose members may log in over SSH
lnx-sudo-admins AD group whose members may use sudo
jsmith An example AD user

2. Why join Linux to Active Directory?

Joining Linux to AD replaces a scattered set of local accounts with the identity system the organisation already runs. The practical benefits are:

Without AD integration With AD integration
Local accounts on every server; passwords managed by hand One identity per person; password policy, expiry and lockout enforced by AD
Leavers keep working accounts until someone remembers Disable the AD account once, and access stops everywhere
Access granted per server, hard to audit Access granted by AD group membership and reviewed centrally
Shared admin passwords; little accountability Every login and sudo command is tied to a named person
MFA difficult to add consistently MFA enforced at a single point (RADIUS) for every server

There are several ways to integrate Linux with AD (Samba Winbind, FreeIPA trusts, third-party agents). This article uses the approach built into Red Hat-family distributions: realmd + adcli + SSSD, extended with pam_radius for MFA [1]. It uses only standard packages and plain configuration files, and once joined the server needs no extra agent.

3. The solution at a glance

Figure 1 shows the complete picture. On the left is the person logging in. In the middle is the Linux server, drawn as layers: the programs people interact with, the PAM modules that make the security decisions, the background services, and the key configuration files. On the right are the two external systems the server depends on: Active Directory and the RADIUS/MFA server.

Figure 1. Solution architecture: components on the Linux server and the external services they talk to

The solution does three separate jobs, and keeping them apart makes everything else easier to understand:

Job Question it answers Done by When
1. Join Is this server a trusted member of the domain? realmd + adcli Once, at installation
2. Identity Who is jsmith? What are his UID, groups and home folder? SSSD through NSS Every time any program looks up a user
3. Authentication and authorisation Is this really jsmith, and is he allowed in? PAM: pam_radius, the group check, pam_sss Every login and every sudo

4. Core concepts in five minutes

Six building blocks appear throughout this article. If you already know them, skip ahead to Section 5.

DNS service (SRV) records

AD publishes its services in DNS. Records such as _ldap._tcp.example.com and _kerberos._tcp.example.com tell any client which hosts are domain controllers. This is why a Linux server must use the AD DNS servers (or servers that forward to them): without these records, it cannot even find the domain.

Kerberos and the keytab

Kerberos is AD's authentication protocol. Instead of sending passwords around, a client proves its identity to the Key Distribution Center (KDC), which runs on every DC, and receives time-limited tickets [10]. When a Linux server joins the domain, it gets its own computer account (for example SRV01$) with a long random password. The keys derived from that password are stored in /etc/krb5.keytab. The keytab is the server's identity: whoever holds it can act as that computer, so it must stay readable by root only.

LDAP and the Global Catalog

LDAP is the protocol used to read the AD database: users, groups and their attributes. The Global Catalog (port 3268) is a read-only, forest-wide copy of the most-used attributes. SSSD binds to LDAP using the machine's Kerberos identity, so no service-account password is stored on the server.

NSS — the Name Service Switch

Whenever a Linux program needs to turn a name into a number (jsmith into UID 1867601105) or the reverse, it asks the C library, which consults /etc/nsswitch.conf. A line such as passwd: files sss means: check the local /etc/passwd first, then ask SSSD. This is how AD users become visible to ls -l, id, sshd and every other program without changing those programs.

PAM — Pluggable Authentication Modules

PAM is the framework that every login program (sshd, sudo, login, the graphical login screen) calls to make security decisions. Each program has a file in /etc/pam.d/ containing a stack of modules for four types of task [6]:

Type Question Example in this design
auth Is the user who they claim to be? pam_unix (local password), pam_radius_auth (AD password + MFA)
account Is the user allowed to log in here, now? AD group check script, pam_sss (account enabled, not expired)
password How is the password changed? Left at distribution defaults
session What must be set up for the login? Home folder creation, SELinux context, limits

Each line has a control value that says what to do with the module's result. This design uses the bracket syntax, which is precise [6]:

Control Meaning
required Must succeed. If it fails, the login fails, but the remaining modules still run (so an attacker can't tell which check failed).
[success=1 default=ignore] If the module succeeds, skip the next 1 line; for any other result, ignore it and continue.
[success=done default=die] If the module succeeds, stop and return success for this phase. On any other result, stop immediately and fail.
include / substack Pull in the lines of another PAM file (such as password-auth or system-auth).

RADIUS and MFA

RADIUS is a long-established protocol (UDP port 1812) for asking a central server whether a user may log in. The client, here the Linux server, sends an Access-Request containing the username and password, protected with a shared secret known to both sides. The server replies with Access-Accept, Access-Reject, or Access-Challenge, which asks the user for more information such as a one-time code [9]. Most enterprise MFA products expose a RADIUS interface, so any device that can speak RADIUS, including a Linux server through the pam_radius_auth module, can use MFA without a vendor agent [5].

5. The software stack — what every package does

Figure 2 arranges every component by layer, with the package that provides it shown underneath. The table that follows explains each one's role.

Figure 2. The software stack, from the programs users touch down to the shared libraries

5.1 Packages you install explicitly

Package What it provides Role in this solution
realmd The realm command and a D-Bus service The orchestrator. It discovers the domain, checks which packages are needed, calls adcli to perform the join, writes sssd.conf, and switches NSS and PAM over to SSSD. You run it once [3].
adcli AD enrolment tool The join engine. It authenticates as the join account, creates the computer object in AD, sets its service principal names and a random password, and writes /etc/krb5.keytab. SSSD can also use it later to rotate the machine password [1][4].

sssd, sssd-ad

(+ sssd-common, sssd-krb5-common, sssd-ldap, sssd-common-pac)

The System Security Services Daemon and its AD back end The brain after the join. It looks up users and groups over LDAP, maps SIDs to UIDs, caches results, checks whether accounts are enabled, applies AD Group Policy logon rules, renews the machine password and registers the server in DNS [4].
sssd-client libnss_sss.so and pam_sss.so The plugs that connect NSS and PAM to the SSSD daemon. Usually already installed on the base OS.
sssd-tools sssctl and related tools Status checks and log analysis, for example sssctl domain-status.
krb5-workstation kinit, klist, kvno Kerberos client tools, mainly for testing and troubleshooting (for example klist -k to list the keytab).
oddjob, oddjob-mkhomedir The oddjobd service and pam_oddjob_mkhomedir.so Creates a user's home folder on first login. Because a privileged helper does the work (asked over D-Bus), it plays well with SELinux.
samba-common-tools The net command and Samba helper tools Listed in Red Hat's guidance for the default ID-mapping setup [1]. It also allows realmd to use Samba as an alternative join method.
pam_radius pam_radius_auth.so (from EPEL or built from the FreeRADIUS project source) The MFA bridge. It turns a PAM authentication into a RADIUS Access-Request and relays challenges (such as "Enter code") back to the user [5].
chrony The chronyd time service Keeps the clock aligned with the DCs so Kerberos tickets are accepted. Usually already installed.

5.2 Dependencies that come along

When you build the offline bundle, the package manager also pulls in libraries. You don't configure them, but it helps to know why they appear:

Library / package Why it is needed
krb5-libs The MIT Kerberos libraries used by SSSD, adcli and the Kerberos tools.
openldap (client libraries) LDAP protocol support for SSSD and adcli.
samba-client-libs, libsmbclient Used by SSSD's AD back end to download Group Policy files from the DC's SYSVOL share over SMB (port 445).
c-ares Asynchronous DNS resolution, so SSSD can look up DCs without blocking.
libtalloc, libtevent, libldb, libdhash Memory, event-loop, database and hash-table libraries that SSSD is built on.
python3 bindings (sssdconfig, ldb, talloc, tdb) Used by realmd and the Samba tools to read and write configuration.

6. Behind the scenes: joining the domain

Running realm join looks like a single command, but it triggers a precise conversation between the server and Active Directory. Figure 3 shows it as a sequence diagram: time flows downwards, and each arrow is one message.

Figure 3. What happens during realm discover and realm join

Phase 1 — Discovery

  1. realm discover example.com asks DNS for the domain's SRV records to find the domain controllers.

  2. realmd contacts a DC over LDAP to confirm it is Active Directory and to read basic details: the domain name, the Kerberos realm and the DC's name.

  3. realmd reports what it found and which required packages the join needs. On an offline server this list matters: realmd can't download missing packages itself, so they must already be installed.

Phase 2 — The join

  1. realm join asks for the join account's password and hands the work to adcli.

  2. adcli authenticates to the KDC as svc-domainjoin and receives a Kerberos ticket. The password is used only for this step and is never stored.

  3. Using that ticket, adcli creates a computer object (for example SRV01$) in the chosen organisational unit over LDAP.

  4. It fills in the object's DNS host name and service principal names (such as host/srv01.example.com), so other systems can request Kerberos tickets for this server.

  5. It generates a strong random machine password, sets it on the computer account, and writes the keys derived from it to /etc/krb5.keytab.

Phase 3 — Configure and start

  1. realmd writes /etc/sssd/sssd.conf with an [domain/example.com] section using the AD provider. Depending on the version, it may also adjust Kerberos client settings in /etc/krb5.conf.

  2. It switches NSS and PAM to SSSD using the distribution's tool: authselect on release 8 and later, authconfig on release 7 [2].

  3. It enables and starts sssd.

  4. SSSD authenticates to AD as the computer account using the keytab, and from then on can read the directory.

  5. If dynamic DNS updates are enabled, SSSD registers the server's A (and optionally PTR) record with a secure, Kerberos-signed DNS update.

What changed after the join

Where Change
Active Directory New computer object SRV01$ with SPNs and a random password; DNS records for the host (if dynamic DNS is enabled).
/etc/krb5.keytab The machine's Kerberos keys: its identity in the domain.
/etc/sssd/sssd.conf Domain configuration for SSSD (root-only, mode 0600).
/etc/nsswitch.conf and PAM system files sss added as a source for users and groups; pam_sss added to the shared PAM stacks.
Services sssd enabled and running. oddjobd enabled for home-folder creation (see Section 11).

7. Behind the scenes: how Linux sees AD users

Before anyone can log in, Linux has to be able to answer a simple question: who is jsmith? Linux identifies users by numeric UID and GID, while AD identifies them by a security identifier (SID). SSSD bridges the two. Figure 4 follows a single lookup.

Figure 4. Path of a user lookup through NSS and SSSD, with the SID-to-UID mapping
  1. A program, such as id, ls -l or sshd, asks the C library for user jsmith.

  2. The C library reads /etc/nsswitch.conf and checks local files first. Local accounts always win, which is what makes a break-glass account possible.

  3. Not found locally, so it calls libnss_sss.so.

  4. A fast memory-mapped cache answers repeated lookups almost instantly.

  5. On a miss, the request goes to SSSD's NSS responder.

  6. SSSD checks its on-disk cache. Fresh entries are returned straight away; this cache is also what keeps lookups working if the DCs are briefly unreachable.

  7. Expired or missing entries are passed to the AD back end (sssd_be).

  8. The back end queries a DC over LDAP, authenticated as the computer account, and converts the SIDs it receives into UIDs and GIDs.

ID mapping: from SID to UID without touching AD

By default SSSD's AD provider uses automatic ID mapping. A SID is made of the domain's identifier plus a relative identifier (RID) that is unique within the domain. SSSD divides the available ID range into slices of 200,000 IDs. It picks a slice for the domain by hashing the domain SID, then computes UID = slice base + RID [4].

Because the calculation is the same everywhere, every domain-joined server gives jsmith the same UID. That matters for shared storage such as NFS, and nothing has to be added to AD (no uidNumber or gidNumber attributes).

Names: short or fully qualified?

By default, realm configures use_fully_qualified_names = True, so users appear as jsmith@example.com and home folders as /home/jsmith@example.com. That avoids clashes with local accounts and with users in trusted domains. Adding default_domain_suffix = example.com to the [sssd] section lets people type just jsmith at the login prompt, while the system still uses the full name internally. Newer SSSD releases mark this option as deprecated in favour of domain_resolution_order, so check man sssd.conf on your release [4].

8. Behind the scenes: one login, step by step

This is where all the components meet. Figure 5 follows an SSH login by an AD user through the three PAM phases. Figure 6 shows the same logic as a decision flow, including the break-glass path for local accounts.

Figure 5. An SSH login by an AD user: AUTH, ACCOUNT and SESSION phases

AUTH phase — proving identity

  • sshd starts a PAM conversation (pam_authenticate) using its file /etc/pam.d/sshd. Prompts reach the user through SSH's keyboard-interactive method, which can carry several prompts in turn.

  • pam_unix goes first. If the name is a local account and the password is correct, the next line (RADIUS) is skipped. For an AD user it fails quietly and the stack moves on.

  • pam_radius_auth reuses the password already typed and sends a RADIUS Access-Request to the MFA server, which checks the password against AD.

  • If the password is valid, the server replies with an Access-Challenge ("Enter code"). pam_radius shows the prompt, collects the one-time code and sends it back. With push-style MFA, the server holds its reply until the user approves on their phone.

  • On Access-Accept, the success=done control ends the AUTH phase successfully. On reject or timeout, default=die ends it immediately with a failure.

ACCOUNT phase — checking permission

  • pam_exec runs a small access-check script with a group name: lnx-ssh-users for SSH, lnx-sudo-admins for sudo. The script refreshes the user's group membership through SSSD and allows the login only on an exact group match. Local accounts are allowed so that break-glass access still works.

  • pam_sss asks SSSD whether the AD account is enabled, not expired or locked out, and whether AD Group Policy logon rules allow access to this host.

  • The account phase runs for every login method, including SSH keys, which skip the AUTH phase entirely. Group-based access control therefore can't be bypassed with a key.

SESSION phase — preparing the environment

  • pam_oddjob_mkhomedir asks oddjobd over D-Bus to create the home folder (for example /home/jsmith@example.com) if it doesn't exist yet, with correct ownership and SELinux labels.

  • The usual session modules (SELinux context, limits, login records) run, and sshd starts the user's shell.

Figure 6. PAM decision flow used for both SSH and sudo

Why RADIUS checks the password, not SSSD

In this design the RADIUS server validates both the AD password and the second factor, in one conversation. SSSD is still essential, but for identity (NSS), account status and home folders rather than password checks. The alternative, where SSSD checks the password and RADIUS checks only the code, is also valid. The single-check approach was chosen because it keeps all authentication policy (lockout, MFA enrolment, conditional rules, logging) in one place. Section 10 covers the trade-offs.

The same pattern for sudo

sudo has its own PAM file, /etc/pam.d/sudo. It uses the same two AUTH lines and the same access-check script, but with the sudo group. Together with a sudoers rule for that group, this means elevating to root needs the AD password, a second factor and membership of lnx-sudo-admins. By default sudo caches successful authentication for 5 minutes per terminal, so engineers aren't challenged for every command.

9. Behind the scenes: keeping the join healthy

Once joined, SSSD quietly maintains the relationship with AD (defaults from the SSSD manual pages [4]):

Background task What happens Default
Machine password rotation SSSD checks daily whether the computer account password is older than the maximum age, and if so renews it and updates the keytab. 30 days (ad_maximum_machine_account_password_age)
Dynamic DNS SSSD updates the host's DNS record when it comes online and then periodically, using secure (GSS-TSIG) updates. Every 24 hours (dyndns_refresh_interval)
Cache expiry User and group entries are re-fetched from AD after they expire. 5,400 seconds (entry_cache_timeout)
Login-time refresh During PAM requests, SSSD refreshes the user's identity information while online, so group changes take effect at the next login. Cached for 5 seconds (pam_id_timeout)
Group Policy access control SSSD downloads applicable GPOs from SYSVOL and applies logon-right settings to decide whether a user may log in. Enforcing (ad_gpo_access_control)
Time chronyd continuously disciplines the clock against the DCs. —

10. Design decisions and security considerations

Availability

  • RADIUS is now critical infrastructure. If no RADIUS server answers, AD users can't log in. Configure at least two RADIUS servers; pam_radius_auth tries them in order and moves on when one doesn't respond.

  • Keep a break-glass account. A local account with a long random password, stored in a password vault and used only in emergencies, still works when AD or RADIUS is down. Restrict and monitor it.

  • SSSD's offline credential cache doesn't help here. Because SSSD doesn't check AD passwords in this design, cached credentials are not used for logins.

  • Set the RADIUS timeout for push MFA. The timeout must be longer than the time a user needs to approve a push notification. pam_radius accepts 3 to 60 seconds [5].

Closing the bypass routes

  • SSH keys and Kerberos (GSSAPI) logins skip the AUTH phase, and therefore skip MFA. If MFA must apply to every SSH login, set AuthenticationMethods keyboard-interactive, or publickey,keyboard-interactive to require a key and MFA. Note that in sshd_config a comma means both, while a space means either [8].

  • Turn off the plain password SSH method (PasswordAuthentication no). It can carry only one prompt, so it can't relay an MFA challenge. keyboard-interactive can.

  • Disable direct root login over SSH (PermitRootLogin no), so privileged work goes through sudo and its MFA.

Protecting secrets and traffic

  • The keytab (/etc/krb5.keytab) and the RADIUS configuration (which holds the shared secret) must be owned by root with mode 0600.

  • Use a long, random RADIUS shared secret, ideally unique per server or per server group.

  • RADIUS hides the password using the shared secret, which is not modern encryption. Keep RADIUS traffic on a trusted management network.

  • Recent pam_radius versions support require_message_authenticator, which rejects responses without a Message-Authenticator attribute. Enable it when your RADIUS server supports it; it protects against the 2024 "Blast-RADIUS" forgery attack (CVE-2024-3596) [5][13].

  • By default pam_radius echoes what the user types at a challenge prompt. If your RADIUS server sends the Prompt attribute, add prompt_attribute so typed codes are hidden.

Access control and least privilege

  • Grant access with groups, never individual users, so access reviews happen in AD.

  • Use separate groups for login and sudo. Not everyone who needs to log in needs root.

  • Delegate the join account rights on one OU only, and use a dedicated OU for Linux computer objects.

  • Log allow/deny decisions to syslog (the access-check script below does this), so they land in /var/log/secure and can be forwarded to a SIEM.

11. Configuration guide (offline installation)

The steps below put everything above into practice. Run them as root on the Linux server unless stated otherwise. Keep a second root session open while you change PAM or SSH settings, so a mistake can't lock you out.

11.1 Before you start — prerequisites checklist

# Prerequisite Detail
1 Information Domain FQDN and realm; DC IP addresses; target OU; RADIUS server names and shared secret; group names.
2 AD groups Create lnx-ssh-users and lnx-sudo-admins and add members.
3 Join account svc-domainjoin with delegated rights to create computer objects in the target OU.
4 RADIUS client entry On the MFA server, register the Linux server's IP address as a RADIUS client with the shared secret.
5 Hostname Fully qualified (srv01.example.com). Keep the short name to 15 characters or fewer, the limit for AD computer account (NetBIOS) names [12].
6 Staging machine A machine with repository access running the same OS release and architecture as the target (used to build the offline bundle).

11.2 Network ports

Open these from the Linux server to the destinations listed (based on Red Hat's guidance for direct AD integration [1]):

Service Port / protocol Destination Purpose
DNS 53 TCP/UDP DCs Find DCs (SRV records); dynamic DNS updates
LDAP 389 TCP/UDP DCs Directory lookups; DC discovery
Global Catalog 3268 TCP DCs Required with id_provider = ad
Kerberos 88 TCP/UDP DCs Authentication tickets
Kerberos password 464 TCP/UDP DCs Setting and rotating the machine password
SMB 445 TCP DCs Downloading Group Policy (GPO access control)
NTP 123 UDP DCs / time source Clock synchronisation
RADIUS 1812 UDP MFA servers Authentication requests
SSH (inbound) 22 TCP From admin networks Remote login

11.3 Step 1 — Build the offline bundle (on the staging machine)

Figure 7. Building and transferring an offline installation bundle

On the connected staging machine (same release as the target), enable EPEL [15] for pam_radius: install epel-release on Rocky/AlmaLinux, or the EPEL release package on RHEL. Then download the packages with their dependencies into a folder and turn it into a repository:

Staging machine
dnf install -y dnf-plugins-core createrepo_c
mkdir -p /opt/adjoin-bundle/repo
 
# Download the packages AND every dependency (--alldeps ignores what is
# already installed on the staging machine, so nothing is missed)
dnf download --resolve --alldeps --destdir /opt/adjoin-bundle/repo \
    realmd adcli sssd sssd-ad sssd-client sssd-tools oddjob oddjob-mkhomedir \
    krb5-workstation samba-common-tools chrony pam_radius
 
# Build repository metadata and include the EPEL signing key
createrepo_c /opt/adjoin-bundle/repo
cp /etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-* /opt/adjoin-bundle/
 
# Package it with a checksum
cd /opt && tar czf adjoin-bundle.tar.gz adjoin-bundle
sha256sum adjoin-bundle.tar.gz > adjoin-bundle.tar.gz.sha256

11.4 Step 2 — Install from the local repository (on the target)

Copy both files to the target using your approved transfer method, then:

Target server
sha256sum -c adjoin-bundle.tar.gz.sha256
tar xzf adjoin-bundle.tar.gz -C /opt
rpm --import /opt/adjoin-bundle/RPM-GPG-KEY-EPEL-*
 
cat > /etc/yum.repos.d/adjoin-local.repo <<'EOF'
[adjoin-local]
name=Local AD join bundle
baseurl=file:///opt/adjoin-bundle/repo
enabled=0
gpgcheck=1
EOF
 
dnf --disablerepo='*' --enablerepo=adjoin-local install -y \
    realmd adcli sssd sssd-ad sssd-client sssd-tools oddjob oddjob-mkhomedir \
    krb5-workstation samba-common-tools chrony pam_radius
 
ls -l /usr/lib64/security/pam_radius_auth.so     # confirm the MFA module is present

The checksum confirms the bundle arrived intact. gpgcheck=1 confirms each package is genuinely signed by the distribution or by EPEL. The repository is left disabled (enabled=0) so it doesn't interfere with normal updates.

11.5 Step 3 — Hostname and time

hostnamectl set-hostname srv01.example.com
 
# /etc/chrony.conf — replace the default pool/server lines with your DCs
server dc1.example.com iburst
server dc2.example.com iburst
 
systemctl enable --now chronyd
systemctl restart chronyd
chronyc sources -v          # the DCs should be listed, one marked with '*'

11.6 Step 4 — DNS pointing at the domain controllers

Set DNS on the network connection rather than editing /etc/resolv.conf directly; NetworkManager would overwrite manual edits.

nmcli con show                                     # find the connection name
nmcli con mod "<connection>" ipv4.dns "10.0.0.10 10.0.0.11" \
      ipv4.dns-search example.com ipv4.ignore-auto-dns yes
nmcli con up "<connection>"
 
host -t SRV _ldap._tcp.example.com                 # must list the DCs
host -t SRV _kerberos._tcp.example.com

11.7 Step 5 — Discover and join

realm discover example.com
#   check: type: kerberos, server-software: active-directory,
#          client-software: sssd, and every required-package is installed
 
realm join --user=svc-domainjoin \
      --computer-ou="OU=Linux,OU=Servers,DC=example,DC=com" example.com
 
realm list                          # shows the domain as configured
adcli testjoin                      # verifies the computer account and keytab
klist -k /etc/krb5.keytab           # lists SRV01$ and host/ principals
id jsmith@example.com               # an AD user resolves through SSSD

11.8 Step 6 — Tune SSSD

Edit /etc/sssd/sssd.conf. Lines written by realm are kept; the additions are marked.

/etc/sssd/sssd.conf
[sssd]
domains = example.com
config_file_version = 2
services = nss, pam
default_domain_suffix = example.com          # added: users can type just "jsmith"
 
[domain/example.com]
ad_domain = example.com
krb5_realm = EXAMPLE.COM
realmd_tags = manages-system joined-with-adcli
cache_credentials = True
id_provider = ad
krb5_store_password_if_offline = True
default_shell = /bin/bash
ldap_id_mapping = True
use_fully_qualified_names = True
fallback_homedir = /home/%u@%d
access_provider = ad
# added: register this host in AD DNS
ad_hostname = srv01.example.com
dyndns_update = true
dyndns_update_ptr = true
chmod 600 /etc/sssd/sssd.conf
sssctl config-check
systemctl restart sssd
id jsmith                                    # now works with the short name

11.9 Step 7 — Automatic home folders

# Release 8 / 9
authselect current                           # shows the active profile (sssd)
authselect enable-feature with-mkhomedir
systemctl enable --now oddjobd
 
# Release 7 equivalent
# authconfig --enablemkhomedir --update && systemctl enable --now oddjobd

11.10 Step 8 — RADIUS client configuration

Create the RADIUS server list. Each line is server[:port] shared-secret timeout-in-seconds; servers are tried in order.

/etc/pam_radius_auth.conf
# /etc/pam_radius_auth.conf
mfa1.example.com:1812    <long-random-shared-secret>    30
mfa2.example.com:1812    <long-random-shared-secret>    30
chown root:root /etc/pam_radius_auth.conf
chmod 600 /etc/pam_radius_auth.conf

On the RADIUS/MFA server, confirm that the Linux server is registered as a client with the same secret, and that the username format users type (jsmith or jsmith@example.com) is accepted. pam_radius sends the PAM service name (sshd, sudo) as the NAS-Identifier, which you can use in RADIUS policies.

11.11 Step 9 — The AD group access-check script

/usr/local/sbin/ad-group-gate.sh
#!/bin/bash
# /usr/local/sbin/ad-group-gate.sh
# PAM account-phase gate. Called as:
#   pam_exec.so quiet /usr/local/sbin/ad-group-gate.sh <group>
GROUP="$1"
DOMAIN="example.com"
USER_NAME="$PAM_USER"
 
if [ -z "$GROUP" ] || [ -z "$USER_NAME" ]; then
    exit 1
fi
 
# Break-glass: accounts defined in local files are always allowed
if getent -s files passwd "$USER_NAME" >/dev/null; then
    exit 0
fi
 
# Force a fresh lookup of this user's groups, then require an exact match
/usr/sbin/sss_cache -u "$USER_NAME" >/dev/null 2>&1
if id -nG "$USER_NAME" 2>/dev/null | tr ' ' '\n' \
     | grep -Fxqi -e "$GROUP" -e "$GROUP@$DOMAIN"; then
    logger -p authpriv.notice -t ad-group-gate \
        "ALLOW user=$USER_NAME group=$GROUP service=$PAM_SERVICE rhost=${PAM_RHOST:-local}"
    exit 0
fi
 
logger -p authpriv.warning -t ad-group-gate \
    "DENY user=$USER_NAME group=$GROUP service=$PAM_SERVICE rhost=${PAM_RHOST:-local}"
exit 1
chown root:root /usr/local/sbin/ad-group-gate.sh
chmod 755 /usr/local/sbin/ad-group-gate.sh

11.12 Step 10 — PAM for SSH

Edit /etc/pam.d/sshd. Insert the two AUTH lines above the existing auth substack password-auth line, and the access-check line as the first account line. Leave everything else exactly as the distribution shipped it; the shipped lines vary slightly between releases. The result looks similar to this:

/etc/pam.d/sshd
#%PAM-1.0
# --- added: local accounts first, then RADIUS (AD password + MFA) ---
auth       [success=1 default=ignore]     pam_unix.so
auth       [success=done default=die]     pam_radius_auth.so conf=/etc/pam_radius_auth.conf
# --- distribution lines (unchanged) ---
auth       substack     password-auth
auth       include      postlogin
# --- added: AD group gate ---
account    required     pam_exec.so quiet /usr/local/sbin/ad-group-gate.sh lnx-ssh-users
# --- distribution lines (unchanged) ---
account    required     pam_sepermit.so
account    required     pam_nologin.so
account    include      password-auth
password   include      password-auth
session    required     pam_selinux.so close
session    required     pam_loginuid.so
session    required     pam_selinux.so open env_params
session    required     pam_namespace.so
session    optional     pam_keyinit.so force revoke
session    optional     pam_motd.so
session    include      password-auth
session    include      postlogin
Line What it does
pam_unix [success=1 default=ignore] Local account with the right password: skip the RADIUS line and continue into password-auth, where pam_unix reuses the password already entered. Anyone else: ignore and move on.
pam_radius_auth [success=done default=die] AD user: the password typed above is reused (pam_radius default try_first_pass), sent to RADIUS, and any MFA challenge is relayed. Accept ends AUTH with success; anything else ends it with failure.
pam_exec … lnx-ssh-users Runs the gate script. A non-zero exit code denies the login, even for SSH-key logins.

11.13 Step 11 — SSH daemon settings

On release 9, create a drop-in file. sshd uses the first value it reads for each setting, and drop-ins are read in name order, so a name starting with 40- takes precedence over the distribution's 50-redhat.conf [8]. On release 8, put the same settings in /etc/ssh/sshd_config itself and use ChallengeResponseAuthentication yes in place of KbdInteractiveAuthentication yes.

/etc/ssh/sshd_config.d/40-ad-mfa.conf
# /etc/ssh/sshd_config.d/40-ad-mfa.conf
UsePAM yes
KbdInteractiveAuthentication yes       # carries password + MFA prompts
PasswordAuthentication no              # single-prompt method; cannot relay MFA
PermitRootLogin no
AuthenticationMethods keyboard-interactive
# or, to require an SSH key AND password+MFA:
# AuthenticationMethods publickey,keyboard-interactive
sshd -t                                              # syntax check — must print nothing
sshd -T | grep -Ei 'kbdinteractive|passwordauth|authenticationmethods'
systemctl reload sshd

11.14 Step 12 — PAM and sudoers for sudo

/etc/pam.d/sudo
#%PAM-1.0
auth       [success=1 default=ignore]     pam_unix.so
auth       [success=done default=die]     pam_radius_auth.so conf=/etc/pam_radius_auth.conf
auth       include      system-auth
account    required     pam_exec.so quiet /usr/local/sbin/ad-group-gate.sh lnx-sudo-admins
account    include      system-auth
password   include      system-auth
session    include      system-auth
Grant sudo to the AD group
echo '%lnx-sudo-admins@example.com  ALL=(ALL)  ALL' > /etc/sudoers.d/50-ad-admins
chmod 440 /etc/sudoers.d/50-ad-admins
visudo -c                                            # validate all sudoers files

12. Testing, troubleshooting, conclusion and references

12.1 Test plan

Test from a second terminal while your original root session stays open.

# Test Expected result
1 ssh jsmith@srv01 as a member of lnx-ssh-users Password prompt, then MFA prompt or push; shell opens; home folder created on first login
2 SSH as an AD user not in lnx-ssh-users Denied after MFA; DENY entry from ad-group-gate in /var/log/secure
3 SSH with the wrong password or a wrong code Denied; Access-Reject recorded on the RADIUS server
4 sudo -k; sudo -v as a member of lnx-sudo-admins Password and MFA requested; sudo succeeds
5 sudo as an AD user not in lnx-sudo-admins Denied
6 Log in with the break-glass local account Succeeds with the local password only (no MFA)
7 Stop RADIUS temporarily (or block UDP 1812) AD logins fail after the timeout; break-glass account still works
8 Disable the AD account, then try again Denied at the account phase

12.2 Troubleshooting

Symptom Likely cause How to check / fix
realm discover finds nothing DNS not pointing at AD, or ports blocked host -t SRV _ldap._tcp.example.com; check DNS settings and firewall
Join fails with "Clock skew too great" Time not synchronised chronyc tracking; fix NTP and retry
Join fails with insufficient rights Join account lacks OU delegation Grant create/delete computer objects on the target OU
id jsmith says "no such user" SSSD not online, or naming settings sssctl domain-status example.com; logs in /var/log/sssd/; try id jsmith@example.com
MFA prompt never appears over SSH Client is using the password method sshd -T should show kbdinteractiveauthentication yes and passwordauthentication no
RADIUS timeouts Firewall, wrong secret, server not registered, or timeout too short for push Check UDP 1812; RADIUS server logs; add debug to the pam_radius line temporarily
Authenticated but denied Group gate or account check failed journalctl -t ad-group-gate; id -nG jsmith; SSSD domain log for GPO denials
Denied by Group Policy A GPO logon right on the OU excludes the user SSSD log shows the GPO decision; temporarily set ad_gpo_access_control = permissive to confirm, then fix the GPO
Script blocked under SELinux Policy denies an action of the gate script ausearch -m avc -ts recent; adjust policy rather than disabling SELinux
No home folder created oddjobd not running or feature not enabled systemctl status oddjobd; authselect current

12.3 Files touched — quick reference

File Purpose Mode
/etc/krb5.keytab Machine identity (created by the join) 0600
/etc/sssd/sssd.conf SSSD domain configuration 0600
/etc/pam_radius_auth.conf RADIUS servers and shared secret 0600
/usr/local/sbin/ad-group-gate.sh AD group access check 0755
/etc/pam.d/sshd, /etc/pam.d/sudo Login rules for SSH and sudo 0644
/etc/ssh/sshd_config.d/40-ad-mfa.conf SSH authentication methods 0600
/etc/sudoers.d/50-ad-admins sudo rights for the AD admin group 0440
/etc/yum.repos.d/adjoin-local.repo Offline repository definition 0644

12.4 Conclusion

Joining Linux to Active Directory comes down to a handful of standard components, each with one clear job. realmd and adcli establish trust once and leave the server with its own identity in the keytab. SSSD turns AD users and groups into Linux users, with consistent UIDs, caching and account checks. PAM ties it all together at login time, and pam_radius extends it to any RADIUS-capable MFA service, so a Linux login gets the same second factor as the rest of the organisation.

Understanding the flow — DNS to find the domain, Kerberos to prove identity, LDAP to read it, NSS to expose it, PAM to enforce it — makes the setup repeatable and much easier to troubleshoot. With an offline bundle, a short list of configuration files and a clear test plan, the same pattern can be rolled out consistently to air-gapped servers.

12.5 References

Numbers in square brackets in the text refer to these sources.

[1] Red Hat. Integrating RHEL systems directly with Windows Active Directory (RHEL 9) — Connecting RHEL systems directly to AD using SSSD. https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/9/html/integrating_rhel_systems_directly_with_windows_active_directory/connecting-rhel-systems-directly-to-ad-using-sssd_integrating-rhel-systems-directly-with-active-directory

[2] Red Hat. Configuring authentication and authorization in RHEL 9 — Configuring user authentication using authselect. https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/9/html/configuring_authentication_and_authorization_in_rhel/configuring-user-authentication-using-authselect_configuring-authentication-and-authorization-in-rhel

[3] realmd project. realm — Manage enrollment in realms. https://www.freedesktop.org/software/realmd/docs/realm.html

[4] SSSD project. System Security Services Daemon — documentation and manual pages (sssd.conf(5), sssd-ad(5), sssd-ldap(5) ID mapping). https://sssd.io/ · https://github.com/SSSD/sssd

[5] FreeRADIUS project. pam_radius — RADIUS PAM module (README and USAGE). https://github.com/FreeRADIUS/pam_radius

[6] Linux-PAM. pam.conf(5) / pam.d(5) — PAM configuration files and control values. https://man7.org/linux/man-pages/man5/pam.d.5.html

[7] Linux-PAM. pam_exec(8) — PAM module which calls an external command. https://man7.org/linux/man-pages/man8/pam_exec.8.html

[8] OpenBSD / OpenSSH. sshd_config(5) — OpenSSH daemon configuration file. https://man.openbsd.org/sshd_config

[9] Rigney, C. et al. RFC 2865 — Remote Authentication Dial In User Service (RADIUS). IETF, 2000. https://www.rfc-editor.org/rfc/rfc2865

[10] Neuman, C. et al. RFC 4120 — The Kerberos Network Authentication Service (V5). IETF, 2005. https://www.rfc-editor.org/rfc/rfc4120

[11] Microsoft Learn. Maximum tolerance for computer clock synchronization. https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-10/security/threat-protection/security-policy-settings/maximum-tolerance-for-computer-clock-synchronization

[12] Microsoft Learn. Naming conventions in Active Directory for computers, domains, sites, and OUs. https://learn.microsoft.com/en-us/troubleshoot/windows-server/active-directory/naming-conventions-for-computer-domain-site-ou

[13] NIST National Vulnerability Database. CVE-2024-3596 (Blast-RADIUS). https://nvd.nist.gov/vuln/detail/CVE-2024-3596

[14] dnf-plugins-core. DNF download plugin (--resolve, --alldeps). https://dnf-plugins-core.readthedocs.io/en/latest/download.html

[15] Fedora Project. EPEL — Extra Packages for Enterprise Linux. https://docs.fedoraproject.org/en-US/epel/

All host names, addresses, group names and secrets in this article are illustrative examples. Validate every configuration in a test environment before applying it to production systems.

share this note

LinkedIn WhatsApp X Email