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 tnowak is 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, an idp provider for OAuth 2.0 / OpenID Connect identity providers, and a proxy provider for older setups.
  • Responders answer local programs: nss for name lookups, pam for logins, and optional ones such as sudo (sudo rules stored in the directory) and ssh (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.
A Linux host joined to Active Directory. On the Linux host, login, sshd, sudo and ls use PAM and NSS, which pass requests to SSSD; SSSD has a local cache and reads the keytab. SSSD talks to the domain controller with LDAP on port 389 for users and groups and with Kerberos on port 88 for authentication, and finds the domain controller through DNS SRV records. Linux host lnx-web01 login sshd sudo ls, id PAM (authentication) NSS (names, groups) SSSD providers · responders cache (offline login) /etc/krb5.keytab Domain controller dc01.ad.example.com LDAP directory Kerberos KDC DNS LDAP 389 Kerberos 88 DC found via DNS SRV records
SSSD sits between the local plug-in systems (PAM and NSS) and the domain controller. It uses LDAP to read users and groups and Kerberos to check passwords.

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. smbclient and parts of the join tooling (the samba-common-tools package 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.com through a public resolver cannot join.
  • Names. With use_fully_qualified_names = True the user is tnowak@ad.example.com, not tnowak; sudoers rules, file ownership scripts and AllowGroups in sshd_config must 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 -E marks 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.