Linux Directory Operator Handbook

Deploy, operate, and integrate Linux Directory — identity, host policy, fleet, and SSO.

Linux Directory Operator Handbook

A practical guide to deploying, operating, and integrating Linux Directory — central identity, host policy, and fleet care for Linux machines.

For operators, SREs, and integrators. Topic guides (OIDC, VPN, LDAP, Fleet, and the rest) sit beside this handbook when you need more detail on one surface.


Table of contents

  1. What Linux Directory is
  2. Mental model: how everything interacts
  3. Components and naming
  4. Install and first boot
  5. Day-one identity: users, groups, hosts, policy
  6. Linux hosts: enroll, sync, PAM, SSH, NSS
  7. Admin surfaces: API, CLI, Web UI
  8. Connecting apps via OIDC (SSO)
  9. Passkeys / WebAuthn
  10. Read-only LDAP gateway
  11. Optional VPN mesh (Tailscale, Headscale, WireGuard)
  12. Fleet (native)
  13. Production hardening checklist
  14. Operations cookbook
  15. Glossary and further reading

1. What Linux Directory is

1.1 Purpose

Linux Directory is a central identity and host-policy server for Linux fleets. It answers questions operators live with every day:

  • Who may log into which machines? (HBAC)
  • Which groups get which sudo rules?
  • Which SSH public keys are authorized for which users?
  • How do web apps get SSO against the same directory? (OIDC)
  • How do legacy LDAP clients read accounts without becoming the source of truth? (LDAP gateway)
  • How do we install packages, reboot, and set energy policy on fleets? (native fleet)
  • Optionally: how do enrolled hosts join a VPN mesh with group-aware ACLs?

SQL (PostgreSQL or MySQL/MariaDB) is the system of record. The Linux client materializes accounts and policy on each host (extrausers, sudoers, PAM, SSH) and applies fleet jobs (packages, energy) on sync.

1.2 What the system holds

Linux Directory keeps, in one place:

  • People and hosts — users, groups, enrollment, one-shot join tokens
  • Host access — HBAC, central sudo, SSH public keys
  • Account policy — length, lockout, must-change, disable
  • App sign-in — OIDC IdP, passkeys, optional compliant-device gate
  • Fleet care — packages (apt/pacman/dnf/zypper), energy, inventory, labels, queries/scripts, vulnerability findings
  • Device posture — compliance scripts and policies; lock and retire
  • Day-to-day ops — attention dashboard, audit search, Admin API / CLI / Web UI
  • Optional VPN — Tailscale, Headscale, or WireGuard plugins

SQL is the system of record; LDAP, when enabled, is a read-only projection. Nextcloud (and similar apps) join as OIDC companions for files and calendar (Chapter 8). Ansible, Landscape, or Foreman may orbit the same estate when you already use them. Chapter 12 details the fleet surface.

1.3 Platforms

First-class server + client: Ubuntu LTS and CachyOS (Arch-based). First-class client also: Debian (apt), Fedora / RHEL-like (dnf), openSUSE / SLES (zypper).

Further reading: Shape of the system, Alternatives, Distributions.


2. Mental model: how everything interacts

2.1 Big picture

                         ┌──────────────────────────────────────┐
                         │         Operators / IdP consumers    │
                         │  Admin UI · CLI · curl · Grafana …   │
                         └──────────────┬───────────────────────┘
                                        │ Bearer token / OIDC
                                        ▼
┌───────────────────────────────────────────────────────────────────────────┐
│                         lindir-server (D)                                 │
│  ┌─────────────┐  ┌────────────┐  ┌──────────┐  ┌──────────────────────┐│
│  │ Identity    │  │ Policy     │  │ Auth     │  │ Optional plugins     ││
│  │ users/groups│  │ HBAC/sudo  │  │ tokens   │  │ VPN · LDAP listener  ││
│  │ hosts       │  │ SSH keys   │  │ PBKDF2   │  │ OIDC · WebAuthn      ││
│  └──────┬──────┘  └─────┬──────┘  └────┬─────┘  └──────────┬───────────┘│
│         └───────────────┴──────────────┴───────────────────┘            │
│                              repo + SQL                                  │
│                   memory:// | postgres | mysql                           │
└───────────────────────────────┬───────────────────────────────────────────┘
                                │ Host auth: Authorization: Host …
                                ▼
                    ┌───────────────────────────┐
                    │     lindir-client (host)  │
                    │ enroll · sync · pam-auth  │
                    │ authorized-keys           │
                    └─────────────┬─────────────┘
                                  │ writes
                                  ▼
                    extrausers · sudoers.d · caches

