Native fleet
lindir includes a built-in fleet layer: host inventory, labels, package management, energy policies, cron / systemd automation (timers, oneshots, services, unit catalog), blocklists (/etc/hosts), remote queries, and scripts. Device management lives in the same SQL control plane as IdM.
Package management
First-class install / remove / upgrade / refresh for supported platforms:
| Distro | Manager | Client backends |
|---|---|---|
| Ubuntu (and Debian) | apt-get |
noninteractive, confold, update before install/upgrade |
| CachyOS (and Arch) | pacman |
--noconfirm, -Sy / -Syu / -R as appropriate |
| Fedora / RHEL / Rocky / Alma | dnf |
-y install/remove/upgrade; check-update for refresh (yum shim OK) |
| openSUSE / SLES | zypper |
--non-interactive install/remove/update; refresh for indexes |
Actions
| Action | Packages | Behavior |
|---|---|---|
install |
required | Install named packages |
remove |
required | Remove named packages (apt-get remove --purge / pacman -R / dnf remove / zypper remove) |
upgrade |
optional | Specific packages, or empty list = full system upgrade (apt-get upgrade / pacman -Syu / dnf upgrade / zypper update) if allowFullSystemUpgrade |
refresh |
none | Update indexes only (apt-get update / pacman -Sy / dnf check-update / zypper refresh) |
reboot |
none | Schedule host reboot a few seconds after the agent reports the job (next sync) |
update-client |
none | Upgrade/install the lindir-client package via apt/pacman/dnf/zypper (must be in host repos) |
add-ppa |
required | Ubuntu/apt only: Launchpad ppa:owner/name via add-apt-repository -y then apt-get update (needs software-properties-common) |
enable-ppa |
required | apt only: re-enable inventoriable source id (.list.disabled → .list, or deb822 Enabled: yes) then apt-get update — no package changes |
disable-ppa |
required | apt only: disable inventoriable source id (.list → .list.disabled, or deb822 Enabled: no) then apt-get update — packages stay installed |
remove-ppa |
required | apt only: disable inventoriable /etc/apt/sources.list.d/{id} and revert packages from that origin to remaining archive versions. packages[] = source basenames (.list / .sources) |
Package names are strictly validated on the server (no shell metacharacters). PPA specs and apt source ids use dedicated validators. The agent never builds shell strings from operator input — it passes argv arrays to apt-get / pacman / dnf / zypper / add-apt-repository.
Queue a reboot
Admin UI Software → Packages → Queue reboot, or:
lindir-admin fleet packages reboot --host-id HOST_UUID
# on the host:
sudo lindir-client sync # reports job, then schedules reboot (~3s)
Update the agent
Admin UI Software → Packages → Update agent, or:
lindir-admin fleet packages update-client --host-id HOST_UUID
Hosts need lindir-client available from their package repos (not only a manual install.sh binary). After reboot, lindir-sync.timer (if enabled) resumes syncs automatically — see Client.
Flow
- Admin queues:
POST /v1/fleet/packages/jobs{ "action":"install"|"upgrade", "allHosts":true, "packages":["nginx"] }(orhostIds/labelIds;allHoststargets every non-retired host) - Host on
lindir-client sync(as root): claims pending jobs (status →running) → runs apt/pacman/dnf/zypper argv → posts result → refreshes inventory - Inspect:
GET /v1/fleet/packages/jobs/{id}or Admin UI Software → Packages (click a row for stdout) - Cancel before claim:
DELETE /v1/fleet/packages/jobs/{id}(onlypending;runningcannot be stopped remotely). List pending:GET /v1/fleet/packages/jobs?status=pending
Claiming prevents a concurrent sync from double-executing the same job. Cancel uses a conditional update so a race with claim cannot resurrect a cancelled job.
Apt locks (Ubuntu / Debian). Package jobs wrap apt-get in timeout (default defaultPackageTimeoutSec, 1800s). On freshly provisioned or auto-updating hosts, unattended-upgrades or another dpkg/apt process may hold /var/lib/dpkg/lock-frontend for a long time — the job then fails or appears stuck until the lock clears. Wait for quiet package managers, raise the timeout for large upgrades, or schedule fleet apt work outside unattended windows. Other managers (dnf/zypper/pacman) have their own locks; the same idea applies: do not overlap interactive and fleet package work on one host.
lindir-admin fleet packages install --host-id HOST_UUID --package nginx --package curl
lindir-admin fleet packages install --label-id LABEL_UUID --package curl # all hosts with label
lindir-admin fleet packages install --all-hosts --package nginx # install on every active host
lindir-admin fleet packages upgrade --all-hosts --package nginx # upgrade on every active host
lindir-admin fleet packages upgrade --host-id HOST_UUID # full upgrade on one host
lindir-admin fleet packages add-ppa --host-id HOST_UUID --package ppa:deadsnakes/ppa
lindir-admin fleet packages disable-ppa --host-id HOST_UUID --package deadsnakes-ubuntu-ppa-jammy.list
lindir-admin fleet packages enable-ppa --host-id HOST_UUID --package deadsnakes-ubuntu-ppa-jammy.list
lindir-admin fleet packages remove-ppa --host-id HOST_UUID --package deadsnakes-ubuntu-ppa-jammy.list
lindir-admin fleet packages list
lindir-admin fleet packages pending
lindir-admin fleet packages cancel --id JOB_UUID
# on the host:
sudo lindir-client sync
Config
"fleet": {
"enabled": true,
"maxInventoryBytes": 1048576,
"maxOutputBytes": 262144,
"defaultQueryTimeoutSec": 30,
"defaultScriptTimeoutSec": 120,
"defaultPackageTimeoutSec": 1800,
"allowFullSystemUpgrade": true,
"maxPackagesPerJob": 50,
"archSecurityUrl": "https://security.archlinux.org/issues/all.json",
"ubuntuSecurityBase": "https://ubuntu.com/security",
"debianDsaListUrl": "https://salsa.debian.org/security-tracker-team/security-tracker/-/raw/master/data/DSA/list",
"osvApiBase": "https://api.osv.dev"
}
Set maxBodyBytes ≥ fleet.maxInventoryBytes (samples use 1MiB) so agents can post full package inventories. Full key list: Operator handbook §4.3.
Package name rules
Allowed: alphanumerics, -_+.@:/ (Debian multiarch pkg:amd64, Arch repo/pkg). Rejected: spaces, shell metacharacters, leading -//, ...
Energy policies
Named power profiles (idle sleep, screen blank/off, lid close) assigned to hosts and/or labels. Host assignment wins over label.
On sync the agent writes:
| Mechanism | Path | Controls |
|---|---|---|
| systemd-logind | /etc/systemd/logind.conf.d/99-lindir-energy.conf |
IdleAction / IdleActionSec, HandleLidSwitch* |
| X11 DPMS | /etc/X11/xorg.conf.d/10-lindir-dpms.conf |
BlankTime / OffTime (minutes) |
| GNOME dconf | /etc/dconf/db/local.d/00-lindir-energy |
session idle-delay + power plugin timeouts |
lindir-admin fleet energy create --name laptop \
--idle-action suspend --idle-delay-sec 1800 \
--screen-blank-sec 600 --screen-off-sec 900 --lid-close suspend
lindir-admin fleet energy assign --policy-id UUID --label-id LABEL_UUID
lindir-admin fleet energy unassign --policy-id UUID --label-id LABEL_UUID
lindir-admin fleet labels assign --host-id UUID --label-id LABEL_UUID
lindir-admin fleet labels hosts --label-id LABEL_UUID
# on hosts:
sudo lindir-client sync
Admin UI: Compliance → Energy. API: /v1/fleet/energy/policies, PUT/DELETE …/policies/{id}, /v1/fleet/energy/assign, /unassign, …/targets, client GET /v1/client/fleet/energy.
Fields: idleAction (ignore|suspend|hibernate|lock|poweroff), idleDelaySec, screenBlankSec, screenOffSec, lidClose, lidCloseDocked. Seconds 0 = never / unmanaged for that setting. Host assignment wins over label (newest label policy if several).
Blocklists (/etc/hosts)
Desired-state hosts policies for fleet /etc/hosts. Lindir is the only source of truth (no upload/merge of local hand-edits). Layout inside # BEGIN lindir-hosts … # END lindir-hosts:
- Tailscale / intranet — positive maps (shared in the policy)
- Allowlist — domains as
# 0.0.0.0 domaincomments; omitted from active block sections - Personal blocks — curated
0.0.0.0lines - Tagged feeds — StevenBlack (and others), content-addressed by sha256
Efficient sync: client GET /v1/client/fleet/hosts returns policy metadata + feed shas only. Feed bodies download via GET /v1/client/fleet/hosts/feeds/{sha256} and are cached under /var/lib/lindir/hosts-feeds/<sha> — re-downloaded only when the sha changes. Upstream refresh is admin-driven (POST …/feeds/{id}/refresh); the default stevenblack feed is seeded on first list.
lindir-admin fleet hosts feeds list
lindir-admin fleet hosts feeds refresh --feed-id UUID
lindir-admin fleet hosts create --name default \
--tailscale-text $'100.1.2.3 Peer\n' \
--allowlist-text $'cdn.jsdelivr.net\n' \
--personal-blocks-text $'ads.example\n' \
--feed-id STEVENBLACK_FEED_UUID
lindir-admin fleet hosts assign --policy-id UUID --label-id LABEL_UUID
# on hosts:
sudo lindir-client sync
Admin UI: Compliance → Blocklists (/fleet/blocklists). API: /v1/fleet/hosts/feeds, …/feeds/{id}/refresh, /v1/fleet/hosts/policies, /v1/fleet/hosts/assign, /unassign. Inventory may include hostsPolicyId, hostsDesiredSha, hostsFeedShas.
Reboot recommended
CachyOS does not leave a reboot marker file — the desktop UI comes from the pacman hook cachyos-reboot-required (D-Bus notification only).
lindir detects reboot-needed as follows:
| Platform | Signals |
|---|---|
| Ubuntu/Debian | /run/reboot-required (+ optional .pkgs) |
| CachyOS/Arch | Hook message in job output (“Reboot is recommended…”); same package targets as cachyos-reboot-required.hook with the hook’s running-kernel heuristics; missing /usr/lib/modules/$(uname -r) after a kernel upgrade |
| Fedora/RHEL | needs-restarting -r when dnf-utils / yum-utils is installed |
| openSUSE/SLES | zypper needs-rebooting (exit 102) and /run/reboot-needed |
Results are stored on the package job (rebootRequired / rebootReason) and on each inventory snapshot so the Admin UI can show host-level status. Reboot itself is not performed automatically.
The agent also writes a boot-id sticky mark (reboot-required next to cachePath, default /var/lib/lindir/reboot-required) when live OS signals or a package job recommend reboot. After any reboot (manual or fleet reboot job), the next sync sees a new /proc/sys/kernel/random/boot_id, clears the mark, and forces an inventory upload (bypassing the usual 20‑minute throttle) so the Admin UI drops rebootRequired promptly. On Ubuntu/Debian the live /run/reboot-required marker is already gone after reboot; without the forced upload the dashboard could keep showing “reboot recommended” until the next scheduled inventory. Inventory includes bootId for debugging.
Other fleet features
| Piece | Role |
|---|---|
| Labels | Tags on hosts (manual assign/unassign or HBAC sync); used for package + energy + cert + blocklist targeting; Admin Hosts → Labels, GET …/labels/{id}/hosts |
| Certificates | ACME/Let’s Encrypt policies assigned to hosts or labels only (never allHosts); native client DNS-01 (eight providers) or HTTP-01; PEM install + optional reload hook — see Certificates and Certificates |
| Inventory | JSON snapshot each sync (OS, packageDetails, upgradable / upgradableCount, aptSources, crontabs, systemdTimers, systemdOneshots, systemdServices, systemdUnits, scheduler, osCodename, memory, rebootRequired, bootId). Package enumeration follows detectPackageManager() (distro-native), so an empty /usr/bin/rpm on Arch/CachyOS does not zero out pacman -Q. upgradableCount is the true total even if the detail array is capped |
| Apt sources | Flattened GET /v1/fleet/apt-sources (Admin Software → Apt sources, CLI fleet apt-sources); enable / disable / remove (remove-ppa = disable + package revert) |
| Cron | User crontab -u + /etc/cron.d/lindir-* — see Cron |
| Timers | System + user lindir-*.timer / .service — see Timers |
| Oneshots | Login/boot lindir-*.service oneshots — see Oneshots |
| Services | Always-on Type=simple lindir-*.service — see Services |
| Systemd | System unit catalog + operate jobs — see Systemd catalog |
| Upgrades | Packages with newer versions in local distro indexes; queue selected packages or full system upgrade from Admin |
| Vulnerabilities | Distro-aware findings (Ubuntu Security API, Arch AVG, OSV for Debian/RHEL/SUSE); see below |
| Queries / scripts | Generic shell jobs (admin-trusted; separate from package mgr) |
| Ops attention | GET /v1/ops/attention — stale sync, reboot needed, failed jobs, denials, upgradable counts, open vulns (Admin Dashboard) |
lindir-admin fleet inventory
lindir-admin fleet upgrades # all hosts
lindir-admin fleet upgrades --host-id UUID
lindir-admin fleet apt-sources # sources.list.d / PPAs from inventory
lindir-admin fleet apt-sources --host-id UUID
Admin UI: Software → Upgrades (/fleet/upgrades, optional ?hostId=). Upgradable counts on Inventory and the Dashboard link to that host’s upgrades. Queue upgrade all enqueues a full system upgrade (packages: [] → apt-get upgrade / pacman -Syu / dnf upgrade / zypper update); checkboxes + Queue selected upgrade named packages (batched at maxPackagesPerJob, default 50). Data comes from the last inventory sync (apt list --upgradable / dnf check-update / pacman -Qu / zypper list-updates); it does not refresh mirrors — queue a refresh package job first if indexes are stale.
Certificates
Host/label-scoped TLS certificates via a native ACME client inside lindir-client (no certbot). Prefer DNS-01 when a DNS provider is set; use HTTP-01 with a webroot otherwise. A host may have many policies (different domains and/or DNS providers); the client issues/renews each independently.
Operator narrative (challenges, discovery, verification): Certificates. Handbook path: Operator handbook §12.
| Piece | Detail |
|---|---|
| Targets | host or label only — API rejects fleet-wide assignment; host + label assignments all apply (deduped by policy) |
| Providers | gandi, hostinger, hetzner, ovh, ionos, infomaniak, scaleway, cloudflare (credentials stored encrypted; plaintext only on host pull). OVH: appKey:appSecret:consumerKey[:ovh-eu]; IONOS: publicprefix.secret |
| Install | certPath / keyPath PEMs per policy; optional reloadHook shell command |
| Renewal | Checked every sync; renew when within renewBeforeDays (default 30) of expiry |
| Status | POST /v1/client/fleet/certs/status per policy → Admin Compliance → Certificates |
| Audit | Real ACME issue/renew → fleet.cert_issued; failures → fleet.cert_issue_failed (actor host:…). Routine “still valid” check-ins are not audited |
| Discover | Admin enqueues scan jobs; client walks common PEM dirs (or --path); findings tag certs already managed by Lindir so they are not duplicated |
lindir-admin fleet certs create --name edge --domain example.com --domain www.example.com \
--dns-provider gandi --dns-credential "$GANDI_PAT" \
--cert-path /etc/lindir/certs/fullchain.pem --key-path /etc/lindir/certs/privkey.pem \
--reload-hook 'systemctl reload nginx'
lindir-admin fleet certs assign --policy-id UUID --host-id HOST_UUID
lindir-admin fleet certs status
lindir-admin fleet certs discover --host-id HOST_UUID
lindir-admin fleet certs discover-status
Staging dry-run: set acmeDirectoryUrl to https://acme-staging-v02.api.letsencrypt.org/directory.
Cron
Imperative jobs (same claim/result shape as packages), not desired-state overwrite of entire crontabs.
| Surface | What lindir touches |
|---|---|
| User | Enrolled usernames that exist on the host via crontab -u USER |
| System | Only /etc/cron.d/lindir-<name> (mode 0644, root:root) |
Does not edit /etc/crontab, foreign /etc/cron.d/*, or anacron scripts. Systemd timers are managed separately under Timers.
Ops: add / remove / enable / disable. Round-trip parse preserves blank lines, # comments, env lines, and jobs that were not targeted. Disable rewrites a job to # lindir:disabled <original line>; enable reverses only that marker — plain # comment lines are never treated as disabled jobs. Missing entryId → soft success note (like a missing apt source).
Requires root lindir-client sync (timer or manual). Inventory uploads crontabs[] with stable entry fingerprints (id), plus scheduler.cron / scheduler.systemdTimers availability (userAvailable / systemAvailable / available + reasons) so the Admin UI can show hosts missing crontab or systemctl without failing the whole sync.
lindir-admin fleet crontabs [--host-id UUID]
lindir-admin fleet cron list|pending|cancel --id JOB
lindir-admin fleet cron add --host-id UUID --scope user --username alice \
--schedule '0 * * * *' --command '/usr/local/bin/job.sh' --comment 'hourly'
lindir-admin fleet cron add --host-id UUID --scope system --cron-d backup \
--schedule '@daily' --command '/usr/local/bin/backup.sh' --user-field root
lindir-admin fleet cron disable --host-id UUID --scope user --username alice --entry-id HEX
Admin UI: Automation → Cron (/fleet/cron, optional ?hostId=). API: GET /v1/fleet/crontabs, POST|GET|DELETE /v1/fleet/cron/jobs. Inventory list exposes flattened cronUserAvailable / systemdTimersAvailable (etc.).
Systemd timers
Imperative jobs (same claim/result shape as cron/packages). Only lindir-<name>.timer + matching .service oneshot units.
| Scope | Units |
|---|---|
| system | /etc/systemd/system/lindir-*.timer + .service (root) |
| user | ~USER/.config/systemd/user/lindir-*.{timer,service} via systemctl --user -M USER@; optional linger (loginctl enable-linger) so timers run without an interactive session |
Does not touch foreign units, path timers, or non-lindir-* names. add writes both unit files, daemon-reload, and enable --now. disable / enable / remove operate on the timer unit; remove deletes both files. status runs systemctl status --no-pager -l (user or system scope) and stores the text on the job for the Admin UI. Optional workingDirectory sets WorkingDirectory= on the companion .service.
lindir-admin fleet timers [--host-id UUID]
lindir-admin fleet timer list|pending|cancel --id JOB
lindir-admin fleet timer add --host-id UUID --scope system --unit backup \
--on-calendar daily --command '/usr/local/bin/backup.sh' --timer-description nightly
lindir-admin fleet timer add --host-id UUID --scope user --username alice --unit mail \
--on-calendar hourly --command '/usr/bin/true' --linger
lindir-admin fleet timer disable --host-id UUID --scope system --unit backup
Admin UI: Automation → Timers (/fleet/timers). API: GET /v1/fleet/timers, POST|GET|DELETE /v1/fleet/timer/jobs. Inventory includes systemdTimers[] plus capability flags.
Systemd oneshots
Imperative jobs for Type=oneshot lindir-*.service units (no .timer). Distinct from calendar timers — use unique unit names (refuses names that already have a .timer).
| Scope | WantedBy | When it runs |
|---|---|---|
| user | default.target |
User session start / login (omit linger for login-only) |
| system | multi-user.target |
Boot |
Optional linger on user scope starts the user manager at boot (not login-only). add writes the unit, daemon-reload, and enable --now (runs once immediately as well as for future logins/boots). status runs systemctl status --no-pager -l on the oneshot unit. Optional workingDirectory sets systemd WorkingDirectory=. Timer companion .service files are ignored in inventory and refused for mutate.
lindir-admin fleet oneshots [--host-id UUID]
lindir-admin fleet oneshot list|pending|cancel --id JOB
lindir-admin fleet oneshot add --host-id UUID --scope user --username alice \
--unit login-setup --command '/usr/local/bin/on-login.sh' \
--timer-description 'setup at login'
# omit --linger for true login-only
lindir-admin fleet oneshot add --host-id UUID --scope system --unit boot-setup \
--command '/usr/local/bin/on-boot.sh'
Admin UI: Automation → Oneshots (/fleet/oneshots). API: GET /v1/fleet/oneshots, POST|GET|DELETE /v1/fleet/oneshot/jobs. Inventory: systemdOneshots[].
Systemd services
Imperative jobs for persistent Type=simple lindir-*.service units (Restart=on-failure). Distinct from oneshots and calendar timers — use unique unit names (refuses sibling .timer or oneshot-marked units).
| Scope | WantedBy | Notes |
|---|---|---|
| user | default.target |
Under ~USER/.config/systemd/user/ via systemctl --user -M USER@; linger defaults on so the daemon stays up without an interactive login |
| system | multi-user.target |
/etc/systemd/system/ |
add writes the unit, optional loginctl enable-linger, daemon-reload, and enable --now. Extra actions restart and status (systemctl status --no-pager -l). Optional workingDirectory sets systemd WorkingDirectory= (absolute path).
lindir-admin fleet services [--host-id UUID]
lindir-admin fleet service list|pending|cancel --id JOB
lindir-admin fleet service add --host-id UUID --scope user --username nicolay \
--unit myapp --command '/home/nicolay/bin/mydaemon' \
--working-directory /home/nicolay/apps/myapp \
--timer-description 'always on'
lindir-admin fleet service restart --host-id UUID --scope user --username nicolay --unit myapp
lindir-admin fleet service add --host-id UUID --scope system --unit mydaemon \
--command '/usr/local/bin/mydaemon'
Admin UI: Automation → Services (/fleet/services). API: GET /v1/fleet/services, POST|GET|DELETE /v1/fleet/service/jobs. Inventory: systemdServices[].
Systemd catalog
Observe + operate for the system manager (PID 1). Not desired-state: inventory lists units; jobs run systemctl lifecycle ops. Does not write unit files (use Automation → Timers / Oneshots / Services for lindir-*).
Inventory: each sync merges list-unit-files + list-units --all into systemdUnits[] (cap ~1500): unit, type, loadState, activeState, subState, unitFileState, description.
Ops: start | stop | restart | reload | enable | disable | mask | unmask | status. Denylist blocks mutating ops on dbus, systemd-logind/journald/udevd/networkd/resolved, and lindir-sync.* / lindir-client.service; status is allowed on those units (read-only). Admin polls the job and shows stdout in a modal after sync.
lindir-admin fleet systemd-units [--host-id UUID]
lindir-admin fleet systemd list|pending|cancel --id JOB
lindir-admin fleet systemd restart --host-id UUID --unit nginx.service
lindir-admin fleet systemd enable --label-id UUID --unit cups.service
Admin UI: Automation → Systemd (/fleet/systemd). API: GET /v1/fleet/systemd-units, POST|GET|DELETE /v1/fleet/systemd/jobs.
Vulnerabilities
Findings use distro security data, rolled up to advisories / security updates (one UI row per advisory when a DSA/AVG/USN covers many CVEs):
| Distro | Feed | Match rule |
|---|---|---|
| Ubuntu | Ubuntu Security API per source package | Host osId=ubuntu (or Ubuntu osCodename) + package version vs release status; rolled up by package + fixed version (USN id when present). Agents report osFamily=debian with osId=ubuntu. |
| Debian | Debian DSA list | One finding per DSA when installed source package < suite fixed version (e.g. linux on trixie < 6.12.111-1 → DSA-6528-1). DSA is used ahead of OSV /v1/query, which under-reports fresh kernel advisories. |
| Arch / CachyOS | Arch AVG | Pacman-style vercmp; one finding per AVG (CVE count in detail). CachyOS kernels aliased to linux / linux-lts / … |
| Fedora / RHEL / Rocky / Alma | OSV (Red Hat / Rocky Linux / AlmaLinux) |
Per package+version; rolled up to one finding per package (RHSA id when present) |
| openSUSE / SLES | OSV (openSUSE) |
Per package+version; rolled up (SUSE-SU id when present) |
Inventory must include packageDetails and (for Ubuntu) osCodename / (for Debian) osCodename or osVersion — collected on each agent sync.
lindir-admin fleet vulns refresh # background: Arch/Ubuntu/Debian-DSA/OSV feeds if cache older than 1 day
lindir-admin fleet vulns refresh --force # bypass daily TTL
lindir-admin fleet vulns scan # background: match inventory against cache only (no HTTP)
lindir-admin fleet vulns job # poll status (idle|running|done|error)
lindir-admin fleet vulns list [--host-id ID]
Admin UI: Software → Vulnerabilities (severity filter + sort; polls job status; Force refresh bypasses the daily TTL). Dashboard ops attention includes openVulns. From findings, operators queue work under Packages or Upgrades.
Arch / CachyOS: matching uses Arch AVG package names. CachyOS kernels (linux-cachyos*) are aliased to linux / linux-lts / … Zero findings are expected when installed versions already meet AVG fixed (or are newer than affected on open advisories). Scan fails loudly if Arch hosts exist but the AVG cache is empty — run Refresh first.
Caching: Scan reads only fleet_vuln_cache (one bulk load per job; payloads stay compressed until a feed key is used). Outbound Arch AVG / Ubuntu Security / Debian DSA / OSV API calls happen on Refresh, and at most once per day per feed (arch_feed_at / ubuntu_feed_at / debian_dsa_feed_at / osv_feed_at). Pass "force": true on refresh (CLI --force / UI Force refresh) to bypass the TTL. Ubuntu Security API pages at limit=20 (API max); a failed Ubuntu refresh does not stamp ubuntu_feed_at so retries are not blocked for a day. Refresh runs in a background thread with its own DB connection.
MySQL/MariaDB: large cache inserts need max_allowed_packet ≥ 64 MiB on the DB server — see Storage. Lindir does not raise this at connect time.
Config overrides (optional):
"fleet": {
"archSecurityUrl": "https://security.archlinux.org/issues/all.json",
"ubuntuSecurityBase": "https://ubuntu.com/security",
"debianDsaListUrl": "https://salsa.debian.org/security-tracker-team/security-tracker/-/raw/master/data/DSA/list",
"osvApiBase": "https://api.osv.dev"
}
Agent sync order
- IdM bundle (users/sudo)
- Console — claim pending interactive sessions and spawn a detached
console-sessionPTY worker (before long jobs so Admin → Console is not blocked) - Package jobs
- Cron / timer / oneshot / service / systemd operate jobs
- Energy policy (logind / DPMS / dconf; clears drop-ins if no policy)
- Hosts / blocklists (
/etc/hostsmanaged block; feed blobs cached by sha) - Inventory upload (throttled; forced after package jobs)
- Generic query/script runs
- MDM compliance report + lock/retire actions (see MDM & Compliance)
Host console (interactive shell)
Admin Hosts → Console (or a per-host Console link) opens an xterm.js session over a WebSocket on lindir-server. The enrolled agent claims the session on the next lindir-client sync (early in the run, before package jobs) and starts a detached lindir-client console-session process that relays a root PTY. Keep lindir-sync.timer enabled so that happens without SSH.
| Step | What happens |
|---|---|
| 1 | Admin POST /v1/hosts/{id}/console → session pending |
| 2 | Browser connects GET /v1/console/{id}/ws?ticket=… (short-lived single-use ticket from create; not the admin Bearer). Non-browser clients may use Authorization: Bearer on the upgrade. |
| 3 | Host sync: GET /v1/client/console/pending → claim → spawn detached PTY worker |
| 4 | Worker long-polls /io, posts /output (base64); hub bridges to the admin WebSocket |
| 5 | Admin DELETE /v1/console/{id} or disconnect closes the session |
Treat as root-equivalent, same as fleet scripts. Requires a recent agent that includes the console worker. The create response includes wsTicket (≈120s, single-use); POST /v1/console/{id}/ws-ticket re-mints one under Bearer auth if the WebSocket must be retried.
Behind nginx (or similar), the TLS location that proxies to lindir-server must forward WebSocket upgrades for /v1/console/:
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 3600s;
Without Upgrade / Connection, the Admin UI shows WebSocket error as soon as you open a console.
Security
- Host can only complete its own jobs and see its effective energy policy.
- Review package lists before enqueue.
- Full system upgrade is powerful — disable with
allowFullSystemUpgrade: falseif desired. - Fleet queries/scripts/console are root shell on the host — treat Bearer admins accordingly (OWASP Controls A05/A01).
- Energy policies may restart
systemd-logindwhen drop-ins change. - Sync timer must run as root for package and energy changes.
How the fleet surface behaves
- Vulnerability findings inform the operator; upgrades are queued deliberately under Packages / Upgrades.
- Host package jobs are noninteractive (no prompts on the host).
- Inventory includes installed packages (name/version/source); truncated only if the package payload exceeds ~900 KiB.
- Energy policies apply one idle/lid/screen profile per assignment.
- Console opens after the next host sync (or a manual
sudo lindir-client sync).
