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.
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.
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.
Phase 1 — Discovery
realm discover example.comasks DNS for the domain's SRV records to find the domain controllers.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.
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
realm joinasks for the join account's password and hands the work to adcli.adcli authenticates to the KDC as
svc-domainjoinand receives a Kerberos ticket. The password is used only for this step and is never stored.Using that ticket, adcli creates a computer object (for example
SRV01$) in the chosen organisational unit over LDAP.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.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
realmd writes
/etc/sssd/sssd.confwith an[domain/example.com]section using the AD provider. Depending on the version, it may also adjust Kerberos client settings in/etc/krb5.conf.It switches NSS and PAM to SSSD using the distribution's tool:
authselecton release 8 and later,authconfigon release 7 [2].It enables and starts
sssd.SSSD authenticates to AD as the computer account using the keytab, and from then on can read the directory.
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.
A program, such as
id,ls -lorsshd, asks the C library for userjsmith.The C library reads
/etc/nsswitch.confand checks local files first. Local accounts always win, which is what makes a break-glass account possible.Not found locally, so it calls
libnss_sss.so.A fast memory-mapped cache answers repeated lookups almost instantly.
On a miss, the request goes to SSSD's NSS responder.
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.
Expired or missing entries are passed to the AD back end (
sssd_be).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.
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=donecontrol ends the AUTH phase successfully. On reject or timeout,default=dieends it immediately with a failure.
ACCOUNT phase — checking permission
pam_exec runs a small access-check script with a group name:
lnx-ssh-usersfor SSH,lnx-sudo-adminsfor 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
oddjobdover 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.
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_authtries 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, orpublickey,keyboard-interactiveto require a key and MFA. Note that insshd_configa comma means both, while a space means either [8].Turn off the plain
passwordSSH method (PasswordAuthentication no). It can carry only one prompt, so it can't relay an MFA challenge.keyboard-interactivecan.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 mode0600.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_attributeso 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/secureand 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)
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:
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.sha25611.4 Step 2 — Install from the local repository (on the target)
Copy both files to the target using your approved transfer method, then:
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 presentThe 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.com11.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 SSSD11.8 Step 6 — Tune SSSD
Edit /etc/sssd/sssd.conf. Lines written by realm are kept; the additions are marked.
[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 name11.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 oddjobd11.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
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.confOn 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
#!/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.sh11.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:
#%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
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 sshd11.14 Step 12 — PAM and sudoers for 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-authecho '%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 files12. 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.