2.2 Trust domains (keep them distinct)

Actor How it authenticates What it can do
Bootstrap / API token Authorization: Bearer … Full admin API (users, policy, fleet, OIDC clients, VPN sync, …)
Enroll token (len_…) Authorization: Bearer len_… Only POST /v1/hosts/enroll (TTL / maxUses / optional hostname hint)
Client API auth Authorization: Host <hostname> <secret> Client API only: bundle, auth, SSH keys, fleet inventory/jobs/energy, console for that host
End user (login) Password or passkey (via OIDC / PAM / LDAP bind) Prove identity; subject to HBAC on hosts + account policy
OIDC relying party Client id (+ secret or PKCE) Exchange codes for tokens; never sees user passwords

A compromised host secret must not grant directory admin. A stolen browser session for /admin is Bearer-scoped — revoke the API token. Treat fleet package/script/energy APIs as root on every targeted host.

2.3 Data flow on a typical day

  1. Operator creates user alice, group admins, HBAC + sudo rules (Admin UI / CLI / API); optionally sets account policy.
  2. Host box1 runs lindir-client enroll with an admin token or a one-shot enroll token → server stores host + enrollment secret; optional VPN enroll.
  3. Timer runs lindir-client sync → pulls bundle → writes extrausers + sudoers → runs pending package jobs → applies energy policy → uploads inventory → runs queries/scripts.
  4. Alice SSHs to box1 → PAM/pam-auth → server checks password + account policy + HBAC (or offline cache if server down).
  5. Alice opens Grafana → OIDC redirect → Linux Directory authorize → ID token → Grafana trusts JWKS.
  6. Optional: vpn sync-acls pushes group membership into Tailscale/Headscale/WireGuard sidecar map.
  7. Operator checks Dashboard / ops attention for stale hosts, reboot recommendations, failed jobs, denials.

2.4 What is not pushed to hosts

Hosts do not get a full admin API surface. They get a host-scoped bundle: enabled users (with hashes for offline PAM), groups, sudo rules for that host, HBAC snapshot. Password hashes travel only on the host-authenticated channel.

Further reading: Architecture.


3. Components and naming

Name Role
Linux Directory Product name (Admin UI brand, documentation)
lindir Technical short name / package / config namespace (/etc/lindir/)
lindir-server Central daemon: HTTP API, OIDC, optional LDAP, admin SPA
lindir-admin Bearer CLI for ops
lindir-client Host agent: enroll, sync, PAM, authorized-keys
Admin Web UI Vue 3 SPA at /admin/ (static files from adminDistPath)
openapi.json Machine-readable HTTP API (GET /openapi.json)

Linux Directory is the product name. Binaries, packages, paths, and env vars keep the short name lindir (lindir-server, lindir-admin, lindir-client, /etc/lindir/, …).

Config path Purpose
/etc/lindir/config.json Server
/etc/lindir/client.json Client (mode 600)
/var/lib/lindir/ Server state (OIDC key, WireGuard, caches…)
/var/lib/lindir/cache.json Client policy cache
/var/lib/lindir/cache-auth.json Client offline password hashes (600)
/var/lib/extrausers/ NSS accounts written by sync

4. Install and first boot

4.1 Lab (from source)

# Server + CLI + admin UI
dub build --compiler=ldc2
(cd cli && dub build --compiler=ldc2)
(cd admin && rm -rf node_modules && npm ci && npm run build)

# Copy and edit config — start with config.example.json
cp config.example.json config.local.json
# Set a strong bootstrapToken; for lab only keep allowInsecureBootstrap

./lindir-server --config config.local.json --migrations sql/migrations
# → http://127.0.0.1:1289/admin/
# → http://127.0.0.1:1289/health

memory:// as dbDsn is fine for short-lived labs. Move to Postgres or MySQL when persistence matters.

4.2 Production-shaped install

  1. Provision PostgreSQL or MySQL (Storage). For MySQL/MariaDB, set max_allowed_packet ≥ 64 MiB before fleet vuln refresh / large inventory (Storage).
  2. Build release binaries and admin UI (Packaging).
  3. Install via packaging/install-server.sh, .deb, or CachyOS PKGBUILD.
  4. Put TLS in front (Caddy / nginx / Traefik). Set:
  • issuerUrl to the public HTTPS URL
  • requireForwardedHttps: true
  • webauthnRpId / webauthnOrigins to match that hostname
  1. Set a long random bootstrapToken, allowInsecureBootstrap: false.
  2. Create a long-lived API token via Admin UI / CLI, then stop using the bootstrap token daily.

