↓ Skip to main content

SMB Authentication with AD on Linux

·10 mins
Table of Contents
After days of configuration and research, I couldn’t find a single source that covered everything needed for this setup end to end. My job requires centralized SSSD across all Linux servers, so here’s what I got working on both RHEL 8 and OpenSUSE 15.6.
Note

Tested on RHEL 8 and OpenSUSE 15.6. Core steps apply to newer versions — package names may differ slightly on RHEL 9+ and newer OpenSUSE releases.

The stack: Samba for SMB/CIFS, SSSD for AD integration, Kerberos for authentication, and realmd for domain join.

Before you start: DNS that resolves your domain controllers, Chrony running and synced, a domain admin account with join privileges, and your domain name and DC hostname to hand — company.com and dc1.company.com throughout below.

Step 1 — Packages
#

sudo dnf install -y realmd sssd oddjob oddjob-mkhomedir adcli \
    samba-common-tools krb5-workstation chrony samba samba-client \
    samba-winbind sssd-winbind-idmap cifs-utils policycoreutils-python-utils

sudo systemctl enable --now chronyd
sudo systemctl enable sssd
sudo apt update
sudo apt install -y realmd sssd sssd-tools libnss-sss libpam-sss \
    adcli samba-common-bin krb5-user chrony samba smbclient \
    winbind cifs-utils

sudo systemctl enable --now chrony
sudo systemctl enable sssd
sudo zypper install -y realmd sssd sssd-ad adcli \
    samba samba-winbind krb5-client chrony samba-client cifs-utils

sudo systemctl enable --now chronyd
sudo systemctl enable sssd
Note

SSSD is only enabled here, not started — it has no configuration yet. realm join (Step 4) will create the initial sssd.conf and start the service automatically.

Note

Ubuntu and OpenSUSE use AppArmor, not SELinux. The policycoreutils-python-utils package and semanage/restorecon commands throughout this guide are RHEL/CentOS-specific. Skip them on Debian-based and OpenSUSE systems.

Step 2 — DNS and time
#

AD is found through DNS SRV records, so name resolution has to point at the domain controllers and nothing else. The result should look like this /etc/resolv.conf — but set it where your system manages it (nmcli con mod <connection> ipv4.dns "192.168.1.10 192.168.1.11" ipv4.dns-search company.com under NetworkManager), or the next DHCP renewal writes over a hand edit:

nameserver 192.168.1.10
nameserver 192.168.1.11
search company.com
domain company.com

The second query is the one that matters. If the SRV lookup comes back empty, realm join will fail later with an error about the domain not being found:

dig company.com
dig _ldap._tcp.company.com SRV

Then point the clock at the same machines — /etc/chrony.conf, or /etc/chrony/chrony.conf on Debian and Ubuntu:

server dc1.company.com iburst prefer
server dc2.company.com iburst
sudo systemctl restart chronyd
chronyc sources -v
sudo systemctl restart chrony
chronyc sources -v
sudo systemctl restart chronyd
chronyc sources -v
Warning

Kerberos requires time to be within 5 minutes of the domain controllers. If clock skew exceeds this, authentication will fail with KRB5KRB_AP_ERR_SKEW.

Step 3 — Kerberos
#

/etc/krb5.conf:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[libdefaults]
    default_realm = COMPANY.COM
    dns_lookup_kdc = true
    dns_lookup_realm = false
    rdns = false
    ticket_lifetime = 24h
    renew_lifetime = 7d
    forwardable = true
    udp_preference_limit = 0

[realms]
    COMPANY.COM = {
        kdc = dc1.company.com
        admin_server = dc1.company.com
        default_domain = company.com
    }

[domain_realm]
    .company.com = COMPANY.COM
    company.com = COMPANY.COM

[logging]
    default = SYSLOG:NOTICE:DAEMON

Test it:

kinit administrator@COMPANY.COM
klist
kdestroy

Step 4 — Joining the domain
#

sudo realm discover company.com

sudo realm join company.com -U administrator \
    --client-software=sssd \
    --membership-software=samba

sudo realm list
net ads testjoin
Note

realm join --membership-software=samba calls net ads join internally — no need to run it again. It auto-configures /etc/sssd/sssd.conf, /etc/krb5.conf, and /etc/krb5.keytab. The custom sssd.conf in Step 6 intentionally replaces what realm join generated with a production-ready configuration.

Step 5 — PAM
#

Home directories have to be created on first login, or AD users land in a shell with no $HOME:

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

libpam-sss registers its own PAM profile when it’s installed; only home directory creation needs switching on.

sudo pam-config --add --sss
sudo pam-config --add --mkhomedir

Step 6 — SSSD
#

This replaces what realm join generated. The generated file works; this one is what you’d want in production — /etc/sssd/sssd.conf:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[sssd]
domains = company.com
config_file_version = 2
services = nss, pam

[domain/company.com]
ad_domain = company.com
krb5_realm = COMPANY.COM
realmd_tags = manages-system joined-with-samba
cache_credentials = True
id_provider = ad
ad_update_samba_machine_account_password = True

full_name_format = %3$s\%1$s
use_fully_qualified_names = False
fallback_homedir = /home/%u
default_shell = /bin/bash

# Requires POSIX attributes (uidNumber, gidNumber) set in AD
ldap_id_mapping = False

krb5_store_password_if_offline = True

access_provider = simple
simple_allow_groups = linuxadmins@company.com, itstaff@company.com
simple_allow_users = testuser@company.com

enumerate = False
sudo chmod 600 /etc/sssd/sssd.conf
sudo systemctl restart sssd

realm join already enabled and started it; a restart is all the new file needs. SSSD refuses to start if the file is readable by anyone but root, which is what the chmod is for.

Warning

ldap_id_mapping = False requires that all AD users and groups have POSIX attributes (uidNumber, gidNumber) configured in Active Directory. If they don’t, user lookups will fail silently. Set to True to let SSSD generate consistent IDs algorithmically instead.

Step 7 — NSS
#

On RHEL, authselect owns /etc/nsswitch.conf and the sssd profile from Step 5 already wrote this — edit it by hand and the next authselect run puts it back. On Debian, Ubuntu and openSUSE, /etc/nsswitch.conf:

passwd:     files sss
group:      files sss
shadow:     files sss
netgroup:   sss files
Warning

Do not add winbind to passwd or group here. In this setup, SSSD handles all NSS resolution. Winbind runs only as an internal Samba component for ID mapping via the sss backend. Mixing sss and winbind in nsswitch causes duplicate lookups and inconsistent results.

Step 8 — Samba
#

/etc/samba/smb.conf:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
[global]
    realm = COMPANY.COM
    workgroup = COMPANY
    security = ads

    kerberos method = secrets and keytab
    dedicated keytab file = /etc/krb5.keytab

    log file = /var/log/samba/log.%m
    log level = 2

    vfs objects = acl_xattr
    map acl inherit = yes
    store dos attributes = yes

    template homedir = /home/%U
    template shell = /bin/bash

    idmap config * : backend = tdb
    idmap config * : range = 10000-199999
    idmap config COMPANY : range = 200000-2147483647
    idmap config COMPANY : backend = sss

    client signing = mandatory
    server signing = mandatory

    socket options = TCP_NODELAY IPTOS_LOWDELAY

[shared]
    path = /srv/shared
    read only = no
    browsable = yes
    valid users = @linuxadmins, @itstaff
    force group = linuxadmins
    create mask = 0664
    directory mask = 0775

[data]
    path = /srv/data
    read only = no
    browsable = yes
    valid users = @dataaccess
    force group = dataaccess
    create mask = 0660
    directory mask = 0770

[homes]
    comment = Home Directories
    valid users = %S
    browsable = no
    read only = no
    create mask = 0700
    directory mask = 0700
Note

idmap config COMPANY : backend = sss tells Samba’s winbind to delegate ID mapping to SSSD for the COMPANY domain. The default tdb backend handles the * (fallback) range. The sss backend is read-only, so a writable fallback is required.

Warning

Group names in valid users must match how SSSD exposes them. With use_fully_qualified_names = False, groups resolve as linuxadmins — not linuxadmins@company.com. Using the FQDN form here will silently block all access.

Validate the configuration before starting services:

sudo testparm -s

The share directories
#

sudo mkdir -p /srv/shared /srv/data

sudo chgrp linuxadmins /srv/shared
sudo chmod 775 /srv/shared
sudo chgrp dataaccess /srv/data
sudo chmod 770 /srv/data

# SELinux (RHEL only): label the shares, and allow [homes]
sudo semanage fcontext -a -t samba_share_t "/srv/shared(/.*)?"
sudo semanage fcontext -a -t samba_share_t "/srv/data(/.*)?"
sudo restorecon -R /srv/shared /srv/data
sudo setsebool -P samba_enable_home_dirs on

Label the share directories rather than turning on samba_export_all_rw: that boolean lets Samba write anywhere on the filesystem, and the labels make it unnecessary.

Step 9 — Start it and test it
#

The service is smb on RHEL and OpenSUSE, smbd on Debian and Ubuntu:

sudo systemctl enable --now smb winbind
sudo systemctl status smb winbind
sudo systemctl enable --now smbd winbind
sudo systemctl status smbd winbind
sudo systemctl enable --now smb winbind
sudo systemctl status smb winbind

Resolution first — if these three don’t return anything, nothing else will work and Samba is not the problem:

getent passwd testuser@company.com
id testuser@company.com
getent group linuxadmins@company.com

Then the share itself:

# List shares
smbclient -L localhost -U testuser@company.com

# Test share access
smbclient //localhost/shared -U testuser@company.com

From a Windows client: \\linux-server\shared

Step 10 — Firewall
#

The named service already covers 137/udp, 138/udp, 139/tcp and 445/tcp — there’s no need to open them individually:

sudo firewall-cmd --permanent --add-service=samba
sudo firewall-cmd --reload
sudo ufw allow Samba    # the app profile name is case-sensitive
sudo ufw status
sudo firewall-cmd --permanent --add-service=samba
sudo firewall-cmd --reload

Troubleshooting
#

Clearing caches
#

sudo sss_cache -E
sudo systemctl restart sssd

sudo net cache flush

# Stale winbind ID cache: drop only the cache file
sudo systemctl stop smb winbind   # smbd on Debian/Ubuntu
sudo rm -f /var/lib/samba/winbindd_cache.tdb
sudo systemctl start winbind smb

Don’t clear /var/lib/samba/*.tdb wholesale. Some of those files are state, not cache — the ID mappings among them — and losing them can change which UID owns which file on disk.

Logs
#

sudo tail -f /var/log/sssd/sssd_company.com.log
sudo tail -f /var/log/samba/log.smbd

For deeper SSSD debugging, add debug_level = 9 in the [domain/company.com] section and restart sssd.

Checking the AD side
#

sudo net ads info
sudo net ads user info testuser
sudo net ads group info "linuxadmins"
klist -k /etc/krb5.keytab

# Winbind checks
sudo wbinfo -t        # verify trust secret
sudo wbinfo -n "COMPANY\testuser"  # resolve a specific user (safe)
# Avoid wbinfo -u / wbinfo -g in large AD environments — enumerates all
# domain users/groups and can cause significant load on domain controllers

# Active connections
sudo smbstatus

The three errors you’ll actually hit
#

NT_STATUS_ACCESS_DENIED — the user resolves but the share refuses. Group name form and SELinux context are the two usual causes:

id username@company.com
sudo testparm -s
ls -Z /srv/shared  # SELinux context (RHEL only)

Users not resolving — almost always a stale cache after a config change:

sudo sss_cache -E
sudo systemctl restart sssd winbind
getent passwd username@company.com

Authentication failures — check the clock before anything else:

klist
kinit username@COMPANY.COM
chronyc sources -v
net ads testjoin

Keeping it working
#

/usr/local/bin/samba-maintenance.sh:

#!/bin/bash
# SSSD manages machine account password rotation automatically via
# ad_update_samba_machine_account_password = True — do not run
# net ads changetrustpw here as it conflicts with SSSD's management.
sss_cache -E
tdbbackup /var/lib/samba/*.tdb 2>/dev/null || true
net ads testjoin
echo "Maintenance completed: $(date)"
sudo chmod +x /usr/local/bin/samba-maintenance.sh

# Weekly, Sunday 02:00
echo "0 2 * * 0 root /usr/local/bin/samba-maintenance.sh >> /var/log/samba-maintenance.log 2>&1" \
  | sudo tee /etc/cron.d/samba-maintenance

A file in /etc/cron.d rather than crontab -: piping into crontab - replaces root’s whole crontab with this one line.

The two settings that cost me the most time are both in Step 6 and Step 8, and neither fails loudly. ldap_id_mapping = False needs POSIX attributes in AD, and without them lookups return nothing rather than an error. valid users = @linuxadmins@company.com blocks everyone while looking exactly like a working line. If something is refusing access and the logs say nothing useful, check those two before anything else.