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
- What Linux Directory is
- Mental model: how everything interacts
- Components and naming
- Install and first boot
- Day-one identity: users, groups, hosts, policy
- Linux hosts: enroll, sync, PAM, SSH, NSS
- Admin surfaces: API, CLI, Web UI
- Connecting apps via OIDC (SSO)
- Passkeys / WebAuthn
- Read-only LDAP gateway
- Optional VPN mesh (Tailscale, Headscale, WireGuard)
- Fleet (native)
- Production hardening checklist
- Operations cookbook
- 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
- Operator creates user
alice, groupadmins, HBAC + sudo rules (Admin UI / CLI / API); optionally sets account policy. - Host
box1runslindir-client enrollwith an admin token or a one-shot enroll token → server stores host + enrollment secret; optional VPN enroll. - Timer runs
lindir-client sync→ pulls bundle → writes extrausers + sudoers → runs pending package jobs → applies energy policy → uploads inventory → runs queries/scripts. - Alice SSHs to
box1→ PAM/pam-auth→ server checks password + account policy + HBAC (or offline cache if server down). - Alice opens Grafana → OIDC redirect → Linux Directory authorize → ID token → Grafana trusts JWKS.
- Optional:
vpn sync-aclspushes group membership into Tailscale/Headscale/WireGuard sidecar map. - Operator checks Dashboard /
ops attentionfor 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
- Provision PostgreSQL or MySQL (Storage). For MySQL/MariaDB, set
max_allowed_packet≥ 64 MiB before fleet vuln refresh / large inventory (Storage). - Build release binaries and admin UI (Packaging).
- Install via
packaging/install-server.sh,.deb, or CachyOSPKGBUILD. - Put TLS in front (Caddy / nginx / Traefik). Set:
issuerUrlto the public HTTPS URLrequireForwardedHttps: truewebauthnRpId/webauthnOriginsto match that hostname
- Set a long random
bootstrapToken,allowInsecureBootstrap: false. - 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
5.1 Recommended order
- Create groups that mirror access roles (
admins,developers,vpn-users). - Create users with passwords (needed for PAM and OIDC password login).
- Add members to groups.
- Enroll hosts (via client or admin enroll).
- HBAC — who may log into which hosts.
- Sudo — which groups get which commands on which hosts.
- SSH keys — attach public keys to users.
- Sync hosts (timer or manual).
- 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:
- Verifies the password (online).
- Loads the user’s group memberships.
- 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/lindiron each host for rules that include that host (or*). - SSH keys are not dumped into
authorized_keysfiles by default; useAuthorizedKeysCommand→lindir-client authorized-keysso 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:
- Pull
/v1/client/bundle→ extrausers + sudoers + offline caches - Claim/run pending package jobs
- Apply energy policy (logind / DPMS / dconf)
- Upload inventory
- 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
- Create confidential client with Grafana’s callback URL.
- Grafana → Authentication → Generic OAuth:
- Client ID / Secret from Linux Directory
- Auth URL / Token URL / API URL (UserInfo) / JWKS as above
- Scopes:
openid email profile
- Map email/login fields from claims (
email,preferred_username/subas your app expects). - 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.
- 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.
-
On Nextcloud, install and enable the user_oidc app.
-
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 |
-
Map claims for login / display / email (
preferred_username,email, name /subas your Nextcloud config expects). Soft auto-provision on first login is the usual setup. -
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
- Admin begins registration for a
userId→ browsercredentials.create. - Finish registration → credential stored.
- 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
- Create an API key with auth-key, device, and ACL scopes.
- Config:
"vpn": {
"provider": "tailscale",
"apiBase": "https://api.tailscale.com",
"apiKey": "tskey-api-…",
"tailnet": "example.ts.net"
}
- Enroll hosts with tags allowed by your policy (default
tag:lindir), or use--skip-vpn/skipVpnwhen the device is already on the mesh. - 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
- 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
hostIdsand/orlabelIds. - Energy policies — idle sleep, screen blank/off, lid close (logind + X11 DPMS + GNOME dconf); assign to hosts/labels.
- Labels (create / assign / HBAC sync).
- 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. - Cron — user crontab +
/etc/cron.d/lindir-*add / enable / disable / remove (preserves comments). - Systemd timers — system + user
lindir-*.timer/.service(optional linger for user scope). - Systemd oneshots — login (
default.target) / boot (multi-user.target)lindir-*.service(omit linger for login-only). - Systemd services — always-on
Type=simplelindir-*.service(Restart=on-failure); user scope under~USER/.config/systemd/user/with linger on by default, or systemmulti-user.target. See example below. - Systemd catalog — observe system units + operate jobs (start/stop/restart/…); no unit-file writes (use Timers / Oneshots / Services for
lindir-*). - Vulnerability findings — Ubuntu Security API, Arch AVG, and OSV for Debian/RHEL/SUSE (CachyOS kernel name aliases); Admin Software → Vulnerabilities.
- Queries & scripts queued per host.
- 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;
issuerUrlHTTPS;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;maxBodyBytesadequate for fleet - Fleet: review who holds Bearer tokens (packages/scripts/energy = root on hosts)
- Backups of SQL +
oidcSigningKeyPath(+ WireGuardconfigDirif 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
- Create user + add to
developers. - Ensure HBAC allows
developerson target hosts. - User sets password (or admin sets it); optional passkey registration.
- Laptop: on CachyOS/Arch
paru -S libnss-extrauserstheninstall.sh→ enroll → sync →getent(see Client). - Optional: OIDC apps already trust Linux Directory — no per-app password.
14.2 Adding a server to the fleet
- Provision OS (Ubuntu/Debian/CachyOS/Fedora/openSUSE — see Distributions).
- Install NSS (
libnss-extrausers: apt, or AUR on pacman hosts), thenlindir-clientvia package /install.sh. enrollwith API token or one-shot enroll token (len_…).- Confirm VPN key/peer if provider enabled.
- Assign fleet labels / energy / baseline packages as needed.
- Verify
getent, SSH key login, sudo rule; check Dashboard for sync age.
14.3 Offboarding a user
- Disable or delete user in Linux Directory (disable preserves audit history).
- Revoke API tokens and WebAuthn credentials.
- Remove from groups; run
vpn sync-aclsif mesh ACLs depend on groups. - 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.
