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
- 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, setmax_allowed_packetto at least 64 MiB before large fleet inventory or vulnerability refresh — see Storage. - 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.
- Build tools if you package from source —
dub, a D compiler (ldc2,dmd, orgdc), 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 MySQLbootstrapToken— the long random secret;allowInsecureBootstrap:falseissuerUrl— the public HTTPS URL browsers will open (scheme, host, no trailing slash surprises)requireForwardedHttps:truebehind TLSwebauthnRpId/webauthnOrigins— match that hostnameadminDistPath— path to the built Vue SPA (admin/distor 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.