4.3 Essential server config knobs

Key Meaning
listenHost / listenPort Bind address (default lab: 127.0.0.1:1289)
dbEngine / dbDsn postgres
bootstrapToken Initial admin Bearer secret
issuerUrl Public base URL for OIDC / WebAuthn
adminDistPath Built Vue SPA directory (admin/dist or /usr/share/lindir/admin)
ldap.enabled Start read-only LDAP listener
vpn.provider null
fleet.enabled Native fleet APIs + agent endpoints (default on)
maxBodyBytes HTTP body cap (default 262144 for fleet output)

Migrations under sql/migrations/ apply automatically on start (001 … 012_fleet_energy, …).

Further reading: Storage, Packaging, OWASP Controls.


5. Day-one identity: users, groups, hosts, policy

  1. Create groups that mirror access roles (admins, developers, vpn-users).
  2. Create users with passwords (needed for PAM and OIDC password login).
  3. Add members to groups.
  4. Enroll hosts (via client or admin enroll).
  5. HBAC — who may log into which hosts.
  6. Sudo — which groups get which commands on which hosts.
  7. SSH keys — attach public keys to users.
  8. Sync hosts (timer or manual).
  9. OIDC / VPN / LDAP as needed.

5.2 HBAC semantics (host login)

When a user authenticates on a host (POST /v1/client/auth or PAM), Linux Directory:

  1. Verifies the password (online).
  2. Loads the user’s group memberships.
  3. Evaluates HBAC rules: enabled rules whose group and host sets match.

Empty HBAC rule set → allow (same idea as “no rules yet”). Once you add rules, unmatched logins are denied (hbac_denied).

Design tip: start with a broad “all users → all hosts” rule in lab; tighten before production.

5.3 Sudo and SSH keys

  • Sudo rules are synced into /etc/sudoers.d/lindir on each host for rules that include that host (or *).
  • SSH keys are not dumped into authorized_keys files by default; use AuthorizedKeysCommand → lindir-client authorized-keys so keys stay central.

5.4 Audit

Admin mutations, host package results, MDM outcomes, and auth events land in audit_events. Browse via Admin UI Audit (filters + last-24h / denials shortcuts; clickable column sort, default When newest first; expand a row for detail and linked package-job stdout/stderr with hostname), CLI lindir-admin audit [--actor …] [--action …] [--since …], or GET /v1/audit?... (results sorted by at descending, then limited). Package-result actors are recorded as host:<hostname> (older rows may still show a host UUID — the UI resolves those when possible).

5.5 Day-to-day operations

