MDM (device compliance)

Compliance policies, lock/retire, seeded fleet-vpn, and OIDC device assertions.

MDM (device compliance)

lindir’s MDM layer provides compliance policies, custom discovery scripts, retire and lock remote actions, and OIDC Conditional Access via device assertions — a calm way to know a host’s posture before it opens an application.

What you get

Capability Behavior
Allowed distro/version Policy allowedDistros[{family,minVersion,maxVersion}]
Disk encryption requireEncryption — dm-crypt/LUKS on root (mapper/dm-)
VPN posture requireVpn — host reports mesh/VPN up (see seeded fleet-vpn below)
Local password mins Compare /etc/security/pwquality.conf (and PAM hints) to policy mins
Custom scripts Admin POSIX scripts → JSON stdout; customAssert on policy
Lock Queue loginctl lock-sessions on next sync
Retire Clear enrollment secret, mark host retired, agent clears local secret + energy drop-ins; VPN removeHost when a provider is configured
Conditional Access OIDC clients with requireCompliantDevice need a short-lived device assertion

Remote actions today: lock sessions and retire enrollment. Compliance can gate OIDC clients that require a compliant device.

Seeded baseline: fleet-vpn

On first start the server ensures an MDM policy named fleet-vpn with {"requireVpn":true} and keeps it assigned to every non-retired host (including each new enroll). A host reports noncompliant until the agent sees the mesh/VPN as up (Tailscale/Headscale/WireGuard/Pangolin as configured). LAN-only fleets that use vpn.provider: "null" should unassign or edit that policy if VPN posture is not required.

Admin UI Compliance → Policies lists it like any other policy. You may tighten rules or add labels; deleting the seed is possible but enroll will recreate the baseline assignment path unless you change server defaults.

Flow

  1. Admin creates a compliance policy and assigns host and/or label (or relies on seeded fleet-vpn).
  2. Optional: create a custom script; set rules.customScriptId + customAssert.
  3. Host lindir-client sync collects signals, runs scripts, POST /v1/client/mdm/compliance-report.
  4. Server evaluates → mdm_host_compliance (compliant / noncompliant / unknown).
  5. Audit action mdm.compliance.evaluated is recorded only when status or reasons change (unchanged posture on every sync does not spam Audit).
  6. For SSO apps that require a device: run lindir-client mdm-broker (listens on 127.0.0.1:1290); OIDC login page fetches /assertion.

Admin API (Bearer)

  • GET|POST /v1/mdm/policies, PUT|DELETE /v1/mdm/policies/{id}
  • GET /v1/mdm/policies/{id}/targets
  • POST /v1/mdm/assign / unassign — { policyId, hostIds[], labelIds[] }
  • GET|POST /v1/mdm/scripts, DELETE /v1/mdm/scripts/{id}
  • GET /v1/mdm/compliance, GET /v1/mdm/compliance/{hostId}
  • GET /v1/mdm/actions
  • POST /v1/mdm/lock / retire — { hostId }

Client API (Host auth)

  • GET /v1/client/mdm/scripts
  • POST /v1/client/mdm/compliance-report — signals JSON
  • GET /v1/client/mdm/actions / POST …/actions/{id}/result
  • POST /v1/client/mdm/assertion → { assertion, expiresIn }

CLI

lindir-admin mdm policy create --name laptop-baseline
lindir-admin mdm policies
# assign via API / Admin UI Compliance → Policies
lindir-admin mdm compliance
lindir-admin mdm lock --host-id HOST_UUID
lindir-admin mdm retire --host-id HOST_UUID

On the host:

sudo lindir-client sync
sudo lindir-client mdm-broker   # for OIDC apps that require compliant device

Admin UI

Compliance → Policies, Scripts, Devices. Platform → OIDC → Require compliant device.

Security notes

  • Retire clears the enrollment secret and rejects Host auth (host_unauthorized on sync). It also calls vpn.removeHost when a mesh provider is configured.
  • Re-enroll after retire: finish the retire action (host sync claims it) before enrolling again under the same hostname. If you enroll while a retire is still pending or the old secret was cleared mid-flight, the new secret can be wiped — sync then returns host_unauthorized. Run enroll a second time after retire has completed; use --skip-vpn if the device is already on the mesh.
  • Device assertions are HS256 JWTs (~5 min) signed with a server secret derived from bootstrap token — protect bootstrap credentials.
  • Localhost broker binds 127.0.0.1 only and sends Access-Control-Allow-Origin: * for the OIDC page origin; do not expose that port remotely.
  • Custom compliance scripts run as root during sync — same trust model as fleet scripts (OWASP Controls).

See also Fleet, Client, VPN, Admin Web UI.