AD Integration & SSSD
Overview
The role integrates Ubuntu hosts with Active Directory via SSSD using realm join. Once joined, AD users authenticate via Kerberos (with their AD password), are authorised via AD group membership, and have their SSH public keys retrieved from the sshPublicKey attribute.
This page documents the integration's moving parts: krb5.conf, SSSD config, realm membership, schema requirements, and the access-control filter.
Realm Join Flow
From roles/ssh-baseline/tasks/ad-join.yml:
- Verify AD domain is resolvable —
getent hosts pbr.org.aureturns at least one DC IP. - Configure
/etc/krb5.conf— from thekrb5.conf.j2template (minimal, SRV-discovery based). - Check current AD join status —
realm list --name-only. If the host is already joined, the join task is skipped. - Join AD —
realm join --user=<svc account> --computer-ou=<OU> --os-name="Ubuntu Server" --os-version=<detected> <domain>. Password is supplied via stdin from the vault. Task hasno_log: true. - Verify Kerberos keytab exists at
/etc/krb5.keytab. - Configure realm access —
realm deny --all, thenrealm permit --groups <ServerAccess> <Sudo>. This is the realmd layer of the group gate (defence-in-depth alongside SSSD'sad_access_filterand sshd'sAllowGroups). - Enable SSS and mkhomedir PAM profiles —
pam-auth-update --enable sss --enable mkhomedir. - Verify
pam_sssin common-auth with correct flow control (sanity check — if pam-auth-update silently failed, we catch it). - Deploy
/etc/sssd/sssd.conf— from thesssd.conf.j2template. - Validate SSSD config —
sssctl config-check. - Enable and start SSSD.
krb5.conf Template
Source: roles/ssh-baseline/templates/krb5.conf.j2
# Managed by Ansible - do not edit manually
# Minimal Kerberos client config; KDC/realm discovery via DNS SRV records.
# SSSD writes dynamic snippets under /var/lib/sss/pubconf/krb5.include.d/
includedir /var/lib/sss/pubconf/krb5.include.d/
[libdefaults]
default_realm = {{ ad_domain | upper }}
rdns = false
dns_lookup_realm = false
dns_lookup_kdc = true
ticket_lifetime = 24h
renew_lifetime = 7d
forwardable = true
udp_preference_limit = 0
Notable settings
| Setting | Value | Why |
|---|---|---|
includedir | /var/lib/sss/pubconf/krb5.include.d/ | SSSD writes dynamic snippets here (realm mappings, KDC lists). Including this directory lets SSSD update krb5 behaviour without touching our template. |
rdns | false | Don't reverse-resolve hostnames into principal names. Avoids principal-mismatch errors when reverse DNS is incomplete. |
dns_lookup_realm | false | The realm is fixed (we know it's PBR.ORG.AU). Don't waste time on DNS lookups for the realm itself. |
dns_lookup_kdc | true | Use SRV records to find KDCs. PBR has 4 DCs; SRV-based discovery is more resilient than static KDC lists. |
udp_preference_limit | 0 | Always use TCP. UDP is unreliable for Kerberos tickets that exceed the default UDP packet size (large PAC for users in many groups). |
ticket_lifetime | 24h | How long a TGT is valid before requiring re-auth. Default for AD-integrated Linux. |
renew_lifetime | 7d | How long a TGT can be renewed before requiring full re-auth. |
SSSD Configuration
Source: roles/ssh-baseline/templates/sssd.conf.j2 — rendered with the variables from defaults/main.yml and group_vars/all/main.yml.
[sssd]
# Explicit services list (alternative to systemd socket activation).
# Includes ssh responder so sss_ssh_authorizedkeys works for sshd.
services = nss, pam, ssh
domains = {{ ad_domain }}
config_file_version = 2
[domain/{{ ad_domain }}]
id_provider = ad
access_provider = ad
ad_domain = {{ ad_domain }}
krb5_realm = {{ ad_domain | upper }}
krb5_store_password_if_offline = True
cache_credentials = True
default_shell = /bin/bash
override_homedir = /home/%u
use_fully_qualified_names = False
ldap_id_mapping = True
realmd_tags = manages-system joined-with-adcli
# Disable GPO-based access control.
ad_gpo_access_control = disabled
ad_access_filter = {{ ad_access_filter }}
# Retrieve SSH public keys from AD via the sshPublicKey attribute
# (OpenSSH-LPK schema extension applied via openssh-lpk.ldif).
ldap_user_extra_attrs = sshPublicKey
ldap_user_ssh_public_key = sshPublicKey
Service responders
services = nss, pam, ssh — SSSD runs three responder daemons:
- nss — serves user/group name resolution.
getent passwd a.mfraserhits this. - pam — handles PAM authentication.
pam_sss.sotalks to it. - ssh — serves SSH public key lookups for
/usr/bin/sss_ssh_authorizedkeys. Without this, sshd cannot retrieve keys from AD.
The explicit list is the alternative to systemd socket activation. Both work, but explicit listing makes the service set inspectable and removes a layer of indirection during troubleshooting.
Identity & access providers
| Setting | Value | Purpose |
|---|---|---|
id_provider | ad | Identity lookups go to AD via LDAP. |
access_provider | ad | Access decisions go to AD — we use ad_access_filter. |
ad_domain / krb5_realm | Per group_vars | Define the AD domain and Kerberos realm. |
krb5_store_password_if_offline | True | Cache the user's Kerberos password if SSSD is offline. Enables offline login. |
cache_credentials | True | Cache user credentials. Required for offline auth. |
default_shell | /bin/bash | Default shell when AD doesn't supply one. |
override_homedir | /home/%u | Force homedir to /home/<username> regardless of what AD has. |
use_fully_qualified_names | False | Users are referenced as a.mfraser, not a.mfraser@pbr.org.au. |
ldap_id_mapping | True | Generate POSIX UIDs/GIDs algorithmically from AD SIDs. No POSIX attributes in AD required. |
realmd_tags | manages-system joined-with-adcli | Standard tags written by realm join — preserved by Ansible to avoid realmd discarding our config. |
ad_gpo_access_control = disabled
This is the single most consequential SSSD setting in the file. Inline comment in the template:
Per
sssd-ad(5), the default isenforcing, which evaluates Windows GPORemoteInteractiveLogonRightsettings on every SSH login. Any GPO at any parent OU that sets this right (intentionally for Windows servers, or inherited from an ancestor container) would silently deny SSH access. We usead_access_filteras the sole access control scheme; the sssd-ad(5) manpage explicitly directs disabling GPO control when doing so.
This is documented behaviour, not a workaround. The default exists to make SSSD respect Windows server access policy when AD admins want it; for Linux servers managed independently, disabling it is the canonical approach.
ad_access_filter
The filter is supplied from defaults/main.yml:
ad_access_filter: >-
(|(memberOf=CN={{ ad_server_access_group }},OU=Security,OU=Groups,DC=pbr,DC=org,DC=au)(memberOf=CN={{ ad_sudo_group }},OU=Security,OU=Groups,DC=pbr,DC=org,DC=au))
Rendered:
(|(memberOf=CN=SG_ServerAccess,OU=Security,OU=Groups,DC=pbr,DC=org,DC=au)(memberOf=CN=SG_Sudo,OU=Security,OU=Groups,DC=pbr,DC=org,DC=au))
The filter uses full DN references because it makes the match unambiguous regardless of LDAP search base. If two groups with the same name existed in different OUs, a name-only filter could match the wrong one.
If the security groups move OUs, defaults/main.yml must be updated.
SSH public key retrieval
The bottom two lines of the SSSD config are the magic:
ldap_user_extra_attrs = sshPublicKey
ldap_user_ssh_public_key = sshPublicKey
ldap_user_extra_attrs tells SSSD to fetch the sshPublicKey attribute alongside the standard user attributes during user lookups. ldap_user_ssh_public_key tells the SSH responder to expose that attribute via sss_ssh_authorizedkeys.
sshd is configured to call /usr/bin/sss_ssh_authorizedkeys %u as the user nobody (see SSH Hardening Reference). The flow:
- User connects to sshd with publickey auth, presenting their public key
- sshd invokes
sss_ssh_authorizedkeys a.mfraserasnobody sss_ssh_authorizedkeysasks the SSSD ssh responder for the user's keys- The SSSD ssh responder queries AD via LDAP for the
sshPublicKeyattribute on the user object - The keys are returned to sshd, which compares against the presented public key
- If a match, publickey auth succeeds — sshd then proceeds to the keyboard-interactive challenge (Duo)
AD Schema Requirements
sshPublicKey attribute
AD does not include the sshPublicKey attribute in its default schema. It must be added via the OpenSSH-LPK schema extension before the role can work.
The schema is applied once, against the AD Schema Master, using an LDIF file (openssh-lpk.ldif). PBR has applied this; preflight verifies it remains present:
# From roles/preflight/tasks/schema.yml
- name: Check sshPublicKey attribute exists in AD schema
community.general.ldap_search:
server_uri: "ldaps://{{ ad_domain }}"
bind_dn: "{{ ad_join_user }}"
bind_pw: "{{ ad_join_password }}"
dn: "CN=Schema,CN=Configuration,DC={{ ad_domain | replace('.', ',DC=') }}"
scope: onelevel
filter: "(cn=sshPublicKey)"
attrs:
- cn
- attributeID
register: schema_check
delegate_to: localhost
become: false
run_once: true
no_log: true
If the schema check fails, preflight aborts with:
sshPublicKey attribute not found in AD schema at pbr.org.au.
Apply openssh-lpk.ldif against the Schema Master before continuing.
Populating sshPublicKey on user objects
End users have their SSH public key populated on their AD user object. This is done manually or via a self-service script — not by this role. The attribute is multi-valued; a user can have multiple keys.
To set programmatically (PowerShell, on a domain-joined Windows host):
Set-ADUser a.mfraser -Replace @{
sshPublicKey = "ssh-ed25519 AAAA... user@workstation"
}
Service Account: ad_join_user
The role uses an AD service account stored in vault as vault_ad_join_user / vault_ad_join_password. Required AD permissions:
- Create computer objects in the target OU (
OU=Linux,OU=Servers,OU=Computers,OU=PBR,DC=pbr,DC=org,DC=au) - Read access to the Schema container (used by the preflight schema check)
It does not need Domain Admin rights. Best practice: a dedicated service account with delegated rights only.
The account password is rotated via a separate process (not by this role) and the vault updated via ansible-vault edit.
Realm Permit (realmd-layer Access Control)
After joining, the role runs:
realm deny --all
realm permit --groups SG_ServerAccess
realm permit --groups SG_Sudo
This adds entries to /etc/sssd/sssd.conf under simple_allow_groups. However, because we set access_provider = ad and use ad_access_filter instead, simple_allow_groups is not the effective gate — the AD access filter is.
The realmd commands are kept for two reasons:
- realmd-managed metadata.
realm listreflects what realmd thinks the access policy is. Keeping it consistent with the SSSD config avoids confusion when troubleshooting. - Defence in depth. If
access_providerwere ever changed tosimple,simple_allow_groupsbecomes the gate, and the realmd-issued permits keep enforcement consistent.
PAM Wiring (Authentication Side)
The role enables the SSS and mkhomedir profiles via pam-auth-update:
pam-auth-update --enable sss --enable mkhomedir
This modifies the Ubuntu-managed common-auth / common-account / common-password / common-session stacks to include pam_sss.so and pam_mkhomedir.so (or equivalent).
The role then verifies the result is what we expected:
- name: Verify pam_sss is in common-auth with correct flow control
ansible.builtin.shell: |
set -o pipefail
grep -E '^auth\s+\[success=1 default=ignore\]\s+pam_sss' /etc/pam.d/common-auth
This sanity check catches the (rare) case where pam-auth-update succeeds at the exit code level but doesn't actually add what we need.
How the Duo PAM stacks consume this: /etc/pam.d/sshd and /etc/pam.d/sudo are custom files (templated by the role). The sudo stack uses @include common-auth after Duo, which lets pam_sss validate the AD password as the post-Duo factor. See Duo MFA Integration for the full flow.
Troubleshooting AD/SSSD
User doesn't resolve via getent
getent passwd a.mfraser
# (no output)
Causes:
- User not in
SG_ServerAccessorSG_Sudo(access filter excludes them — SSSD won't surface them via NSS) - SSSD service not running —
systemctl status sssd - Stale SSSD cache —
sudo sss_cache -Eto invalidate - LDAP connectivity to DCs broken —
sssctl domain-status pbr.org.aushows ONLINE / OFFLINE
SSH key not found
sudo -u nobody /usr/bin/sss_ssh_authorizedkeys a.mfraser
# (no output or error)
Causes:
sshPublicKeyattribute not populated on the user's AD object — check in ADUC- SSS ssh responder not running —
services = nss, pam, sshin sssd.conf? Restart SSSD. - SSSD service account can't read user attributes — LDAP bind ACL issue (not the join account; SSSD uses the host's keytab)
sssctl config-check fails
This is caught by the role itself — the deploy halts if SSSD config doesn't validate. Inspect output:
sudo sssctl config-check
Usually a typo in ad_access_filter after a manual edit. Re-run the role to restore the template.
Where to Read Next
- Duo MFA Integration — how PAM connects AD authentication with Duo MFA
- SSH Hardening Reference —
AuthorizedKeysCommandand the sshd-side of key retrieval - Known Limitations, Troubleshooting & Version History — the realm join retry pattern caused by AD replication lag