Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,24 @@ any pair).

### Fixed

- **A failed remediation step no longer reports the host as restored.** When a
remediation handler failed partway, Kensa reported `rolled_back` with
`HostUnchanged: true`. The engine does not reverse a step whose own apply
failed, and it cannot know what that step changed before failing, so the
host could still carry a partial change while the record said it had been
restored. Those transactions now report `rollback_failed` with
`HostUnchanged: false`, which is the status that means the host is in an
unconfirmed state. Steps that succeeded and could not be reversed keep their
stranded marking, and the flag is never set on the step that failed.
Transactions whose steps all applied are unaffected: a clean reversal after
a failed validation still reports `rolled_back`.

In OpenWatch this moves the affected remediations from "Reverted, host
unchanged" to "Partially applied" with an inspection prompt. That is the
point: those hosts may need a look. It also means a handler that failed
without changing anything now reports an unconfirmed restoration, because
the engine has no evidence either way.

- **Evidence signature verification is fixed** for partially applied
transactions with stranded steps, and for returned evidence after a
terminal persistence failure. The engine settles every signed field before
Expand Down
47 changes: 32 additions & 15 deletions api/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,18 @@ const (
// evidence envelope is signed and persisted.
StatusCommitted TransactionStatus = "committed"

// StatusRolledBack indicates apply or validate failed and every
// applied capturable step was reversed using captured pre-state.
// The host is in the exact pre-change state.
// StatusRolledBack indicates apply or validate failed, no invoked
// Apply reported failure, and every applied capturable step was
// reversed using captured pre-state. The host is in the exact
// pre-change state.
StatusRolledBack TransactionStatus = "rolled_back"

// StatusPartiallyApplied indicates that for a transactional:false
// rule, at least one non-capturable step ran before a failure.
// Those steps are not reversed; per-step Stranded flags in
// [TransactionResult.Steps] identify which ones.
// [TransactionResult.Steps] identify which ones. A transaction that
// also has a failed invoked Apply reports [StatusRollbackFailed]
// instead; the Stranded flags are set either way.
StatusPartiallyApplied TransactionStatus = "partially_applied"

// StatusErrored indicates the engine could not complete a phase
Expand All @@ -38,14 +41,23 @@ const (
// that never mutated the host from one that left a mutation behind.
StatusErrored TransactionStatus = "errored"

// StatusRollbackFailed indicates apply or validate failed and the
// engine attempted to reverse the applied steps, but the restoration
// could NOT be machine-verified as complete (a rollback step failed
// or reported PartialRestore, or the recaptured post-state did not
// match the captured pre-state). The host is in an unconfirmed state;
// per-step detail is in [TransactionResult.RollbackResults]. Produced
// by the engine when a reversed step fails or reports a partial
// restore (the verdict-computed rollback status).
// StatusRollbackFailed indicates the host is in an unconfirmed state,
// from either of two causes. A rollback handler reported failure or a
// partial restore for a step it attempted to reverse; or an invoked
// Apply reported failure, whose effects are unresolved because the
// engine does not reverse a failed step and cannot know what it
// changed before failing. Per-step detail is in
// [TransactionResult.RollbackResults] and [TransactionResult.Steps].
//
// A transaction can reach this status with a rollback set in which
// every row succeeded: the failed step produces no RollbackResult,
// while earlier successful steps may have reversed cleanly. The
// status describes the transaction, not the rollback rows.
//
// Recapture is advisory and separate: the engine re-reads post-state
// and records, per reversed step, whether it byte-matches the captured
// pre-state. That finding is carried in the RollbackResult Detail for
// audit and does not determine this status.
StatusRollbackFailed TransactionStatus = "rollback_failed"

// StatusRecovered indicates an interrupted transaction (the process
Expand Down Expand Up @@ -277,9 +289,14 @@ type StepResult struct {
Success bool
// Detail is human-readable per-step output suitable for logs and UI.
Detail string
// Stranded is true for non-capturable steps that succeeded before
// a later failure left the rule [StatusPartiallyApplied]. Such
// steps are not reversed by rollback.
// Stranded is true for non-capturable steps that succeeded before a
// later failure in a transactional:false transaction. It is set when
// the transaction's verdict is [StatusPartiallyApplied] or
// [StatusRollbackFailed], before the evidence is built, and it stays
// set if a later evidence or persistence failure demotes the result to
// [StatusErrored]. A transaction whose verdict was Committed carries no
// stranded marking even if it is later demoted. Such steps are not
// reversed by rollback. The flag is never set on a step that failed.
Stranded bool
// Staged is true when the mechanism could not converge live state and
// instead wrote a reboot-deferred persist change (audit_rule_set on an
Expand Down
6 changes: 3 additions & 3 deletions docs/guide/05-rollback-and-history.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ terminal status the transaction lands in is what the log records:
| Status | How it gets there |
|---|---|
| `committed` | Every apply step succeeded and every validator passed. The host is in the target state; the signed evidence envelope (the Ed25519-signed record of what the transaction did) is persisted. This is the status `rollback` reverses. |
| `rolled_back` | Apply or validate failed, and the engine reversed every applied capturable step using the captured pre-state, in the **same run**. The host is back in its exact pre-change state. |
| `partially_applied` | A `transactional: false` rule ran at least one non-capturable step before failing. Those steps are not reversed; per-step `Stranded` flags say which. |
| `rolled_back` | The engine began reversing in the **same run**, and three things held: no apply step failed, every reversed step reported `Success: true` with no partial restore, and no successful non-capturable step was left behind. The host is back in its exact pre-change state. |
| `partially_applied` | The same conditions on apply and reversal held, but a `transactional: false` rule ran at least one non-capturable step that succeeded and cannot be reversed. Those steps keep their per-step `Stranded` flags, which say which ones. |
| `errored` | A phase could not complete within the deadline, or a terminal step (signing or persistence) failed. `HostUnchanged` distinguishes an abort that never mutated the host. |
| `rollback_failed` | Apply/validate failed and the engine tried to reverse, but the restoration could not be machine-verified (a rollback step failed or reported a partial restore). The host is in an unconfirmed state. |
| `rollback_failed` | The host is in an unconfirmed state, for either of two reasons. **An apply step failed.** The engine does not reverse a step whose own apply failed, and it cannot know what that step changed before failing, so its effects are unresolved; earlier successful capturable steps are still reversed. **Or a reversed step reported failure or a partial restore.** Either reason wins over `partially_applied`, and any successful non-capturable step keeps its `Stranded` flag. Recapture is recorded per reversed step for the audit record and does not decide this status. |
| `recovered` | An interrupted transaction (the process died after pre-state was persisted but before any terminal record) was reversed out-of-band by [`kensa recover`](#recover-crash-recovery). |

The key distinction: **`rolled_back`** is the engine undoing a failure
Expand Down
Loading
Loading