Skip to content

Reserved audit store: layout, enforced reserve, and capacity reporting #775

Description

@sehkone

Reserved audit store: layout, enforced reserve, and capacity reporting

Summary

The registrar's detection story rests on two artifacts on the bootroot host: the verb-level records the daemon writes for every invocation of either verb, and the OpenBao file audit device that records the mint's OpenBao writes. Both are fail-closed inputs to a live security argument, and today they live in unrelated places with no shared budget and no measurement. This group gives them a bounded, measured home. It is a grouping issue and carries no work of its own.

Why this is not merely hygiene: the OpenBao file audit device is mandatorybootroot init refuses to proceed without it — and OpenBao fails requests it cannot audit. A filesystem that fills therefore does not just lose detection; it can stop OpenBao serving, which is the renewal path for every certificate in the deployment, on the one host that must not be restarted. The two artifacts are also written from different sides of the container boundary: OpenBao writes into a named volume as a Compose service, while the daemon writing the verb records is a host process. A budget covering only the volume leaves the records on the host root filesystem, where filling them still stops OpenBao if the two share a backing device.

The three children split by failure mode, not by subject matter, and that is deliberate. The layout child touches the installer, the Compose rendering and the Docker lifecycle. The enforcement child creates host mounts and takes a position on boot ordering. The capacity child is arithmetic, an alarm state machine and a health member. They fail in different ways and are reviewed with different eyes, so keeping them apart is what keeps each review competent.

All three are strictly ordered: layout, then enforcement, then capacity. Enforcement builds on the layout, and the capacity child lands after enforcement rather than beside it. That ordering is deliberate and worth stating, because an earlier reading had the probe independent of the enforcement mechanism:

  • Measuring the store is mode-dependent, so the mode has to be real first. In filesystem mode the store is its own filesystem and one statvfs call answers both usage and availability; in directory mode it shares a filesystem with the host, so usage has to be summed over the store's files. The probe implements both, but it cannot be exercised against the shipped default until that default exists.
  • An alarm over an unenforced number reports headroom nothing holds. Shipping the low-water alarm before the ceiling is real would put a green capacity member on the wire for a store whose only bound is a configuration value — the precise misreading the enforcement child exists to end, arriving instead through the surface built to detect it.

directory mode is unaffected by the ordering: it stays reachable by explicit configuration after enforcement lands, and the capacity child reports it through the enforcement member so a console can tell a kernel-enforced reserve from a configured estimate.

Children

  • Lay out the reserved audit store and move both audit writers onto it — Detection
  • Make the audit store's reserve a kernel-enforced ceiling — Detection
  • Probe the reserved audit store's capacity and report it on the endpoint — Detection
  • Complete audit capacity overflow verification — Detection (verify follow-up of the capacity issue)

Shared background

What this group buys, and what it does not — stated up front so no child's documentation overclaims. Isolation bounds the blast radius to the audit artifacts and protects everything else on the bootroot host; that is real, and a configured number alone does not deliver it. What it cannot deliver is that exhausting the artifacts "cannot stop OpenBao serving": OpenBao fails requests when its own device is unwritable, so a full audit filesystem stops it whether that filesystem is isolated or not — only sooner, and with the rest of the host intact. Closing that residual means bounding the device's own growth, which is a separate issue outside this group. No child here may claim in code comments, error text or documentation that the reservation keeps OpenBao serving.

The store bounds two writers, so both must actually be inside it. The layout child settles that for the records as a rule rather than an intention: with the endpoint enabled, audit_record_dir defaults to the store's records/ subdirectory and a configured value outside audit_store_dir fails validation. That rule is what the capacity child's usage number depends on — it measures the store subtree, so records living outside it would be reported on by nothing while the reserve claimed to bound them. The OpenBao device is inside by construction, through the rendered bind mount.

Measuring the store is not one syscall, and that is why the capacity child follows enforcement rather than sitting beside it. statvfs describes a filesystem, so in filesystem mode — where the store is a filesystem — it answers usage and availability together, while in directory mode the store shares a filesystem with the host and its usage has to be summed over the files. The capacity child specifies both. What it depends on is which mode the enforcement child made real, not how that child provisioned it: the probe never allocates an image, makes a filesystem or mounts anything, and it must not grow a fallback that measures a store the enforcement child failed to enclose.

Enforcement must need nothing the installer cannot assume. The objection that ruled a reserve out earlier is that bootroot cannot repartition a host and cannot assume a quota-capable filesystem. The shipped form answers that without either: a loopback-backed filesystem sized to the configured reserve, mounted by a systemd unit so it is restored on boot. It needs root on the bootroot host — which the endpoint-enabled daemon already has — a loop device and mkfs, and no free partition, no repartitioning and no host-filesystem quota support.

The ordering trap is the same one the endpoint's socket activation has, and it fails the same silent way. If the OpenBao container starts before the mount is up, its bind mount resolves to the underlying directory and OpenBao writes onto the host's root filesystem, with the reservation mounted over it later and empty — the reservation looks healthy and protects nothing. Mounting a fresh filesystem over the store also hides whatever the layout created underneath it, so the subdirectories must be created on the mounted filesystem, after the mount is up and before the container first starts. The daemon verifies at start that the store really is a mount point, and never degrades to the unenforced mode on a provisioning failure or infers it from a missing mount.

The whole group is conditional on the registrar endpoint being enabled, which is what keeps it additive. The endpoint is off by default and exists only on the bootroot host, so a deployment that never enables the registrar keeps its existing volume, gets no rendered override, and sees no change to its install path, its documentation or its E2E expectations. The residual belongs in the documentation rather than hidden: on a bootroot host that never enables the registrar, the mandatory OpenBao device keeps its existing unbounded behaviour. That is a pre-existing condition this group does not create, and widening the store to every deployment later is purely additive.

What fails closed is the registrar endpoint, never the daemon. Every check in this group — a missing or unwritable store, a store that is not the mount point the configuration claims — refuses to serve the verbs and reports an error naming the condition, while the daemon's fast-poll loop and per-service ACME renewal keep running. Serving the verbs onto storage that is not where the configuration says it is defeats the point; taking certificate renewal down over it would be a second and larger outage.

A ceiling tells nobody it is about to be reached, and the signals must leave the host. The daemon is not a control-plane protocol peer and has no channel of its own — the co-located registrar relays what the daemon exposes on the endpoint. So the capacity child carries not only its own alarm but the record store's anomaly count, malformed-line count and derived retention shortfall onto the endpoint's health container, reading the record store's existing reader and its existing window constant rather than re-deriving either. A second definition of the scan or the window would let the relayed values and the host-local ones drift silently apart. Absence before the first successful measurement and staleness afterwards are both visible on the wire by design; neither may be encoded as healthy.

One disjoint member of a shared container. The capacity child adds exactly one member to the health container the endpoint protocol issue reserved. The certificates member and the limiter member belong to other issues, all three are disjoint, none may reshape the container or touch a sibling's member, and there is no ordering edge in any direction. The snapshot holder the container is serialized from is shared on the same terms: the protocol issue requires one and ships none, so whichever member lands first creates it for the whole container rather than for its own member, and the others use it. Three holders on three cadences would assemble one wire object out of values of three different ages.

Execution order

Issues in the same wave have no unmet dependencies among these children and can run in parallel.

External dependencies:

graph TD
  issue772["#772 Lay out the reserved audit store and move both audit writers onto it [phase: Detection]"]
  issue773["#773 Make the audit store's reserve a kernel-enforced ceiling [phase: Detection]"]
  issue774["#774 Probe the reserved audit store's capacity and report it on the endpoint [phase: Detection]"]
  issue776["#776 Add the append-only record store and its on-disk format for registrar audit records [phase: Detection]"]
  issue776 -.-> issue772
  issue761["#761 Serve the registrar verbs on a root-owned, socket-activated unix socket [phase: Surface]"]
  issue761 -.-> issue772
  issue772 --> issue773
  issue773 --> issue774
  issue779["#779 Scan the audit store for anomalies and report them on `bootroot status` [phase: Detection]"]
  issue779 -.-> issue774
  issue762["#762 Implement the registrar endpoint's versioned wire protocol and its codec [phase: Surface]"]
  issue762 -.-> issue774
Loading

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions