How to install Linux Directory (Lindir)

A calm walkthrough from empty machine to first enrolled host — database, server, day-one identity, and the client that keeps accounts in sync.

Every small constellation of Linux machines needs a shared sense of who may arrive, and which doors they may open. Passwords scattered across laptops, sudoers edited by hand, SSH keys copied from chat logs — these are workable for a season, then they become weather you cannot predict.

Linux Directory (technical short name lindir) is a calm home for that shared memory: users and groups, host-based access, sudo and keys, optional app SSO, and a native fleet that can tend packages and services on the machines you enroll. This article follows a production-shaped path — a real database, TLS in front of the server, a strong bootstrap secret, then identity and a first host. For a short-lived lab with an in-memory store, see the Operator handbook. Full tables for config keys live there and in Storage and Packaging.


What you are putting in place

Three pieces matter on day one:

Piece Role
lindir-server HTTP API, Admin UI, OIDC issuer, fleet control plane
lindir-admin Thin CLI over the same Bearer Admin API
lindir-client Agent on each Linux host — enroll, sync accounts and policy, run fleet jobs

The product name is Linux Directory. Binaries, paths, and environment variables keep the short name lindir (/etc/lindir/, LINDIR_URL, and so on).


Prerequisites

  1. A SQL database — PostgreSQL or MySQL/MariaDB. Lab installs may use memory://; anything you intend to keep should use a real engine. For MySQL/MariaDB, set max_allowed_packet to at least 64 MiB before large fleet inventory or vulnerability refresh — see Storage.
  2. TLS termination — Caddy, nginx, Traefik, or another reverse proxy that presents HTTPS to browsers and forwards to the server. Passkeys and OIDC need a stable public hostname.
  3. Build tools if you package from source — dub, a D compiler (ldc2, dmd, or gdc), and Node.js for the Admin SPA. Distro packages skip most of that once artifacts exist.

Generate a long random bootstrap token now and keep it offline. You will use it once to create a durable API token, then stop reaching for it daily.


Install the server

Build release binaries and the Admin UI, then install. The shapes that matter most:

Local install script (from a Lindir source tree):

COMPILER="${COMPILER:-ldc2}"
dub build --compiler="$COMPILER" --build=release
(cd cli && dub build --compiler="$COMPILER" --build=release)
(cd admin && rm -rf node_modules && npm ci && npm run build)
sudo ./packaging/install-server.sh

Ubuntu .deb:

# after release builds as above
./scripts/packaging/build-deb-server.sh
sudo dpkg -i dist/lindir-server_*.deb

CachyOS / Arch PKGBUILD:

cd packaging/cachyos && makepkg -si

Details and RPM paths: Packaging.

Start from a sample config that matches your database — config.postgres.example.json or config.mysql.example.json in the Lindir tree — and place the live file where your package expects it (often under /etc/lindir/).

Set at least:

  • dbEngine / dbDsn — connection to PostgreSQL or MySQL
  • bootstrapToken — the long random secret; allowInsecureBootstrap: false
  • issuerUrl — the public HTTPS URL browsers will open (scheme, host, no trailing slash surprises)
  • requireForwardedHttps: true behind TLS
  • webauthnRpId / webauthnOrigins — match that hostname
  • adminDistPath — path to the built Vue SPA (admin/dist or the packaged location)

Ensure the database is up before (and with) lindir-server. Packaged units typically use After= for the DB, not Requires=. If SQL is down, Admin and OIDC can look empty. Prefer a drop-in with Requires= / Wants= on your database unit, or a supervisor that starts the DB first.

Migrations under sql/migrations/ apply automatically on start.

Confirm the process:

curl -fsS https://id.example.com/health
# → ok / JSON health, depending on version

Open https://id.example.com/admin/ in a browser. First entry is with the bootstrap Bearer token (or the flow your package documents). When the SPA looks stuck on “token-only” login after an upgrade, rebuild and redeploy adminDistPath — a stale admin/dist is a common cause.


Point the CLI at the directory

On an operator machine that can reach the server:

export LINDIR_URL=https://id.example.com
export LINDIR_TOKEN='…'   # bootstrap for now; replace after token create
# lab TLS only: add --insecure, or use --ca-file /path/to/ca.pem
lindir-admin health

lindir-admin is a thin Bearer client. The same token works for automation and for emergency break-glass. Day-to-day human login to /admin/ uses a directory username/password (or passkey) plus an Admin ACL role — that is separate from Bearer tokens.


Day-one identity

Recommended order: groups that mirror roles → users with passwords → membership → hosts → HBAC → sudo and SSH keys → sync. Optional OIDC, VPN, and LDAP come after the core loop works. See handbook §5.

Create an API token and retire daily bootstrap use

lindir-admin token create --name laptop-ops --role admin