Concern Where
Stale sync / reboot / failed jobs / denials / upgradables / vulns Admin Dashboard, GET /v1/ops/attention, lindir-admin ops attention
Account policy (length, expiry, lockout, must-change, disable) Admin Directory → Account policy / Users, GET/PUT /v1/account-policy, POST /v1/users/enable
One-shot join Admin Hosts enroll tokens (len_…), /v1/enroll-tokens
Label-targeted packages/reboot Fleet UI / labelIds on package jobs
Distro upgrades / select or full upgrade Fleet Upgrades / GET /v1/fleet/upgrades
Energy (sleep / screen / lid) Fleet UI / /v1/fleet/energy/*
Incident search Admin Audit (filters, sort, host logins ~24h, CSV export, package job logs)

Further reading: Admin CLI, Fleet, OWASP Controls.


6. Linux hosts: enroll, sync, PAM, SSH, NSS

6.0 Install the client (CachyOS / Arch)

paru -S libnss-extrausers                 # AUR — required
sudo ./client/packaging/install.sh        # from repo root

That installs the agent, configure-login.sh, and lindir-sync.timer. Details: Client → CachyOS. On Debian/Ubuntu: apt install libnss-extrausers then the same install.sh.

6.1 Enroll

On the host (as root), after install:

# bootstrap or API token (ldt_…), or a one-shot enroll token (len_…):
sudo lindir-client enroll --url https://id.example.com --token "$TOKEN"
# lab / self-signed:
sudo lindir-client enroll --url https://id.example.com --token "$TOKEN" --insecure

Create one-shot tokens in Admin Hosts or:

lindir-admin enroll-token create --name lab --ttl-hours 24 --max-uses 1 --hostname-hint box1

This writes /etc/lindir/client.json (URL, hostname, enrollment secret, TLS settings) and registers the host on the server. If a VPN provider is configured, enroll may also create a mesh auth key / WireGuard peer (see Chapter 11).

6.2 Sync

sudo lindir-client sync
# or: systemctl enable --now lindir-sync.timer

Sync (as root) does, in order:

  1. Pull /v1/client/bundle → extrausers + sudoers + offline caches
  2. Claim/run pending package jobs
  3. Apply energy policy (logind / DPMS / dconf)
  4. Upload inventory
  5. Run pending query/script jobs
Artifact Path
Users / groups /var/lib/extrausers/passwd, group, shadow
Homes Created on sync from /etc/skel if missing; pam_mkhomedir
Shell (Arch) /usr/bin/fish when fish is installed
Sudo /etc/sudoers.d/lindir
Policy cache /var/lib/lindir/cache.json
Offline hashes /var/lib/lindir/cache-auth.json (TTL renewed each sync)
Energy (logind) /etc/systemd/logind.conf.d/99-lindir-energy.conf
Energy (X11) /etc/X11/xorg.conf.d/10-lindir-dpms.conf
Energy (GNOME) /etc/dconf/db/local.d/00-lindir-energy

6.3 Host login wiring

As root, enroll runs lindir-client configure-login (also safe to re-run). That wires NSS extrausers, PAM pam-auth / pam-account, and SSH AuthorizedKeysCommand.

Requirement: libnss-extrausers must already be installed — apt on Debian/Ubuntu; AUR on every pacman-based host (paru -S libnss-extrausers). Official Arch repos do not ship it. Verify with getent passwd alice.

Extrausers shadow uses a ! placeholder on purpose — password hashes are never written there. Sync refreshes the login cache and renews offline validity for offlineMaxAgeSeconds (default 72 hours); within that window login works with the server down. When the server is reachable, PAM tries online auth first (lockout / must-change); 401/403 never fall back to cache. Details: Client, Architecture.

Further reading: Client.


7. Admin surfaces: API, CLI, Web UI

All three talk to the same Bearer JSON API. The UI suits exploration; the CLI suits scripts; OpenAPI suits integrations.

7.1 Authentication

export LINDIR_URL=https://id.example.com
export LINDIR_TOKEN=…   # bootstrap or ldt_… API token

Create durable tokens: Admin UI Platform → API tokens, or lindir-admin / POST /v1/auth/tokens. Revoke compromised ones promptly.

7.2 Web UI

Build once: cd admin && rm -rf node_modules && npm ci && npm run build.
Open https://id.example.com/admin/ — branded Linux Directory. Login stores the Bearer token in sessionStorage only.

Pages: Dashboard; Directory (Users, Groups, Passkeys, Account policy); Hosts (list, Console, Inventory, Labels, enroll tokens); Host access (HBAC, Sudo, SSH keys); Software (Packages, Upgrades, Apt sources, Vulnerabilities); Compliance (Devices, Policies, Scripts, Energy); Automation (Cron, Timers, Oneshots, Services, Systemd, Queries, Scripts, Runs); Platform (API tokens, OIDC, VPN); Audit.

Dev: Vite on :5173 proxies /v1 and /health to the server (Admin Web UI).

7.3 CLI examples

lindir-admin health
lindir-admin ops attention
lindir-admin user create --username alice --password '…' --email alice@example.com
lindir-admin user disable --user-id UUID
lindir-admin group create --name admins
lindir-admin group add-member --group-id UUID --user-id UUID
lindir-admin group remove-member --group-id UUID --user-id UUID
lindir-admin enroll-token create --name lab --ttl-hours 24 --max-uses 1
lindir-admin account-policy set --min-password-length 12 --lockout-threshold 5
lindir-admin host enroll --hostname box1 --tag tag:lindir
lindir-admin oidc client create --name grafana --redirect-uri 'https://grafana/…'
lindir-admin vpn sync-acls
lindir-admin fleet packages install --label-id UUID --package curl
lindir-admin fleet upgrades --host-id UUID
lindir-admin fleet vulns refresh --force
lindir-admin fleet vulns scan
lindir-admin fleet energy create --name laptop --idle-action suspend --idle-delay-sec 1800
lindir-admin fleet energy assign --policy-id UUID --label-id UUID
lindir-admin fleet energy unassign --policy-id UUID --label-id UUID
lindir-admin fleet labels assign --host-id UUID --label-id UUID
# Always-on user daemon (linger on by default for add/enable)
lindir-admin fleet service add --host-id UUID --scope user --username nicolay \
  --unit myapp --command '/home/nicolay/bin/mydaemon' \
  --timer-description 'always-on myapp'
lindir-admin fleet service restart --host-id UUID --scope user --username nicolay --unit myapp
lindir-admin policy hbac check --host-id UUID --group-id UUID
lindir-admin passkey list --user-id UUID
lindir-admin audit --action auth_ok --since '2026-01-01 00:00:00'

7.4 API map (high level)

Area Prefix / paths
Health / meta GET /health, GET /v1/info, GET /openapi.json
Ops attention GET /v1/ops/attention
Directory /v1/users, /v1/users/password, /v1/users/enable, /v1/groups, /v1/groups/members, /v1/groups/members/remove, /v1/groups/{id}/members, /v1/hosts
Account policy GET/PUT /v1/account-policy
Enroll tokens /v1/enroll-tokens, /v1/enroll-tokens/revoke
Policy /v1/policy/hbac, /v1/policy/sudo, /v1/policy/ssh-keys
Authz tokens /v1/auth/tokens
Fleet /v1/fleet/labels, /inventory, /upgrades, /packages/jobs (+ ?status=pending, DELETE …/jobs/{id} cancel), /vulns/*, /energy/*, /queries, /scripts, /runs
Audit GET /v1/audit?actor&action&since&… (newest first)
OIDC IdP /oidc/*, /.well-known/openid-configuration
OIDC admin /v1/oidc/clients
WebAuthn /v1/webauthn/*
Admin SPA /admin/

Full schema: openapi.json. CLI: Admin CLI. UI: Admin Web UI.


8. Connecting apps via OIDC (SSO)

Linux Directory is the OpenID Connect identity provider. Apps (Grafana, Nextcloud, internal portals, SPAs) redirect users here; they never store Linux Directory passwords.

8.1 Server prerequisites

{
  "issuerUrl": "https://id.example.com",
  "oidcSigningKeyPath": "/var/lib/lindir/oidc-rsa.pem"
}
  • Discovery: https://id.example.com/.well-known/openid-configuration
  • JWKS (RS256): https://id.example.com/oidc/jwks
  • Authorize / token / userinfo under /oidc/…
  • Users need passwords (or passkeys — Chapter 9)

8.2 Register a confidential client (server-side apps)

lindir-admin oidc client create \
  --name grafana \
  --redirect-uri 'https://grafana.example.com/login/generic_oauth'

Save clientId and clientSecret (shown once). In the app:

Setting Value
Issuer / Auth URL Discovery or …/oidc/authorize
Token URL …/oidc/token
UserInfo …/oidc/userinfo
JWKS …/oidc/jwks
Scopes openid profile email
Auth method client_secret_basic or client_secret_post

8.3 Register a public client (SPA / native) with PKCE

lindir-admin oidc client create \
  --name spa \
  --redirect-uri 'https://app.example.com/cb' \
  --public

No client secret. PKCE (S256) is required. Token auth method: none.

8.4 Example: Grafana Generic OAuth

  1. Create confidential client with Grafana’s callback URL.
  2. Grafana → Authentication → Generic OAuth:
  • Client ID / Secret from Linux Directory
  • Auth URL / Token URL / API URL (UserInfo) / JWKS as above
  • Scopes: openid email profile
  1. Map email/login fields from claims (email, preferred_username / sub as your app expects).
  2. Test with a Linux Directory user that has a password (or passkey on the authorize page).

8.5 Refresh tokens

Token responses include a single-use refresh token (rotated; ~30 day lifetime by default). Apps should treat refresh like a session secret.

8.6 Example: Nextcloud (user_oidc)

Nextcloud is a natural companion for files, calendar, and contacts (optional Mail app). Linux Directory stays the IdP; Nextcloud never stores Linux Directory passwords.

  1. Create a confidential OIDC client with Nextcloud’s callback:
lindir-admin oidc client create \
  --name nextcloud \
  --redirect-uri 'https://cloud.example.com/apps/user_oidc/code'

Optional MDM gate: set requireCompliantDevice on the client (Admin UI Platform → OIDC, or API) so login needs a compliant enrolled host + lindir-client mdm-broker — see MDM & Compliance.

  1. On Nextcloud, install and enable the user_oidc app.

  2. Add an OpenID Connect provider pointing at Linux Directory:

Setting Value
Discovery / Issuer https://id.example.com/.well-known/openid-configuration
Client ID / Secret From oidc client create
Scopes openid profile email
Auth method client_secret_basic or client_secret_post
  1. Map claims for login / display / email (preferred_username, email, name / sub as your Nextcloud config expects). Soft auto-provision on first login is the usual setup.

  2. Test browser SSO with a Linux Directory user (password or passkey on the authorize page).

Clients: CalDAV / CardDAV / desktop sync use Nextcloud app passwords (or session tokens), not the browser OIDC redirect.

Mail: Nextcloud’s Mail app can talk IMAP/SMTP to a separate mailbox stack (Mailcow, Stalwart, …). That mail plane sits outside Linux Directory — register those apps for SSO separately if they support OIDC.

Further reading: OIDC SSO.


9. Passkeys / WebAuthn

Passkeys are registered against Linux Directory as WebAuthn RP (ES256). They can replace passwords on the OIDC authorize page.

9.1 Config

{
  "issuerUrl": "https://id.example.com",
  "webauthnRpId": "id.example.com",
  "webauthnRpName": "Linux Directory",
  "webauthnOrigins": ["https://id.example.com"]
}

Requires a secure context (HTTPS or localhost). Behind a reverse proxy, enable requireForwardedHttps.

9.2 Operator flow

  1. Admin begins registration for a userId → browser credentials.create.
  2. Finish registration → credential stored.
  3. User hits OIDC login → Sign in with passkey → short-lived loginToken → authorization code.

List/revoke credentials via Admin API (/v1/webauthn/credentials…).

Further reading: WebAuthn.


10. Read-only LDAP gateway

Some apps (NAS panels, older Grafana LDAP auth, lab tools) only speak LDAP. Linux Directory can project SQL users/groups as LDAP without making LDAP writable or authoritative.

10.1 Enable

"ldap": {
  "enabled": true,
  "listenHost": "127.0.0.1",
  "listenPort": 1389,
  "baseDn": "dc=lindir,dc=local",
  "allowAnonymous": false
}

Tree: ou=people (posixAccount) and ou=groups (posixGroup). Bind with user DN + password, or anonymous if allowAnonymous is true (keep false in production — LDAP Gateway / OWASP Controls).

10.2 Connect an app

Point the LDAP client at ldap://127.0.0.1:1389 (or expose via stunnel / a private network when you need an encrypted path).

ldapsearch -H ldap://127.0.0.1:1389 -x -b 'dc=lindir,dc=local' '(uid=alice)'

OIDC suits new apps; use LDAP when the client cannot speak OAuth.

Further reading: LDAP Gateway.


11. Optional VPN mesh (Tailscale, Headscale, WireGuard)

VPN is pluggable and optional. Core IdM works with vpn.provider: "null" (LAN-only).

11.1 How VPN hooks into IdM

Event VPN action
Host enroll enrollHost → auth/preauth key or WireGuard peer
POST /v1/vpn/sync-acls Push IdM groups → mesh ACL / policy / groups.json
Host remove removeHost → delete device/peer

Identity code does not import vendor SDKs outside lindir.vpn.*.

11.2 Choosing a provider

Provider When to use
null No mesh; LAN or external VPN
tailscale Managed Tailscale tailnet + API ACL merge
headscale Self-hosted coordination; policy.mode: database
wireguard Self-managed peers + config files under configDir

11.3 Connect Tailscale

  1. Create an API key with auth-key, device, and ACL scopes.
  2. Config:
"vpn": {
  "provider": "tailscale",
  "apiBase": "https://api.tailscale.com",
  "apiKey": "tskey-api-…",
  "tailnet": "example.ts.net"
}
  1. Enroll hosts with tags allowed by your policy (default tag:lindir), or use --skip-vpn / skipVpn when the device is already on the mesh.
  2. After group changes: lindir-admin vpn sync-acls.

Without apiKey/tailnet, the provider dry-runs (logs only).

11.4 Connect Headscale

"vpn": {
  "provider": "headscale",
  "apiBase": "https://headscale.example.com",
  "apiKey": "…",
  "tailnet": "lindir"
}

tailnet is the Headscale user name. Ensure policy is API-writable.

11.5 Connect WireGuard

"vpn": {
  "provider": "wireguard",
  "wireguard": {
    "endpoint": "vpn.example.com:51820",
    "iface": "wg0",
    "serverAddress": "10.66.0.1/24",
    "peerNetwork": "10.66.0.0/24",
    "configDir": "/var/lib/lindir/wireguard",
    "applyLive": false
  }
}

Enroll writes client + server configs. Bring up with wg-quick yourself unless you enable applyLive (needs root + tools). ACL “sync” writes groups.json for whatever external enforcer you use.

Further reading: VPN.


12. Fleet (native)

Built-in fleet — inventory, labels, apt/pacman/dnf/zypper package management, energy policies, shell queries/scripts. Details: Fleet.

12.1 Division of responsibility

Concern Owner
Users, groups, HBAC, sudo, SSH, SSO, account policy Linux Directory IdM
Host inventory, labels, packages, reboot, update-client, energy, cron, systemd timers/oneshots/services/catalog, query/script, vuln findings Linux Directory fleet (same server)
Files / calendar / contacts Companion (Nextcloud via OIDC — §8.6)
Heavy CVE estates / Landscape Optional companions

12.2 What works today

  1. Package management — install / remove / upgrade / refresh / reboot / update-client via apt (Ubuntu/Debian), pacman (CachyOS), dnf (Fedora/RHEL-like), and zypper (openSUSE); add-ppa / enable-ppa / disable-ppa / remove-ppa (purge-revert) on apt hosts; target by hostIds and/or labelIds.
  2. Energy policies — idle sleep, screen blank/off, lid close (logind + X11 DPMS + GNOME dconf); assign to hosts/labels.
  3. Labels (create / assign / HBAC sync).
  4. Inventory upload on client sync (includes packageDetails, osCodename/osVersion, upgradable, aptSources, crontabs, systemdTimers, systemdOneshots, systemdServices, systemdUnits, reboot-recommended signals). Admin Software → Upgrades / Apt sources; Automation → Systemd / Cron / Timers / Oneshots / Services; Hosts → Inventory.
  5. Cron — user crontab + /etc/cron.d/lindir-* add / enable / disable / remove (preserves comments).
  6. Systemd timers — system + user lindir-*.timer / .service (optional linger for user scope).
  7. Systemd oneshots — login (default.target) / boot (multi-user.target) lindir-*.service (omit linger for login-only).
  8. Systemd services — always-on Type=simple lindir-*.service (Restart=on-failure); user scope under ~USER/.config/systemd/user/ with linger on by default, or system multi-user.target. See example below.
  9. Systemd catalog — observe system units + operate jobs (start/stop/restart/…); no unit-file writes (use Timers / Oneshots / Services for lindir-*).
  10. Vulnerability findings — Ubuntu Security API, Arch AVG, and OSV for Debian/RHEL/SUSE (CachyOS kernel name aliases); Admin Software → Vulnerabilities.
  11. Queries & scripts queued per host.
  12. Admin API, CLI, Web UI (Software / Compliance / Automation / Hosts pages).

Example: always-on user service

Install a persistent daemon as Linux user nicolay on a host (Admin Automation → Services, or CLI). The unit becomes lindir-myapp.service; linger keeps the user manager (and the service) up without an interactive login.

# Queue from the admin machine (Bearer token in LINDIR_TOKEN / config)
lindir-admin fleet service add --host-id HOST_UUID \
  --scope user --username nicolay \
  --unit myapp \
  --command '/home/nicolay/bin/mydaemon --config /home/nicolay/.config/myapp.toml' \
  --timer-description 'myapp always on'

# On the host (or wait for lindir-sync.timer)
sudo lindir-client sync
# → writes ~nicolay/.config/systemd/user/lindir-myapp.service
# → loginctl enable-linger nicolay
# → systemctl --user -M nicolay@ enable --now lindir-myapp.service

# Later: restart after a binary update
lindir-admin fleet service restart --host-id HOST_UUID \
  --scope user --username nicolay --unit myapp
sudo lindir-client sync   # on the host

Inventory then lists the unit under systemdServices[] (Admin Automation → Services / Hosts → Inventory). Full reference: Fleet → Systemd services.

12.3 Ops tip

Queue a package, cron, timer, oneshot, service, systemd operate, or energy assignment, then sudo lindir-client sync on the target (or wait for the timer). Package results: GET /v1/fleet/packages/jobs (pending: ?status=pending; cancel: DELETE …/jobs/{id}); cron: GET /v1/fleet/cron/jobs / GET /v1/fleet/crontabs; timers: GET /v1/fleet/timer/jobs / GET /v1/fleet/timers; oneshots: GET /v1/fleet/oneshot/jobs / GET /v1/fleet/oneshots; services: GET /v1/fleet/service/jobs / GET /v1/fleet/services; systemd: GET /v1/fleet/systemd/jobs / GET /v1/fleet/systemd-units; energy effective policy on host: GET /v1/client/fleet/energy; query results: GET /v1/fleet/runs; upgradable packages: GET /v1/fleet/upgrades; vulns: POST /v1/fleet/vulns/scan.


13. Production hardening checklist

  • Strong bootstrapToken; allowInsecureBootstrap: false
  • Postgres or MySQL (not memory://)
  • TLS terminator; issuerUrl HTTPS; requireForwardedHttps: true
  • WebAuthn RP ID / origins match public hostname
  • Daily ops use API tokens, not bootstrap; revoke old tokens
  • Admin UI / API not exposed without authz at the edge if possible
  • LDAP bound to localhost or private net until StartTLS exists
  • VPN API keys stored only in server config (0600)
  • Client: tlsVerify: true; offline TTL understood; sync timer enabled
  • HBAC rules reviewed (empty-set allow semantics — see OWASP Controls)
  • exposeErrorDetails: false; rate limits sane; maxBodyBytes adequate for fleet
  • Fleet: review who holds Bearer tokens (packages/scripts/energy = root on hosts)
  • Backups of SQL + oidcSigningKeyPath (+ WireGuard configDir if used)
  • Plan for audit alerting (logs alone are not enough for A09:2025)

Details: OWASP Controls.


14. Operations cookbook

14.1 Onboarding a developer laptop

  1. Create user + add to developers.
  2. Ensure HBAC allows developers on target hosts.
  3. User sets password (or admin sets it); optional passkey registration.
  4. Laptop: on CachyOS/Arch paru -S libnss-extrausers then install.sh → enroll → sync → getent (see Client).
  5. Optional: OIDC apps already trust Linux Directory — no per-app password.

14.2 Adding a server to the fleet

  1. Provision OS (Ubuntu/Debian/CachyOS/Fedora/openSUSE — see Distributions).
  2. Install NSS (libnss-extrausers: apt, or AUR on pacman hosts), then lindir-client via package / install.sh.
  3. enroll with API token or one-shot enroll token (len_…).
  4. Confirm VPN key/peer if provider enabled.
  5. Assign fleet labels / energy / baseline packages as needed.
  6. Verify getent, SSH key login, sudo rule; check Dashboard for sync age.

14.3 Offboarding a user

  1. Disable or delete user in Linux Directory (disable preserves audit history).
  2. Revoke API tokens and WebAuthn credentials.
  3. Remove from groups; run vpn sync-acls if mesh ACLs depend on groups.
  4. Hosts pick up removal on next sync (and HBAC deny online immediately for auth).

14.4 Adding SSO for an application

OIDC is the usual path (Chapter 8). Grafana: §8.4. Nextcloud (files/calendar/contacts): §8.6. Use the LDAP gateway only when the app cannot speak OAuth.

14.5 Login when the server is unreachable

After a successful sync, offline PAM works for offlineMaxAgeSeconds (default 72 hours) — the sync is the cache refresh that renews that window. Explicit denials online (401/403) stay denials. Re-sync while online to renew the window again.

14.6 Health probes

curl -fsS https://id.example.com/health
curl -fsS -H "Authorization: Bearer $TOKEN" https://id.example.com/v1/info
# expect adminUi / oidc / ldap / webauthn flags as configured

15. Glossary and further reading

Term Meaning
HBAC Host-based access control — which groups may log into which hosts
Bundle Host-scoped JSON policy+accounts returned by /v1/client/bundle
Bearer token Admin API credential (Authorization: Bearer)
Host auth Authorization: Host <hostname> <secret>
Extrausers NSS module reading /var/lib/extrausers/*
IdP Identity provider (here: Linux Directory’s OIDC stack)
RP OIDC/WebAuthn relying party (the app / browser origin)
Fleet Native device management in Linux Directory (packages, energy, inventory, labels, queries)
Linux Directory Product name; technical short name is lindir
Enroll token Short-lived len_… Bearer limited to host enroll
Account policy Min password length, expiry, lockout, must-change, disable-without-delete

Document index

Doc Content
Shape of the system Current capabilities and boundaries
Architecture Module layout
Storage Postgres / MySQL
Client Host agent
Fleet Packages, energy, inventory
MDM & Compliance Compliance, lock/retire, OIDC device gate
Admin CLI / Admin Web UI CLI / Web UI
OIDC SSO / WebAuthn SSO / passkeys
LDAP Gateway Read-only directory projection
VPN Tailscale / Headscale / WireGuard
Packaging / Distributions Install & platforms
OWASP Controls OWASP Top 10:2025 controls + residuals
Alternatives Landscape of Linux IdM systems
Reference labs Reference labs
openapi.json HTTP API contract

Linux Directory operator handbook — identity, host policy, fleet care, and the day-to-day path from first boot to production.