Make action runner rotation activation-safe

This commit is contained in:
Richard Courtman
2026-08-30 17:58:23 +01:00
parent c0b190bfa1
commit 770733fc92
27 changed files with 1062 additions and 189 deletions
+2 -2
View File
@@ -196,12 +196,12 @@ or generic command path.
| Platform or capability | Preferred boundary | Safe-profile behavior | Qualification and default implication | Residual owner and removal condition |
|---|---|---|---|---|
| Proxmox VE/PBS/PMG inventory, status, storage, and ordinary metrics | API-only connection with a narrowly scoped token; no host agent | No collector, helper, or runner authority is required for this data | Supported independently of the safe host-agent profile; it does not prove host-local SMART, LXC filesystem, or action parity | `agent-lifecycle`: keep API permissions and returned telemetry covered by provider tests |
| Standard Linux systemd host telemetry and collector update | Unprivileged `pulse-agent` plus the root-owned typed helper | Core `/proc`, filesystem, network, RAID, and hwmon telemetry stays in the collector; signed update activation crosses the fixed helper transaction | **Qualified on disposable Ubuntu 24.04.4 arm64 at committed main** for install, migration, explicit/automatic rollback, update, helper health, reporting continuity, and process/credential separation. Remains opt-in pending exact-RC reproduction and external review | `deployment-installability` and `security-privacy`: reproduce the twelve-scenario receipt from the designated release candidate and accept the external boundary review |
| Standard Linux systemd host telemetry and collector update | Unprivileged `pulse-agent` plus the root-owned typed helper | Core `/proc`, filesystem, network, RAID, and hwmon telemetry stays in the collector; helper-backed signed update activation is implemented but its live activation/recovery transaction is not qualified | **Qualified on disposable Ubuntu 24.04.4 arm64 at committed main** for install, migration, explicit/automatic profile rollback, helper health, reporting continuity, and process/credential separation. The receipt's ordinary update ran under the downgraded root monitoring profile and does not prove `agent_update.activate.v1`, executable-digest commit, watchdog rollback, interrupted recovery, or last-known-good restoration. Remains opt-in pending those live scenarios, exact-RC reproduction, and external review | `deployment-installability` and `security-privacy`: qualify helper activation/failure/recovery from the designated release candidate and accept the external boundary review |
| Linux SMART telemetry | `smart.snapshot` through the no-network helper; no caller-selected device or arguments | Implemented, unqualified on representative physical disks. Helper failure omits/degrades SMART only; the collector does not retry as root | Does not yet justify SMART parity or a default change | `agent-lifecycle`: record live SATA, SAS/controller, USB bridge, and NVMe evidence, including standby, permission failure, timeout, and partial-data cases |
| Proxmox node-local LXC filesystem telemetry | `proxmox.lxc_filesystems` through the no-network helper using fixed bounded `pct` operations | Implemented, unqualified on a representative PVE node. Helper failure omits/degrades this snapshot only | Does not yet justify Proxmox host-agent parity or a default change | `agent-lifecycle`: record live running/stopped LXC, mount, timeout, output-bound, and helper-loss behavior on supported PVE versions |
| Rootful Docker or Podman inventory | No direct collector access to a root-equivalent daemon socket | **Unavailable in the safe profile.** Migration disables the provider visibly. A closed helper `container.inventory` operation exists, but collector integration and live parity are not qualified | Rootful container parity is an explicit default blocker; the legacy/root profile is not safe-profile evidence | `agent-lifecycle`: either integrate and qualify bounded helper inventory or retain the explicit degradation permanently |
| Collector-owned rootless Docker or Podman | Direct access only to one usable runtime socket owned by the `pulse-agent` UID | Implemented, unqualified live. Ambiguous, root-owned, unreadable, unwritable, or unavailable sockets disable container monitoring | Does not yet justify container-runtime parity or a default change | `deployment-installability`: record fresh install, migration, restart, socket-loss, ambiguity, and telemetry parity on both rootless Docker and rootless Podman |
| Separate runner package update and package-cache cleanup | Root-owned `pulse-agent-runner`, host-bound action credential, typed request, postcondition, and durable receipt | A real verified apt-cache mutation, stale-fingerprint refusal, replay, rotation, and self-revocation are qualified in the committed-main systemd receipt | Qualified only for the exercised apt-cache mutation; it does not qualify every runner operation or an exact release candidate | `agent-lifecycle` and `api-contracts`: reproduce from the RC and add representative package-update success/failure/cancellation evidence |
| Separate runner package update and package-cache cleanup | Root-owned `pulse-agent-runner`, host-bound action credential, typed request, postcondition, and durable receipt | A real verified apt-cache mutation, stale-fingerprint refusal, replay, and self-revocation are present in the committed-main systemd receipt. Two-phase credential activation and nonce-bound readiness are implemented but need fresh production-path qualification | Qualified only for the exercised apt-cache mutation; the receipt does not prove the current Router/TLS/durable-persistence credential lifecycle, every runner operation, or an exact release candidate | `agent-lifecycle` and `api-contracts`: reproduce through the production Router over HTTPS from the RC, including failed activation/rollback and representative package-update success/failure/cancellation evidence |
| Separate runner Proxmox guest and container lifecycle/update actions | Root-owned runner with closed typed protocols; never the monitoring collector | Implemented, unqualified on representative PVE and container-runtime targets | No live-provider action-parity claim and no default change | `agent-lifecycle`: record target-bound success, stale-state refusal, cancellation, reconnect/replay, and independent postconditions on disposable real targets |
| Appliance, non-systemd, Windows, and macOS host-agent profiles | Platform API where sufficient; otherwise an explicitly named legacy/full-trust profile | **Unavailable for safe-profile apply.** The installer fails closed instead of silently installing a root-equivalent profile | Excluded from the Linux safe-profile claim | `deployment-installability`: land a platform-specific service, filesystem, update, helper, migration, rollback, and live-proof contract before marking that platform supported |
@@ -106,18 +106,28 @@ proving generic command denial.
The runner service persists the same normalized canonical hostname used when
its credential was issued; it must not substitute the machine's incidental OS
hostname when the collector was enrolled under an override. Credential
rotation invalidates exactly the superseded organization/token/agent/hostname
session only after the replacement token inventory is durably stored. That
invalidation removes the session from dispatch, closes its transport, and
unblocks server-side waits; an already-started host mutation remains governed
by its typed receipt and best-effort cancellation semantics rather than being
described as rolled back. Runner uninstall attempts an authenticated
rotation is a prepare/commit transaction. Issuance stores a ten-minute pending
replacement beside the active predecessor; the pending transport may register
but is not dispatchable. The runner first durably replaces a private health
marker carrying the current installer-generated activation nonce, then calls
the authenticated activation method. Activation atomically removes the exact
predecessor set, removes the replacement expiry/pending state, and promotes the
exact registered session. Failure before that commit leaves the predecessor
valid, while persistence failure restores both records. The installer removes
the prior marker before restart, ignores wall-clock mtime as authority, and
accepts only an activated marker whose nonce and canonical agent ID match the
current attempt; rollback never restores a prior marker. An already-started
host mutation remains governed by its typed receipt and best-effort
cancellation semantics rather than being described as rolled back. Runner
uninstall attempts an authenticated
credential self-revoke before deleting local state. The delete route may
remove only the caller's exact host-bound action-runner record; an unreachable
server cannot prevent local runner removal and leaves an explicit operator
revocation residual.
Runner readiness is exposed through a bounded, secret-free health marker that
is replaced atomically only after its contents reach stable storage. POSIX
is replaced atomically only after its contents reach stable storage. The
marker distinguishes registered/pending from activated and carries only the
per-attempt nonce, never the bearer credential. POSIX
targets must sync the containing directory after rename; Windows targets must
use a write-through replacement rather than attempting to flush the read-only
directory handle returned by the standard library. The marker and helper-update
@@ -39,12 +39,22 @@ monitored `agentId` / `hostname` pair and returns a new, separately persisted
runtime role `action-runner`, binding version 1, and capability
`typed_actions.v1`. The route re-resolves the pair against exactly one live,
non-conflicted, non-integration host in the request tenant. Issuing again
atomically replaces only the earlier action-runner record for that tenant and
agent ID; a persistence failure restores the complete token inventory and
returns no usable new secret. Only after that durable replacement may the
server close the exact superseded live action-runner session, remove it from
new dispatch, and unblock its outstanding server waits. The created response
is non-cacheable.
atomically prepares a ten-minute replacement beside the current active record;
older unactivated replacements are removed, while persistence failure restores
the complete token inventory and returns no usable new secret. The created
response is non-cacheable and exposes the activation-pending state and
deadline, not predecessor secrets.
`PATCH /api/agents/action-runner/credential` is the runner-owned activation
commit. It requires the current `agent:exec` bearer, exact organization,
canonical agent/hostname binding, and an exact registered pending transport.
A pending transport is visible to this commit proof but unavailable to action
dispatch. The commit clears the pending expiry, durably removes only the
server-recorded predecessor IDs, restores the complete inventory on persistence
failure, promotes the exact replacement session, and invalidates exact stale
sessions without allowing a caller-selected token ID. Repeating activation for
an already active exact session is idempotent so a lost HTTP response can be
reconciled safely.
`DELETE /api/agents/action-runner/credential` is the runner's narrowly scoped
self-revoke operation. It requires the current `agent:exec` bearer credential,
@@ -146,9 +146,14 @@ The same supported Linux systemd profile can install `pulse-agent-runner` only
through the separate `--enable-action-runner` choice, a private token file,
and the already selected typed helper profile. The runner binary, unit,
configuration, credential, health record, and receipt database are root-owned
and independent from collector state. Activation is transactional and restores
the previous runner-only files if health does not become current; disable and
uninstall remove only remediation and leave monitoring running. The action
and independent from collector state. Activation is transactional: the server
keeps the prior credential valid while a bounded replacement registers without
dispatch authority; the runner durably writes an installer-nonce-bound health
marker and commits activation before the installer removes backups. The
installer deletes the prior marker before restart, does not trust filesystem
timestamps, never restores a prior marker, and restores the previous runner-only
files if the current nonce never reaches activated state. Disable and uninstall
remove only remediation and leave monitoring running. The action
credential is never placed in argv or reused as the collector token. The
installer persists the canonical enrollment hostname for runner admission and
uses a private curl configuration for best-effort exact self-revocation before
@@ -89,16 +89,21 @@ set, so `ProtectSystem=strict` is not a valid runner sandbox claim. Its separate
credential, no-listener transport, typed admission, target and digest binding,
bounded capabilities, state privacy, and durable receipts are the compensating
boundary; collector and helper filesystem hardening remains independent.
Action-runner issuance and rotation are one durable host-bound transition per
organization and canonical agent ID. Re-issuance replaces the prior runner
record even when the monitored hostname has changed, returns the new plaintext
secret only after persistence succeeds, and restores the complete prior token
inventory if persistence fails. A successful rotation invalidates the previous
secret immediately; it never widens the collector credential or turns a
monitoring session into an action session.
Action-runner issuance and rotation are a two-phase durable host-bound
transition per organization and canonical agent ID. Re-issuance prepares a
ten-minute replacement credential while retaining the prior active record,
returns the new plaintext secret only after persistence succeeds, and removes
older unactivated replacements. The pending credential may authenticate one
exact runner transport but cannot become dispatch authority. After the runner
durably records its current activation nonce, its authenticated activation
request atomically promotes the replacement and revokes the server-recorded
predecessor set. A persistence failure restores both sides of the transition;
an unactivated replacement expires without revoking the predecessor.
That invalidation is exact and post-persistence: it matches organization,
token, canonical agent, hostname, runtime role, and typed capability before
closing the session, so a stale rotation cannot evict a replacement. The
closing the session, so a stale rotation cannot evict a replacement. Activation
is idempotent for the exact live session so transport-level response loss is
recoverable. The
runner may also delete only its own matching record using its bearer
credential; browser sessions and a caller-selected token ID are rejected, and
persistence failure restores the prior inventory. Installer teardown keeps the
@@ -34,10 +34,12 @@ preserves collector identity and installation files only; it must not rewrite
storage/recovery evidence or reinterpret a restored legacy collector profile as
recovery success. Typed `host.storage_cleanup` remains governed remediation,
not storage-recovery authority inferred from the credential itself.
Runner credential rotation follows the shared token-inventory commit boundary:
failed persistence restores the complete prior inventory and returns no new
secret, while successful persistence replaces the previous organization/agent
binding and invalidates only that superseded live session. Exact bearer
Runner credential rotation follows the shared two-phase token-inventory commit
boundary: issuance durably prepares a bounded, non-dispatchable replacement
without removing the active predecessor; activation after durable runner health
proof promotes the exact registered replacement and removes only its recorded
predecessor set. Failed persistence restores the complete inventory, and an
unactivated replacement expires without changing the predecessor. Exact bearer
self-revocation uses the same durable boundary and cannot select another token,
organization, or host. That rollback protects restart-time
credential truth only; it is not a customer backup, recovery point, restore