Linux and Identity
Bringing Linux into a central identity system: SSSD and realmd to join Active Directory, Kerberos and LDAP on Linux, sudo rules from the directory, FreeIPA, and SSH with central accounts.
Why centralize Linux accounts
A stand-alone Linux server keeps its accounts in local files:
/etc/passwd for the account,
/etc/shadow for the password hash and
/etc/group for groups (see
Users, Groups and Permissions). That works for one machine. With fifty, it falls apart:
- Leavers. When an engineer leaves, someone must remove the account on every server. One forgotten server is an orphaned account with a valid password (see Identity Lifecycle).
- Passwords. Each server has its own copy of each password, with its own age and no shared lockout or MFA (multi-factor authentication).
-
UIDs. A user ID (UID) is just a number. If
tnowakis 1003 on one server and 1007 on another, files on a shared NFS (Network File System) export end up owned by the wrong person. - Audit. "Who has root on which servers?" becomes a script that reads fifty sudoers files instead of one group membership.
The fix is the same as for Windows: keep accounts, groups and
passwords in one directory (see
Directory Services and LDAP) and let every server ask it. In most organizations that directory
is Active Directory (AD), because the Windows clients are already
there; Linux-only shops often use FreeIPA. Linux still keeps local
accounts for root and system services, and should: when
the directory is down you still need a way in.
SSSD: the client side of the directory
Linux programs do not talk to a directory themselves. When
ls -l needs the name for UID 1594201104, it asks the C
library through NSS (the Name Service Switch, configured in
/etc/nsswitch.conf). When sshd or the
console login checks a password, it goes through PAM (Pluggable
Authentication Modules). Both are plug-in systems, and something has
to plug the directory in.
SSSD (System Security Services Daemon) is that something on most current distributions. It is a client-side service that fetches users, groups and authentication from a remote identity provider and hands them to NSS and PAM. Its main parts:
-
Providers talk to the back end:
ad(Active Directory),ipa(FreeIPA),ldap,krb5, anidpprovider for OAuth 2.0 / OpenID Connect identity providers, and aproxyprovider for older setups. -
Responders answer local programs:
nssfor name lookups,pamfor logins, and optional ones such assudo(sudo rules stored in the directory) andssh(public keys stored in the directory). -
A cache. Every user and group SSSD looks up is
cached, which keeps lookups fast and the domain controllers quiet.
With
cache_credentials = True, SSSD also keeps a hash of the password of users who logged in successfully before, so they can still log in when the server cannot reach any domain controller — a laptop on a train, or a data center link that is down.
SSSD reads one configuration file, /etc/sssd/sssd.conf,
which must be owned by root with mode 600 or SSSD refuses to start.
A file written by realm join looks like this:
[sssd] domains = ad.example.com services = nss, pam [domain/ad.example.com] id_provider = ad access_provider = ad ad_domain = ad.example.com krb5_realm = AD.EXAMPLE.COM realmd_tags = manages-system joined-with-adcli cache_credentials = True krb5_store_password_if_offline = True ldap_id_mapping = True use_fully_qualified_names = True fallback_homedir = /home/%u@%d default_shell = /bin/bash
ldap_id_mapping = True means SSSD calculates each UID
and group ID (GID) from the AD security identifier (SID), so every
server that uses the same algorithm gets the same numbers without
storing anything in AD. The alternative is to store POSIX attributes
(uidNumber, gidNumber) in AD itself, which
you need when some systems are not using SSSD. %u and
%d in fallback_homedir become the user
name and the domain.
The example below reads such a file with Python's
configparser and reports what matters during a review.
Note interpolation=None: without it,
configparser treats %u as its own
placeholder syntax and fails.
import configparser
SSSD_CONF = """
[sssd]
domains = ad.example.com
services = nss, pam
[domain/ad.example.com]
id_provider = ad
access_provider = simple
simple_allow_groups = linux-admins@ad.example.com, web-team@ad.example.com
ad_domain = ad.example.com
krb5_realm = AD.EXAMPLE.COM
cache_credentials = False
use_fully_qualified_names = True
fallback_homedir = /home/%u@%d
"""
# interpolation=None: otherwise configparser treats %u and %d as its own syntax
config = configparser.ConfigParser(interpolation=None)
config.read_string(SSSD_CONF)
for domain in config["sssd"]["domains"].split(","):
section = config[f"domain/{domain.strip()}"]
print(f"Domain: {domain.strip()}")
print(f"Realm: {section.get('krb5_realm', '(not set)')}")
print(f"Identity from: {section.get('id_provider')}")
print(f"Login access: {section.get('access_provider', 'permit (default)')}")
for group in section.get("simple_allow_groups", "").split(","):
if group.strip():
print(f" - {group.strip()}")
print(f"Home folder: {section.get('fallback_homedir')}")
warnings = []
if not section.getboolean("cache_credentials", fallback=False):
warnings.append("cache_credentials is off: no offline login when the DCs are unreachable")
if section.get("access_provider", "permit") == "permit":
warnings.append("access_provider is permit: every account in the domain may log in")
for warning in warnings:
print(f"WARNING: {warning}")
Domain: ad.example.com Realm: AD.EXAMPLE.COM Identity from: ad Login access: simple - linux-admins@ad.example.com - web-team@ad.example.com Home folder: /home/%u@%d WARNING: cache_credentials is off: no offline login when the DCs are unreachable
The second check matters more than it looks. With
access_provider = permit, every enabled account in the
whole domain — including the receptionist's — can log in to the
server by SSH. Restricting logins to named groups is the Linux side
of least privilege.
Joining a Linux host to Active Directory
Joining by hand means creating a computer account in AD, generating
a keytab, and editing krb5.conf,
sssd.conf, nsswitch.conf and the PAM stack
consistently. realmd does all of that
with one command. It discovers what kind of domain a name points to,
installs or lists the packages it needs, and joins, usually with
adcli (a small tool that creates the computer account
in AD over LDAP and Kerberos) and with SSSD as the client. These
commands are not run on this site; the output is illustrative.
realm discover ad.example.com
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
Discovery uses DNS: it looks up the SRV records that every AD domain
publishes (see
DNS and Active Directory). If realm discover finds nothing, the Linux host is
almost always using a DNS server that does not know the AD zone.
Then join, with an account that has been delegated the right to
create computer objects in one organizational unit (OU) — not a
Domain Admin:
sudo realm join --user=linux-joiner \
--computer-ou="OU=Linux Servers,OU=Servers,DC=ad,DC=example,DC=com" ad.example.com
id tnowak@ad.example.com
Password for linux-joiner: uid=1594201104(tnowak@ad.example.com) gid=1594200513(domain users@ad.example.com) groups=1594200513(domain users@ad.example.com),1594201110(linux-admins@ad.example.com)
The id output proves that NSS and SSSD work: the user
and their AD groups resolve on Linux. The numbers are the ID-mapped
values described above, and the names come back in lower case. To
limit who may log in:
sudo realm deny --all
sudo realm permit -g linux-admins@ad.example.com web-team@ad.example.com
This switches SSSD to the simple access provider with
an allow list of groups. With
access_provider = ad instead, SSSD can also apply the
logon rights that AD Group Policy assigns to the computer (see
Group Policy) — which surprises
people when a policy meant for Windows servers suddenly blocks Linux
logins.
Kerberos and LDAP on Linux
Under the hood, an AD-joined Linux host speaks the same two protocols as Windows: Kerberos to prove who someone is, and LDAP to look up users and groups. The details of Kerberos itself are on Kerberos and NTLM; this section covers the Linux tools.
kinit, klist and krb5.conf
/etc/krb5.conf tells the Kerberos libraries which realm
is the default and how to find its key distribution centers. A
Kerberos realm name is the AD domain name in upper case:
[libdefaults]
default_realm = AD.EXAMPLE.COM
dns_lookup_kdc = true
dns_lookup_realm = false
rdns = false
kinit asks the key distribution center for a
ticket-granting ticket; klist shows the tickets in your
cache. After a normal SSSD login you already have one, so
klist is the quickest test of "does Kerberos work for
this user?":
kinit tnowak@AD.EXAMPLE.COM
klist
Password for tnowak@AD.EXAMPLE.COM: Ticket cache: KCM:1594201104 Default principal: tnowak@AD.EXAMPLE.COM Valid starting Expires Service principal 10/01/2026 09:00:12 10/01/2026 19:00:12 krbtgt/AD.EXAMPLE.COM@AD.EXAMPLE.COM renew until 10/08/2026 09:00:08
The ticket is valid for 10 hours, the default maximum
ticket-granting ticket lifetime in an AD domain.
kdestroy throws the cache away.
Keytabs
A person types a password; a server cannot. A
keytab
(key table) is a file that holds the long-term Kerberos keys of one
or more principals, so a service or a host can authenticate without
anyone typing anything.
realm join writes /etc/krb5.keytab with
the keys of the new computer account; SSSD uses it to authenticate
the host to AD, and services such as sshd use it to
accept Kerberos tickets from users.
sudo klist -k /etc/krb5.keytab
Keytab name: FILE:/etc/krb5.keytab KVNO Principal ---- -------------------------------------------------------------------------- 2 LNX-WEB01$@AD.EXAMPLE.COM 2 host/LNX-WEB01@AD.EXAMPLE.COM 2 host/lnx-web01.ad.example.com@AD.EXAMPLE.COM
KVNO is the key version number; it goes up each time the password of the account changes. A keytab is equivalent to a password: whoever copies it can act as that computer or service. Keep it readable by root only (mode 600), never put it in a Git repository or a container image, and when a host is rebuilt, rejoin it instead of copying the old keytab.
LDAP on Linux
SSSD does its own LDAP lookups, so you rarely need an LDAP client.
For troubleshooting,
ldapsearch (from the OpenLDAP client tools) can query
AD directly, using your Kerberos ticket instead of a password (-Y GSSAPI). The filter syntax and distinguished names are explained on
Directory Services and LDAP.
ldapsearch -H ldap://dc01.ad.example.com -Y GSSAPI \
-b "DC=ad,DC=example,DC=com" "(sAMAccountName=tnowak)" memberOf
SASL/GSSAPI authentication started SASL username: tnowak@AD.EXAMPLE.COM dn: CN=Tomasz Nowak,OU=NOC,OU=Staff,DC=ad,DC=example,DC=com memberOf: CN=linux-admins,OU=Groups,DC=ad,DC=example,DC=com memberOf: CN=noc-engineers,OU=Groups,DC=ad,DC=example,DC=com
Because the bind uses Kerberos, no password crosses the network and the session can be signed — which matters because domain controllers on Windows Server 2025 require LDAP signing by default on new deployments.
Sudo rules from directory groups
The sudoers syntax (see
sudo and the sudoers file) accepts a group with %, and once SSSD resolves AD
groups, that includes AD groups. A drop-in file per role keeps it
readable:
# Linux administrators: full root %linux-admins@ad.example.com ALL=(ALL) ALL # Web team: only restart the web server %web-team@ad.example.com ALL=(root) /usr/bin/systemctl restart nginx # A group name with a space needs a backslash %linux\ server\ admins@ad.example.com ALL=(ALL) ALL
Now access is managed where it belongs: the identity team adds
someone to
web-team in AD, and within the SSSD cache timeout they
can restart nginx on every web server. Removing them from the group
removes the right everywhere. Edit the file with
sudo visudo -f /etc/sudoers.d/ad-groups, which checks
the syntax before saving; a broken sudoers file can lock everyone
out of root.
Two alternatives exist. FreeIPA stores sudo rules centrally in the
directory, and SSSD's sudo responder delivers them to
each host, so no host has a sudoers file to maintain. Configuration
management (Ansible, Puppet) can push the same drop-in file to every
server. Either way, the rule is written once.
The matching itself is simple, but case and name format trip people up. This model shows which rules apply to which users, comparing names case-insensitively as SSSD does with AD by default:
# Sudo rules keyed by directory group (as written in /etc/sudoers.d/)
SUDO_RULES = {
"linux-admins@ad.example.com": "ALL=(ALL) ALL",
"web-team@ad.example.com": "ALL=(root) /usr/bin/systemctl restart nginx",
"dba-team@ad.example.com": "ALL=(postgres) ALL",
}
def sudo_rules_for(user, groups):
"""Return the sudo rules that apply to a user, given the groups from `id`."""
# SSSD returns AD names in lower case; compare case-insensitively
member_of = {g.lower() for g in groups}
return [(group, rule) for group, rule in sorted(SUDO_RULES.items())
if group.lower() in member_of]
users = {
"tnowak@ad.example.com": ["Domain Users@ad.example.com", "Linux-Admins@ad.example.com"],
"rkumar@ad.example.com": ["domain users@ad.example.com", "web-team@ad.example.com"],
"lromero@ad.example.com": ["domain users@ad.example.com"],
}
for user, groups in users.items():
rules = sudo_rules_for(user, groups)
print(user)
if not rules:
print(" no sudo rights")
for group, rule in rules:
print(f" via %{group}: {rule}")
tnowak@ad.example.com via %linux-admins@ad.example.com: ALL=(ALL) ALL rkumar@ad.example.com via %web-team@ad.example.com: ALL=(root) /usr/bin/systemctl restart nginx lromero@ad.example.com no sudo rights
Real sudo is stricter than this model: it compares the name in the
file with the name NSS returns. If SSSD returns
linux-admins@ad.example.com and the file says
%Linux-Admins (no domain, different case), the rule
silently never matches.
SSH with central accounts
Once SSSD works,
ssh -l tnowak@ad.example.com lnx-web01.ad.example.com
logs in with the AD password through PAM, and
pam_mkhomedir or
oddjob-mkhomedir
creates the home directory on first login. Disabling the account in
AD stops the next login everywhere. There are three ways to
authenticate, with different trade-offs:
| Method | How it works | Watch out for |
|---|---|---|
| Directory password | sshd asks PAM, PAM asks SSSD, SSSD checks with Kerberos | Passwords over SSH can be phished and guessed; combine with MFA or restrict to a bastion host |
| SSH keys (see SSH key pairs) |
The public key is in ~/.ssh/authorized_keys on
each server, or in the directory and fetched by
sss_ssh_authorizedkeys through sshd's
AuthorizedKeysCommand
|
A key login skips the password, so only SSSD's account check stops a disabled user; keys copied to local accounts are easy to forget during offboarding |
| Kerberos (GSSAPI) |
The client presents a service ticket for
host/lnx-web01; no password and no key on the
server
|
Needs a valid ticket, correct DNS names (the ticket is for the name you typed) and synchronized clocks |
Kerberos with SSH gives real single sign-on: log in to your
workstation once, then SSH to any joined server without typing a
password. It is enabled with
GSSAPIAuthentication yes in sshd_config on
the server and in the client configuration. Storing public keys in
the directory (FreeIPA does this out of the box; with AD it needs an
attribute for the key) combines keys with central offboarding:
disable the account and the key stops working everywhere.
FreeIPA and Samba
FreeIPA and Red Hat Identity Management
FreeIPA is an open-source identity
management server built for Linux and Unix. It combines components
you would otherwise integrate yourself: 389 Directory Server for
LDAP, MIT Kerberos as the key distribution center, Dogtag as
certificate authority, BIND for DNS, SSSD on the clients and time
synchronization, managed through a web interface, a command-line
tool (ipa) and an API. FreeIPA is the upstream project
of Red Hat Identity Management (IdM), the supported version in Red
Hat Enterprise Linux.
Its strengths are Linux-native features that AD lacks without
extensions: sudo rules, host-based access control (who may log in to
which host groups), SSH public keys and automount maps, all stored
centrally and delivered by SSSD. Hosts join with
ipa-client-install or realm join. A
FreeIPA domain can set up a trust with Active Directory, so AD users
log in to FreeIPA-managed Linux hosts with their AD accounts while
Linux policy stays in FreeIPA. That split — AD for people, FreeIPA
for Linux policy — is a common design in large Linux estates.
Samba
Samba is open-source software that implements the Windows file-sharing and domain protocols on Linux and Unix: SMB (Server Message Block) file and print sharing, plus the RPC (remote procedure call) and authentication protocols around it. It plays three roles:
-
File server. A Linux server shares folders with
Windows clients over SMB. As a member of an AD domain (through
SSSD or Samba's own
winbind), it checks AD accounts and applies permissions to AD groups. -
Client tools.
smbclientand parts of the join tooling (thesamba-common-toolspackage in the discover output above) come from Samba. - AD-compatible domain controller. According to the Samba project, Samba has been able to act as an Active Directory domain controller since version 4.0 (2012). For years it stopped at the Windows Server 2008 R2 functional level; Samba 4.19 added the 2012 and 2012 R2 levels, and 4.20 added the 2016 level as an opt-in, still-maturing feature. Windows clients join it like an AD domain. It suits small organizations and labs; large AD estates with newer features stay on Windows domain controllers.
Common mistakes and troubleshooting
Common mistakes
-
DNS. The host must use DNS servers that hold the
AD zone, both to find domain controllers (SRV records) and because
Kerberos tickets are issued for DNS names. A host that resolves
ad.example.comthrough a public resolver cannot join. -
Names. With
use_fully_qualified_names = Truethe user istnowak@ad.example.com, nottnowak; sudoers rules, file ownership scripts andAllowGroupsinsshd_configmust use the form SSSD returns, in lower case. - Host name. The computer account name in AD is limited to 15 characters (the old NetBIOS limit). A long Linux host name is shortened on join, which confuses people later.
-
Stale cache. After a group change in AD, the
server can show old membership until the cache entry expires.
sss_cache -Emarks the whole cache as expired; the user still needs to log in again to get a new group list in their session.
Troubleshooting tools
Work from the outside in: can the host find and reach the domain, does SSSD see the user, does authentication work, and is the user allowed in? These commands are not run on this site:
realm list # joined? which domain, which access rules?
sudo sssctl domain-status ad.example.com # online? which DC is SSSD talking to?
getent passwd tnowak@ad.example.com # does NSS resolve the user through SSSD?
sudo sssctl user-checks tnowak@ad.example.com # simulate the PAM steps for this user
sudo journalctl -u sssd --since "10 min ago" # SSSD service messages
sudo tail /var/log/secure # PAM and sshd messages (auth.log on Debian/Ubuntu)
Online status: Online Active servers: AD Global Catalog: dc01.ad.example.com AD Domain Controller: dc01.ad.example.com Discovered AD Global Catalog servers: - dc01.ad.example.com - dc02.ad.example.com
"Online status: Offline" points to DNS, the network or time. If the
user resolves but login fails, look at the PAM part: wrong password,
account locked in AD, or not in an allowed group. For more detail,
set debug_level = 6 in the domain section temporarily;
SSSD then writes detailed logs in /var/log/sssd/. Turn
it off again afterward, as these logs grow fast. The general
approach is on
Monitoring and Troubleshooting Identity
and
Troubleshooting Linux.