Linux client

lindir-client: enroll, sync, PAM, SSH, NSS, and fleet apply.

Linux client

lindir-client joins a host to the domain, syncs POSIX accounts/policy, applies fleet package and energy jobs, and hooks into PAM/SSH/sudo.

Build

# server → ./lindir-server
dub build

# client → ./lindir-client
cd client && dub build

Requirements

Directory accounts are projected into /var/lib/extrausers/ and must be visible to glibc via NSS extrausers. Install the module before enroll/sync (or password login will not see users).

Host family Package Source
Debian / Ubuntu libnss-extrausers apt (official)
CachyOS / Arch / other pacman-based libnss-extrausers AUR (required) — not in official pacman repos
Fedora / RHEL-like / openSUSE libnss-extrausers or equivalent distro package when available

Install on CachyOS / Arch

From a checkout of the lindir repo (needs ldc / dub for the build step inside install.sh):

# 1) NSS module (AUR — required; not in official repos)
paru -S libnss-extrausers    # or: yay -S libnss-extrausers

# 2) Build + install agent, configure-login.sh, and sync timer
#    (run from the repo root)
sudo ./client/packaging/install.sh

# 3) Join the directory (root enroll also runs configure-login)
sudo lindir-client enroll --url https://id.example.com --token "$TOKEN" --skip-vpn
# If the host is already enrolled, wire login stacks once:
# sudo lindir-client configure-login

# 4) Pull accounts / renew offline login cache
sudo lindir-client sync
getent passwd alice

install.sh installs /usr/bin/lindir-client, /usr/lib/lindir/configure-login.sh, and enables lindir-sync.timer. You do not need a separate install of configure-login.sh.

Optional: package via PKGBUILD instead of install.sh — see Packaging (paru -S libnss-extrausers, then client/packaging/cachyos → makepkg -si).

Install (Ubuntu / Debian / Fedora / openSUSE)

# Debian/Ubuntu first:
sudo apt install libnss-extrausers

# from repo root
sudo ./client/packaging/install.sh

This installs /usr/bin/lindir-client, configure-login.sh, and enables lindir-sync.timer (sync every 10 minutes after boot). Packaging: Packaging.

Quick start (any distro)

On the server, create a user with a password (admin API). On the host (after the install steps above):

sudo lindir-client enroll --url https://id.example.com --token BOOTSTRAP_OR_API_TOKEN
# already on Tailscale / mesh — skip creating a new auth key:
sudo lindir-client enroll --url https://id.example.com --token TOKEN --skip-vpn
# one-shot enroll token (len_… from Admin → Hosts or lindir-admin enroll-token create):
sudo lindir-client enroll --url https://id.example.com --token len_… --skip-vpn
# lab / self-signed:
sudo lindir-client enroll --url https://id.example.com --token TOKEN --insecure
# custom CA:
sudo lindir-client enroll --url https://id.example.com --token TOKEN --ca-file /etc/lindir/ca.pem

sudo lindir-client sync
getent passwd alice

Root enroll runs configure-login (NSS extrausers, PAM pam-auth / pam-account / pam_mkhomedir, SSH AuthorizedKeysCommand). Re-run anytime:

sudo lindir-client configure-login

http:// still works for local lab. TLS verification is on by default (tlsVerify in client.json); --insecure or "tlsVerify": false skips it. Use --skip-vpn when the host is already on the mesh (lindir still registers IdM; it will not mint a Tailscale/Headscale auth key).

Config lands in /etc/lindir/client.json (mode 600). Policy cache in /var/lib/lindir/cache.json; password hashes for offline PAM in /var/lib/lindir/cache-auth.json (also mode 600).

Without root, enroll writes under ./.lindir/ for local testing — then run configure-login as root after install.

Login cache (sync renews TTL)

Sync refreshes the login cache. Each successful sync renews offline password login for offlineMaxAgeSeconds (default 72 hours). Within that window the server is not required at login time.

Control Behavior
Sync Writes hashes + HBAC snapshot; stamps issuedAtUnix (renews the window)
Server down / timeout / 5xx / 429 Authenticate from cache-auth.json if the window is still open
Explicit deny online HTTP 401/403 → no cache fallback
Cache TTL offlineMaxAgeSeconds (default 259200 = 72h); stale → deny
Disable offline "offlineAuthEnabled": false in client config
HBAC Empty rule set allows (same as server); otherwise group+host must match
Secrets Hashes live only in cache-auth.json, not in the main cache

Keep lindir-sync.timer enabled so hosts renew the window while online.

Integration tests (Podman)

Multi-OS client authentication (NSS, configure-login, sync, getent, online + offline pam-auth, homes, Arch fish shell) is gated from a lindir checkout by:

./scripts/integration/auth/run.sh          # debian ubuntu fedora opensuse arch rocky
./scripts/integration/auth/run.sh arch     # CachyOS stand-in

Details: scripts/integration/auth/README.md in the source tree. The suite asserts the login path on Debian, Ubuntu, Fedora, openSUSE, Arch (CachyOS stand-in), and Rocky — including world-readable nsswitch, PAM markers, and offline auth after the server is stopped.

What sync installs

Path Content
/var/lib/extrausers/passwd Domain users (NSS); on Arch/CachyOS, shell is /usr/bin/fish when fish is installed
/var/lib/extrausers/group Domain groups
/var/lib/extrausers/shadow Placeholder (! — auth via PAM)
Home dirs from bundle Created on sync from /etc/skel if missing; pam_mkhomedir also wires first-login create
/etc/sudoers.d/lindir Sudo rules for this host
/etc/systemd/logind.conf.d/99-lindir-energy.conf Idle sleep / lid (when energy policy assigned)
/etc/X11/xorg.conf.d/10-lindir-dpms.conf Screen blank/off (when set)
/etc/dconf/db/local.d/00-lindir-energy GNOME idle/power (when /etc/dconf exists)

Sync order (root): IdM bundle → package jobs → energy policy → inventory upload → query/script runs. See Fleet.

Host login wiring (configure-login)

Password login needs three pieces: NSS (see accounts), PAM (check passwords), SSH (optional keys). Prefer the automatic path:

# see Requirements above for libnss-extrausers (apt vs AUR)
sudo lindir-client configure-login
sudo lindir-client sync
getent passwd alice

configure-login is idempotent: updates /etc/nsswitch.conf, inserts marked blocks into common-auth/common-account (Debian) or system-auth (Arch/Fedora), and writes /etc/ssh/sshd_config.d/60-lindir.conf.

Manual NSS lines (if you prefer not to run the script):

passwd:         files extrausers systemd
group:          files extrausers systemd
shadow:         files extrausers

Online PAM auth hits POST /v1/client/auth (password + HBAC + account policy). Offline fallback only when the server is unreachable or returns a transient error — see Login cache above.

configure-login also adds pam_mkhomedir (session) so first login can create a missing home; sync creates homes from /etc/skel when absent.

HTTPS

The client uses libcurl (std.net.curl) for both http:// and https://. Certificate verification is enabled by default.

Setting Where Meaning
tlsVerify client.json / enroll Verify peer + hostname (default true)
tlsCaFile client.json / --ca-file Custom CA bundle path
--insecure enroll CLI Sets tlsVerify: false (lab only)

SSH public keys

configure-login installs this drop-in (or set it yourself in sshd_config):

AuthorizedKeysCommand /usr/bin/lindir-client authorized-keys
AuthorizedKeysCommandUser nobody

Then sudo systemctl reload sshd (or ssh on some distros).

Host API auth

After enroll, the client uses:

Authorization: Host <hostname> <enrollmentSecret>
Endpoint Purpose
GET /v1/client/bundle Users, groups, sudo, HBAC snapshot (+ offline hashes)
POST /v1/client/auth Online PAM password + HBAC + account policy
GET /v1/client/ssh-keys/{user} AuthorizedKeysCommand
POST /v1/client/fleet/inventory Inventory snapshot (packageDetails, osCodename, upgradable / upgradableCount, packages)
GET/POST /v1/client/fleet/packages/jobs… Claim / complete package jobs
GET /v1/client/fleet/energy Effective energy policy (applied:false if none)
GET/POST /v1/client/fleet/runs… Query/script jobs

systemd timer

lindir-sync.timer runs lindir-client sync periodically so accounts, packages, energy policy, inventory, MDM compliance, and console session claims stay current. For OIDC apps that require a compliant device, also run lindir-client mdm-broker (see MDM & Compliance). Pending Hosts → Console sessions are claimed during sync and run as a detached lindir-client console-session PTY worker (see Fleet).

Optional host tools (crontab, apt/pacman/dnf/zypper, wg-quick, loginctl, …) are invoked through a non-throwing spawn helper. Missing binaries fail that job/step with a clear error (and sync continues) instead of aborting the whole sync. Live WireGuard apply on the server does the same for wg / wg-quick / ip.

sudo systemctl enable --now lindir-sync.timer
systemctl status lindir-sync.timer

After reboot: if the timer is enabled (systemctl is-enabled lindir-sync.timer), systemd starts it again automatically (OnBootSec=1min, then every 10 minutes). No crontab. Package install scripts (install.sh, .deb postinst, PKGBUILD) enable the timer; a bare binary copy does not — run enable --now once.

Updating the agent (lindir-client)

The agent updates itself through the normal package managers (same path as other fleet package jobs):

Distro What runs
Ubuntu / Debian apt-get update then apt-get install lindir-client
CachyOS pacman -Sy --noconfirm lindir-client
Fedora / RHEL dnf -y upgrade lindir-client (falls back to install)
openSUSE / SLES zypper --non-interactive install lindir-client

Queue from Admin UI Software → Packages → Update agent, or:

lindir-admin fleet packages update-client --host-id HOST_UUID
# host picks it up on next timer tick or:
sudo lindir-client sync

Requirement: lindir-client must be published in an apt/pacman/dnf/zypper repo the host already uses. A one-shot install.sh binary drop is not self-updatable until you package it (see Packaging).

The running sync finishes with the old binary in memory; the next timer run uses the new /usr/bin/lindir-client. .deb / PKGBUILD / RPM install should keep lindir-sync.timer enabled.