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.