Store the returned ldt_… secret. Switch LINDIR_TOKEN to it. Keep the bootstrap token for recovery, not for every command.

Persist credentials in ~/.config/lindir/mcp.env

Shell exports are fine for one session. For everyday use — and for lindir-mcp, which speaks the same Admin API — keep the URL and token in a small env file under your home directory, with tight permissions, outside any shared project tree.

mkdir -p ~/.config/lindir && chmod 700 ~/.config/lindir
cat > ~/.config/lindir/mcp.env <<'EOF'
LINDIR_URL=https://id.example.com
LINDIR_TOKEN=ldt_…
LINDIR_MCP_MODE=readonly
EOF
chmod 600 ~/.config/lindir/mcp.env
Variable Notes
LINDIR_URL Public HTTPS base of your directory (same as for lindir-admin)
LINDIR_TOKEN Prefer a durable ldt_… API token, not the bootstrap secret
LINDIR_MCP_MODE readonly (default) for inspection; full only when tools may enqueue fleet jobs
LINDIR_TOKEN_FILE Optional alternate path if you prefer the secret in its own file
LINDIR_ALLOW_HTTP / LINDIR_INSECURE / LINDIR_ALLOW_INSECURE_BOOTSTRAP Lab-only escapes — leave unset in production

Load the file when you open a terminal:

set -a
source ~/.config/lindir/mcp.env
set +a
lindir-admin health

For an MCP client, start lindir-mcp with no token in its config file — it reads ~/.config/lindir/mcp.env itself:

{
  "mcpServers": {
    "lindir": {
      "command": "node",
      "args": ["/absolute/path/to/lindir/mcp/dist/index.js"]
    }
  }
}

Details of the tool surface are in the Lindir repository’s docs/MCP.md. Keep the directory mode 700 and the env file 600.

Groups and users

lindir-admin group create --name admins --description 'People who may administer hosts'
lindir-admin group create --name developers

lindir-admin user create --username alice --password '…' --email alice@example.com
# optional: --host-admin adds alice to seeded group lindir-host-admins (sudo ALL on host *)
# that is host privilege, not Lindir console ACL

lindir-admin group add-member --group-id GROUP_UUID --user-id USER_UUID

Grant console access explicitly when needed:

lindir-admin access admin-acl grant --user USER_UUID --role admin_rw
# or --group GROUP_UUID — roles: admin_ro | admin_rw

HBAC sketch

Host-based access control decides who may log into which machines. An empty HBAC rule set allows login (useful in early lab). Once you add rules, unmatched logins are denied (hbac_denied).

In early production, create a deliberate rule in Admin Access → HBAC (for example admins → all hosts), then tighten. Verify with:

lindir-admin policy hbac check --host-id HOST_UUID --group-id GROUP_UUID

Sudo rules sync into /etc/sudoers.d/lindir on each host. SSH public keys stay central when you wire AuthorizedKeysCommand to lindir-client authorized-keys — see the handbook and Client.


Install the client and enroll a host

On each Linux machine that should join the directory.

CachyOS / Arch (extrausers is required for password login and lives in the AUR):

paru -S libnss-extrausers
# from Lindir source, or install the client package:
sudo ./client/packaging/install.sh
# or: cd client/packaging/cachyos && makepkg -si

Debian / Ubuntu:

sudo apt install libnss-extrausers
# then client install.sh or the .deb from packaging

Create a one-shot enroll token from the operator machine (or use a long-lived API token carefully):

lindir-admin enroll-token create --name first-host --ttl-hours 24 --max-uses 1 --hostname-hint trantor

On the host, as root:

sudo lindir-client enroll --url https://id.example.com --token 'len_…'
# self-signed lab CA: add --insecure
sudo lindir-client sync

Enroll writes /etc/lindir/client.json and registers the host. Sync pulls the account bundle (extrausers, sudoers, offline login cache), claims pending fleet jobs, and reports inventory. Prefer enabling the timer so hosts stay current without a manual push:

sudo systemctl enable --now lindir-sync.timer

Confirm a directory user is visible on the host:

getent passwd alice

If HBAC and PAM are configured, alice should be able to sign in according to your rules. Audit of denials and mutations lives under Admin Audit and lindir-admin audit.


Where to go next

Once the directory is alight — server healthy, API token in use, at least one host syncing — the same control plane can tend software and services across the fleet. Labels, package jobs, timers, and user systemd units are covered in Fleet and handbook §13. App SSO is in OIDC; passkeys in WebAuthn.

A concrete next chapter: placing large language models on a machine with a GPU while keeping their weight off the root disk — How to install Ollama on Linux with models on external storage using Lindir.


Further reading: Linux Directory product page, Operator handbook, Admin CLI, OWASP controls.