Skip to content

2026.09 umbrella — native device-config actions + compliance-on-inventory #492

Description

@PaulDotterer

Goal

Close the native device-configuration gaps vs Intune-class tools (Intune Linux is mostly "run a script"; Fleet/JumpCloud/Landscape don't ship native firewall/cert/network actions either; Kandji is Apple-only but its catalog is mined for translatable ideas). Eight new action types + compliance-on-inventory. Design converged 2026-07-03. Scheduled 2026.09 (feature release; 2026.08 is stability/compliance, tracked separately). Decided out: SESSION_LOCK (lock without containment is theater), printer control (backlog, needs a full spec), update-deferral UX (backlog).

Key finding from design review: the SDK muscle largely exists already — sys/firewall (namespace-scoped Manager, nftables + firewalld), sys/dns (resolved + NM), sys/netconfig (multi-address InterfaceConfig, validate-before-mutate), sys/timesync (status + detect, chrony/timedatectl), sys/catrust (cross-distro anchors) — and the proto backend-enum scaffolds are in actions.proto. The work is proto params + server validators + agent executors + small SDK extensions. No parallel manager interfaces; backends bind at construction per the SDK backend pattern.

Action types

FIREWALL (ACTION_TYPE_FIREWALL)

  • Proto: enums (not strings) for direction/action/protocol/default policies, all fields @gotags validate; port ranges (port_start/port_end); backend enum ships NFTABLES + FIREWALLD only (implemented set)
  • Ownership via existing sys/firewall namespaces: manager constructed per action ID; flush = replace rules in this action's namespace only; ABSENT = remove namespace
  • SDK extension: default-policy surface on sys/firewall (Manager is rules-only today)
  • Self-preservation: agent injects implicit allows (established/related + its gateway connection) whenever a deny default applies; server validator refuses inbound-deny without an explicit management allow
  • Server validator pre-rejects firewalld-inexpressible rules (deny / src-dst scoping) instead of failing at execution
  • Decide reboot-persistence story (native persist vs documented reconcile window)
  • Document multi-action conflict semantics for default policies

CERT (ACTION_TYPE_CERT)

  • CA type routes through sys/catrust (named anchors, cross-distro) — no hardcoded Debian paths/tools
  • CLIENT type: PEM cert + key + owner/group/mode (validated octal), default paths overridable
  • reload_units (validated unit names) instead of free-form post_apply_commands — no arbitrary-exec channel inside CERT, no WS8 argv surface
  • params.privateKeyPem in redaction schemas; explicit at-rest decision (accepted-risk like Wi-Fi PSK, or sealed)
  • CertType gets 0 = UNSPECIFIED
  • Inventory fact: cert expiry (compliance-assertable)

HOSTNAME (ACTION_TYPE_HOSTNAME) — instant, dispatch-only

  • validate:"required,hostname_rfc1123" (or FQDN variant); reject CreateAction (all dispatch-surface lockdown as for instant types)
  • hostnamectl set-hostname; systemd required, fail closed
  • Handle or document Debian /etc/hosts 127.0.1.1 entry (stale entry breaks sudo name resolution)
  • Future: target for enrollment-token template variables (mechanism only; templates out of scope)

TIME (ACTION_TYPE_TIME)

  • SDK extension: sys/timesync gains SetTimezone/SetNTP (package is Status-only today) — no raw execs in the agent
  • Explicit backend enum (TIMESYNCD, CHRONY — implemented set); unset = timesync.Detect()
  • Invert set_rtc_to_utcrtc_in_local_time so the proto3 zero value is the safe default (no comment-vs-wire default lies)
  • validate:"timezone" on the IANA zone; PRESENT-only, documented

DNS (ACTION_TYPE_DNS)

  • Routes through existing sys/dns (Apply(Config)); backend enum ships RESOLVED + NETWORKMANAGER only
  • Add interface_name param — the NM backend requires it (host-global is resolved-only)
  • options constrained to an allowlist / validated key:value grammar, not free-form strings
  • ABSENT = revert to platform default (remove drop-in / per-connection reset)

NETWORK_CONFIG (ACTION_TYPE_NETWORK_CONFIG)

  • Params mirror sys/netconfig.InterfaceConfig: repeated addresses (dual-stack), gateway, DNS, MTU, routes; AddressMode gets 0 = UNSPECIFIED
  • SDK extension: VLAN (parent + vlan_id 1-4094). Bridges cut from v1 (no InterfaceKind yet); MANUAL mode only if a concrete use case exists
  • Commit-confirm revert in v1: stage old config, apply, auto-revert if the gateway is unreachable within N minutes (offline scheduler makes this natural). Minimum bar: detect the interface carries the gateway route and require explicit acknowledgment
  • ABSENT = revert to DHCP

SYSCTL (ACTION_TYPE_SYSCTL)

  • Proto: SysctlParams{ backend, repeated SysctlEntry{key, value} }; key validated against dotted-path grammar (^[a-z0-9_.-]+$-class regex + length caps), value length-capped, entry-count cap; all @gotags validate
  • SDK package sys/sysctl with the standard interface → backend structure: sysctl.New(Backend, runner) — explicit Backend enum even with one implementation today (BackendSysctlD = owned /etc/sysctl.d/ drop-in + sysctl --system), zero value invalid, ErrUnknownBackend; Manager with Apply/Remove/Get (Get reads effective /proc/sys values for idempotency)
  • PRESENT: write owned drop-in pm-<actionId>.conf + apply live; idempotent by comparing effective values. ABSENT: remove drop-in + re-run sysctl --system; document that keys not covered by remaining config revert only at reboot
  • No key allowlist/denylist — operator choice (consistent with repo-trust/gpgcheck philosophy); values are admin policy
  • Docs action-reference page anchored from birth; prime consumer of the CIS/NIS2 marketplace packs

REMOVABLE_STORAGE (ACTION_TYPE_REMOVABLE_STORAGE)

  • Proto: mode enum (UNSPECIFIED / BLOCKED / ALLOWED), @gotags validate
  • SDK package (standard Backend pattern, single backend today): modprobe.d deny for usb-storage/uas + udev deauthorize rule (v1 backend targets USB mass storage; the generic name leaves room for SD/MMC, eSATA, MTP later), udevadm reload; idempotency by reading owned files back; container tests at the file level
  • v1 semantics (documented): affects newly plugged devices — already-mounted media untouched until unplug; no vendor/serial allowlist in v1 (the scope cliff — later on demand)
  • Agent executor PRESENT/ABSENT; server validator; docs page anchored from birth; compliance fact candidate (removable_storage.blocked)

Compliance on top of inventory

(user direction: derive from inventory, no parallel posture pipeline; today compliance is fully script-based is_compliance SHELL detection)

  • CompliancePolicyRule gains a FACT type alongside SCRIPT — {fact_path, operator, value} reusing the dynamic-group condition evaluator (same operators, same device.inventory.* field resolution), feeding the existing compliance statuses/grace periods
  • Facts by derivation tier: T1 server-derivable from existing inventory (LUKS/root-encrypted via lsblk crypt containers, agent/OS/kernel versions); T1.5 server-known, no device data (PM-managed cert days_until_expiry from the stored PEM; assigned-action-converged from execution status); T2 inventory extension via the existing baseline collector (firewall active + default policies, pending security updates — SDK managers already read this state)
  • Web rule builder reuses the dynamic-query condition UX; scripts stay for the custom long tail
  • Synergy: one fact namespace serves dynamic groups (remediation targeting) + compliance + IdP device-trust integration groundwork — authentik as first target, vendor-neutral core #490 posture export
  • Docs: freshness contract — facts are "as of last inventory" (connect + interval + manual refresh); grace periods absorb the cadence

Triaged out (decided 2026-07-03)

  • Printer control → backlog, requires a full spec before any pickup. CUPS quirk surface (localized lpstat/lpadmin needs forced C locale, absent-CUPS handling, driverless-IPP-only, exact-set queue semantics). Queue provisioning via lpadmin -m everywhere is the v1 shape when it comes back.
  • UPDATE enforcement deadlines + user deferral → backlog. Maintenance windows already provide enforcement timing; restart notifications suffice.
  • Managed app catalog → marketplace base-template content, not server scope.
  • CIS/NIS2 hardening packs → marketplace base-template content (FILE-action definitions), not server scope.

Cross-cutting requirements (apply to every action)

  1. No accepted-but-ignored backend enum values — ship only implemented values; server validator rejects everything else fail-closed (cf. agent#169).
  2. No bool whose documented default contradicts the proto3 wire default — name bools so zero value = safe behavior.
  3. Every new enum has 0 = UNSPECIFIED; every field crossing the boundary carries @gotags validate.
  4. SDK-first: extend existing sys/* packages; backend selected at construction, never per call.
  5. Redaction-schema entries for every sensitive param; audit events for all state changes.
  6. Each new action ships its docref-anchored docs page + SDK docs claims.
  7. Per spec-driven-dev: each action type gets its own spec (acceptance criteria + rejection-paths table) approved before implementation; child issues per repo at work start.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions