Skip to content

Move audit records onto the reserve, at activation and when the reserve changes #926

Description

@AcoPiper

Move audit records onto the reserve, at activation and when the reserve changes

Context

The registrar's two audit artifacts share one directory on the bootroot host. [registrar] audit_store_dir (default /var/lib/bootroot/audit-store) is the only definition of where that store is; the daemon's verb records go to <audit_store_dir>/records and the OpenBao file audit device writes <audit_store_dir>/openbao/audit.log, bound into the container through a rendered Compose override.

Separate work makes audit_store_reserve_bytes a kernel-enforced ceiling in the default filesystem mode: a fully allocated loopback image at <audit_store_dir>.img, an ext4 filesystem made with mkfs.ext4 -m 0 -E nodiscard,lazy_itable_init=0, a generated systemd mount unit that mounts it at audit_store_dir and is restored on boot, and an audit bind declared create_host_path: false so Docker refuses to manufacture the bind source when the mount is not there. That work runs in three phases — bootroot derives, evaluates, preflights and renders in phase 1; the operator performs every step that changes the host in phase 2; bootroot verifies with metadata reads and reports in phase 3 — and reports exactly one outcome per run: enforced, provisioned, not activated, failed, or unenforced (directory) for an explicitly configured directory deployment. Two properties of it are the reason this issue exists:

  • The mount hides whatever is underneath it. A store that already holds records keeps them — intact, but invisible and outside the reserve — the moment a filesystem is mounted over it. So the underlying directory must be empty before activation.
  • One writer is held out by the deployment; the other is held out only by being stopped. The audit bind is declared create_host_path: false, so from the aside-rename onward — when <audit_store_dir>/openbao no longer exists — the OpenBao container fails to start rather than writing into whatever the path resolves to. The daemon's verb records are the other half, and nothing enforces them once a mount exists: a daemon-side gate refuses the verbs only while audit_store_dir is not a mount point, which stops holding the moment the mount is established part-way through. So the migration runs with both writers down throughout, and for the records that is the only mechanism — procedural, not a property of the mount.

Consequently a host that has ever written to the store is refused, not activated: its store is reported as provisioned but not activated, with nothing deleted, moved or mounted over. That refusal is safe and it is also a dead end. This issue is the way out of it — the procedure that relocates the existing contents onto the mounted reserve without losing a record and without leaving a half-finished state that reads as success.

One other operation moves the same records the same way, and it belongs here for that reason. Changing audit_store_reserve_bytes changes a size already committed to a formatted image holding records, and the rule that answers it is settled: the image is replaced, never resized, truncated or reformatted in place, in either direction, and bootroot infra up fails with a size mismatch naming both sizes and pointing at a documented procedure, rendering no image command at all. What does not exist yet is the procedure it points at, and writing it beside the refusal, thinner than the relocation contract, is how records are lost: two copy procedures over the same irreplaceable data will disagree, and the thinner one is the one someone follows in a hurry. So this issue writes both, and the second reuses the first's rules rather than restating them in a form that can drift.

The records matter enough to justify the care: the OpenBao device is mandatory and OpenBao fails requests it cannot audit, and the verb records are the only detection for an abuse whose signature is a rate.

Scope

Every step that changes the host is the operator's throughout, on the same terms as the activation work: the installer renders each step as a command to run verbatim, verifies the result by reading metadata, and performs none of it. Everything below is either a bootroot-side check, a rendered command, or — for the reserve-change procedure alone — a documented one.

That surface is three commands. bootroot init provisions the audit store from the [registrar] keys it reads through --agent-config, and is where the holding-directory check and the capacity arithmetic below live; bootroot reinit raises the same refusals in its pre-wipe preflight; and bootroot infra up, on which that same --agent-config is mandatory whenever the recorded registrar predicate is enabled or an audit override is rendered, is what an operator runs inside the maintenance window, because bootroot init bails on an already-initialised host. infra up runs the whole bootroot-side pass — rendering and verification alike — and refuses the bring-up before starting a container, which is what makes it and not init the command every rendered "run this next" instruction below names.

The holding directory is a first-class state

  • The path is derived, not configured: <audit_store_dir>.pre-mount. A sibling of the store in the store's own parent directory, like the image, and outside the filesystem about to be mounted. No new configuration key.
  • Its presence means a migration is in progress, and bootroot checks for it before any other verdict, in both enforcement modes. Without that ordering, an operator who completes the rename and stops there leaves a valid image, a valid mount and an empty store, and the next run reports the reserve as enforced over an audit trail stranded in a sibling directory. The check is an ordinary stat of a plain path that stays readable throughout — unlike the directory beneath a mount point, which cannot be named once the filesystem is mounted.
  • A pre-existing holding path is a refusal, never an overwrite. If <audit_store_dir>.pre-mount already exists where the migration would begin, bootroot renders no rename and fails naming the path. mv dir dir.pre-mount where the destination is an existing directory does not replace it — it moves the source inside it — so an unchecked re-run nests one store in another and the next copy reads the wrong tree.
  • bootroot never deletes, moves or modifies a holding directory. It may hold the only copy of records written before the reserve existed, so every operation on it is rendered for the operator and performed by them. Once it exists — the aside-rename in step 2 of the sequence below is what creates it — exactly two operations may change it, both renames and neither recursive: the closing rename to <audit_store_dir>.migrated, once all three verification comparisons have exited zero, and the rollback rename back to <audit_store_dir>, while the migration is still open. Nothing else touches it — no delete, no partial prune, no in-place repair.

The migration outcome

A new reported outcome, migration incomplete, sits ahead of every other verdict.

  • It is reported before the image contract, the artifacts and the mount are considered, and overrides all of them: however complete those are, the run fails under this outcome and never reports the reserve as enforced.
  • It names the holding path, states which step is outstanding, and renders what to run next: the capacity verdict and the copy where the contents fit, the verification and the closing rename where the copy is done, the resize or pruning route where they do not, and the rollback in every case.
  • Whenever the mount is up and the migration-set refusals below hold, it reports the three capacity figures below, so an operator can tell "finish the copy" from "this will never fit". A refusal — a mount point at or under the tree, a forbidden entry, or a figure that cannot be represented — is reported in its place and no figures are computed, the capacity verdict describing the same entries as the copy only where those refusals hold.
  • Where the mount is not yet up and the migration-set refusals below hold, what it renders is the activation itself — and it comes from the provisioning renderer, not from a second copy written here. The aside rename leaves the underlying store empty, so the non-empty-store containment that withheld those commands no longer applies, and this outcome is then the only verdict between the operator and an activated reserve. So migration incomplete calls the existing phase-1 renderer and reports its outstanding commands under this outcome rather than under provisioned, not activated: the image commands for the state the image is in, the artifact installation, the systemctl daemon-reload and the systemctl enable --now, closing with the re-run step naming the command actually invoked.
  • With the mount absent, a migration-set refusal withholds the activation as well as the copy, and that follows from the same refusals-first ordering. A refusal decided ahead of the mount state — a mount point at or under the tree, or a forbidden entry — arises under either mount state. With the mount up it keeps the activation: the reserve is already there, nothing in the list creates the mount, and the artifacts the copy still needs are worth installing while the operator clears the entry. With the mount absent it withholds it, because the rollback rendered beside an activation is built for the mount state the run observed: rendered with the mount absent it carries no umount, so an operator who ran the activation and then took its rollback would meet rmdir against a live mount point, and under sh -e the sequence would stop before the restoring rename. The operator clears the entry first; the pass after that carries no refusal and renders the activation as usual. A holding path that is not a directory withholds it under both mount states, no copy being reachable from such a path on any pass.
  • One step is withheld from that rendering, because the copy is what satisfies it. The subdirectory step — the mkdir, chmod and chown of records/ and openbao/ on the mounted filesystem — is not rendered while a holding directory exists, and their absence on the mounted store is not reported as remediation either. The copy recreates both with the source's own ownership and modes in one operation, where a directory made by hand ahead of it is a set of attributes the verification then compares against the source's anyway. The provisioning renderer emits that step as its own unit so this outcome can drop precisely it.
  • In directory mode, where none of the enforcement machinery applies, a holding directory is still named — as an unfinished migration whose contents are the only copy of those records, not as debris.

While a holding directory exists, bootroot creates nothing on the mounted store, and both writers stay down

So <audit_store_dir>/openbao stays absent for the whole duration, and a bring-up attempted during the window is refused by the migration incomplete outcome before any container starts. On the automatic boot path, where no bootroot verdict runs, the absent bind source is what keeps the OpenBao container out: the rendered override declares it create_host_path: false, so the container fails to start instead of manufacturing it. The daemon has no equivalent once the mount is up, so step 1 of the sequence — both writers stopped, for the whole window — is what keeps its records out.

It is also why no rendered command creates them either: the subdirectory step the provisioning renderer would otherwise emit is withheld for the whole window, and the copy is one whole-subtree operation that recreates records/ and openbao/ with their original ownership and modes in a single step.

Completing this procedure is also what makes the bind declaration effective on that host, and both manuals say so. create_host_path: false governs creation only: a store still holding openbao/ on the underlying filesystem is bound as it stands, so until this procedure moves those contents aside a boot with a failed mount still writes the audit device to the root filesystem. The aside-rename empties the underlying directory and the copy puts both subdirectories on the mounted filesystem, after which the bind source is absent whenever the mount is, and the guard bites. This issue retires that residual; it adds no new mechanism for it.

The capacity check is against the mounted filesystem, not the reserve

audit_store_reserve_bytes is the image's nominal size. The ext4 filesystem inside it holds less — the journal and the metadata are real and already allocated — and an existing OpenBao audit log, which nothing in this deployment bounds and OpenBao does not rotate, can exceed either figure. A check against the configured reserve therefore passes cases that then fail part-way through the copy, leaving a partially populated mounted store beside a holding directory.

  • Once the mount is up and a holding directory is present, bootroot compares the actual available bytes on the mounted filesystem — f_bavail × f_frsize from a statvfs of audit_store_dir, the space available to an unprivileged writer rather than f_bfree, which counts blocks reserved for root — against the holding directory's allocated size.
  • It requires a margin above the source figure, a copy being able to allocate more than the source did through block rounding and per-directory metadata. The margin is a named constant at the top of its file.
  • When it does not fit, no copy command is rendered at all. The run reports migration incomplete naming the available figure, the source figure and the margin, and points at the two ways forward: a larger reserve through the documented replacement procedure, or pruning the OpenBao audit log before retrying. It never renders a copy it expects to fail.
  • Summing the source calls the allocated-size subtree walk the provisioning work already ships, rather than restating its rules in a second implementation. That walk sums a subtree's allocated bytes — symlink_metadata on every entry and never a traversal through a symlink; st_blocks times its fixed 512-byte unit, itself a named constant, rather than apparent length; each distinct (st_dev, st_ino) counted once; and no descent across a device boundary — and it is called here against the holding directory. Where it needs a different root or a caller-visible per-entry error, generalise it in place; never copy it, because two walks that disagree about a hard link are two different answers to "does it fit". What it produces here is a one-shot migration figure and not a capacity probe: it computes no headroom, raises no alarm and touches no health response.
  • Every byte figure here is checked arithmetic, and one that cannot be represented is a refusal rather than a wrapped number. f_bavail × f_frsize, st_blocks times the 512-byte constant, the running sum over unique (st_dev, st_ino) entries, and the source figure plus the margin are each a checked_mul or checked_add on u64. A None at any of them reports migration incomplete, names the figure and renders no copy. * and + wrap silently in a release build, so an unchecked product is a fit check that can turn "does not fit" into a pass; never as, never unwrap.

One rule for what is in the migration set

The measurement, the copy and the verification must cover exactly the same entries, or the capacity verdict is answering a question about a different set than the copy will move. Two conditions would otherwise split them, and both are refusals rather than special cases:

  • A mount point anywhere at or under the holding directory is refused, and the test for one is mount identity rather than device. A mount --bind of a directory on the same filesystem has the same st_dev, so find -xdev and cp --one-file-system both walk into it and the bind source's contents get counted, copied into the reserve as if they were audit data, and duplicated on the host.
    • The detection is /proc/self/mountinfo: read it once and refuse when any mount point is the holding directory or lies underneath it. One read answers it for every mount regardless of device and needs no crate beyond the standard library; the STATX_MNT_ID alternative would need a statx per directory and a kernel floor this repository does not otherwise impose.
    • Unescape the mount-point field before comparing it. mountinfo encodes space, tab, newline and backslash as \040, \011, \012 and \134, and comparing the raw field against a real path silently fails to match exactly the names that matter here. Compare path components, not string prefixes, so <store>.pre-mount-other is not read as being under <store>.pre-mount.
    • A match is reported under migration incomplete naming the mount point, and the operator unmounts it or moves it out before retrying.
  • A symlink is refused too, with the device nodes, FIFOs and sockets, naming the path, under migration incomplete and with no copy rendered. This is a migration rule of this work's own, stricter than anything the store enforces today, and it is stated as that rather than as a restatement of the contract: what the store refuses is a symlink at a named path and nothing walks arbitrary entries, so a symlink at openbao/audit.log — inside the one directory the layout contract deliberately leaves to the container's entrypoint — is refused by nothing on the default branch. Migration refuses it for two reasons of its own. It is the one entry that can put audit bytes outside the ceiling this effort imposes, cp -a recreating a link pointing wherever it pointed before. And carrying one is not verifiable at acceptable cost: a manifest recording path and target as two variable-length fields cannot tell a b linked to c from a linked to b c. Nothing in the deployment creates one, so the rule costs a correct deployment nothing.

With those refused, the tree holds only directories and regular files, which is what lets the three manifests below describe it completely: every entry is a directory, or a regular file whose bytes the content pass hashes.

bootroot's refusal is a render-time verdict and cannot be the whole guarantee. The operator's cp -a comes later, and -a carries --no-dereference, so an entry planted in between is copied as itself and then stands identically on both sides — same %y, same path — which the metadata comparison cannot fault and the other two passes skip, both being -type f. So the sequence renders a type guard: find <tree> -xdev -mindepth 1 ! -type d ! -type f -exec false {} +. It exits non-zero both when it finds a disallowed entry and when the walk itself fails, and exits 0 only over a fully walked tree of directories and regular files — no pipe, and no tool the verification does not already name. It reports by exit status alone; the path is named by the next bootroot pass. It runs twice — immediately before the copy, so a long copy is not spent on a set that will be rejected, and over both trees as the first verification pass, which is the run that carries the guarantee: any non-regular entry present while the copy ran is caught there, before the closing rename, whenever it was planted. A check is still a moment earlier than the command it authorises, so the window covers the rest: nothing under the source tree changes between a check and the command it authorises, in both manuals beside the mount-change precondition it belongs with.

The device restriction is carried by every operation in its own right, the enumeration underneath all of them being a single find -xdev, so a different-device mount appearing at any time is excluded from the measurement, the copy and all three comparisons alike and they cannot disagree over one. A same-device bind mount is the other instance of the same limit, and it is stated rather than implied: only the mountinfo read catches one, -xdev being blind to it, and bootroot reads mountinfo when it renders while the operator copies afterwards — so a bind appearing in between is inside none of bootroot's checks and inside all three of the operator's operations, and the failure is a bind source's contents copied into the reserve as audit data with both sides then matching. So the guarantee is conditional on the same maintenance window: no privileged mount change between the run that renders a step and the operator running it. bootroot re-reads mountinfo at every pass, so a bind still there at the next pass is refused; what no check covers is one that comes and goes between two of them.

The exact rendered commands

bootroot renders commands an operator runs verbatim, so the tools and flags are pinned here rather than left as a contract. The bootroot host is GNU/Linux — systemd units, ext4, loop devices — so GNU coreutils and findutils are present; two prerequisites are named up front rather than failing halfway through: diffutils for cmp, which a stripped host may lack, and coreutils 8.25 or newer, where sha256sum gained --zero.

Every manifest is NUL-delimited and compared byte-for-byte, because part of the source is attacker-influenced by design. openbao/ is chowned to the OpenBao container's uid, so every filename under it is whatever that container wrote — including one holding a newline, which a line-delimited manifest cannot represent unambiguously: a crafted name can split one record into two, forge a record that matches the other side, or hide an entry behind what looks like a complete line. So find emits \0-terminated records, sort -z orders them, and cmp compares the two files as bytes. Nothing in the chain parses a line.

No rendered command contains a pipe, and that is a correctness rule rather than a style one. A pipeline's exit status is its last command's, so find … | sort -z and find … | cmp both report success when the walk failed and the consumer merely saw a short or empty stream — an incomplete tree read as a clean one, which is the single direction this sequence must never fail in. set -o pipefail would fix it but is not in every sh an operator may paste into. Each stage is instead its own command over its own file, and the sequence stops at the first non-zero exit: find … > <scratch>/<name>.raw, then sort -z -o <scratch>/<name> <scratch>/<name>.raw, then for the content pass xargs -0 -r sha256sum --binary --zero < <scratch>/<name> > <scratch>/content.<side>, and finally cmp over the two sides' files. <scratch> is a mktemp -d directory outside both trees, removed by the operator afterwards — inside either, the manifests would describe themselves.

  • The copy is cp -a --one-file-system <source>/. <destination>/. -a is -dR --preserve=all, and the -d in it is --no-dereference --preserve=links: nothing is followed and hard links stay shared, which is what the measurement walk counted when it counted each (st_dev, st_ino) once. The trailing /. copies the directory's contents, so records/ and openbao/ are recreated on the destination with their original ownership and modes in one operation.
  • The verification is the type guard above over both trees, then three comparisons, and all four must pass. None modifies anything. Each is produced from both sides after cd into that side, so recorded paths are relative and directly comparable, and each uses -mindepth 1 so the container directory's own mode is excluded — set by the mount, checked elsewhere, and legitimately different between the two.
    • Metadata, over every entry including directories: find . -xdev -mindepth 1 -printf '%y %m %U %G %n %P\0', staged and sorted as above, the two sides compared with cmp. That is type, octal mode, numeric uid and gid, hard-link count, and the relative path. The path is the only variable-length field and it is last, so the record's own NUL delimits it and no name the container wrote can be read as ending elsewhere. A second variable-length field after it would destroy that: the split between two space-separated names is not recoverable from the bytes.
    • Regular-file sizes: find . -xdev -mindepth 1 -type f -printf '%s %P\0', staged the same way and compared with cmp. Sizes are their own pass because a directory's size legitimately differs between two filesystems and would fail every correct migration. Modification times are excluded for the same reason: cp -a preserves them, but timestamp granularity differs between filesystems.
    • Content, over regular files: find . -xdev -mindepth 1 -type f -print0, staged, sorted, then fed to xargs -0 -r sha256sum --binary --zero from the file, the two outputs compared with cmp. It reads every byte — a truncated file of the right length is exactly what a size comparison misses. --zero keeps this pass NUL-delimited and is not optional: without it sha256sum newline-terminates each record and escapes a filename holding a newline or backslash, which is a line-delimited manifest whatever fed it.
  • A mismatch is detected by exit status, not by reading output: cmp exits non-zero, and the rendered sequence stops there.
  • The step that closes the migration is a rename, not a deletion. Once the guard and all three comparisons exit zero, bootroot renders mv <audit_store_dir>.pre-mount <audit_store_dir>.migrated — a separate step, never chained onto the copy or the verification. It clears migration incomplete, and it is safe under every condition here: rename is atomic, traverses nothing, opens nothing and destroys nothing, so a mount appearing between the check and the command cannot turn it into data loss. A directory that contains a mount renames with it intact; one that is a mount fails EBUSY.
    • Reclaiming the space is outside the guaranteed sequence. <audit_store_dir>.migrated holds a redundant second copy outside the reserve. bootroot reports its path and size under the normal outcome as reclaimable — never as migration incomplete — and the documentation tells the operator to confirm nothing is mounted under it before removing it. It gets the same treatment as the holding path: bootroot refuses, naming it, rather than renaming onto a path that already exists.
  • bootroot performs none of the copy, the verification or the closing rename. What it enforces is that none can be skipped: while a .pre-mount holding directory exists, the run reports migration incomplete and refuses to call the store enforced.

Resume and rollback

Both are rendered, both are idempotent, and bootroot performs neither.

  • Resume re-runs the same copy over a partial one and then the verification. Nothing about a partial copy makes the source unusable, because the source is never modified.
  • Rollback unmounts the store, removes the now-empty mount point with a non-recursive rmdir and renames the holding directory back. The partial copy on the mounted filesystem is discarded with the unmount, which is safe precisely because the original was never touched. It is offered only while the holding directory still carries its .pre-mount name: once the closing rename has run the migration is complete, the store is authoritative, and there is nothing to roll back to.

The rendered sequence

Every step below is either a command bootroot renders for the operator to run verbatim or a bootroot infra up pass that renders the next ones. Nothing in it depends on output from a run that cannot happen.

  1. Stop both writers: the Compose stack, so no container holds a bind under audit_store_dir, and bootroot-registrar.service, so no verb record is written into the store while the migration is in flight. Restarting OpenBao afterwards costs a manual shamir unseal, so this is a scheduled maintenance window rather than something attempted live.
  2. mv <audit_store_dir> <audit_store_dir>.pre-mount, then install -d -m 0700 -o root -g root <audit_store_dir> to recreate the mount point empty and root-owned.
  3. Run bootroot infra up --agent-config <path>. It finds the holding directory, reports migration incomplete and starts no container. The underlying store is empty again, so what it renders here is the outstanding activation: the image commands, the installation of the three artifacts, the systemctl daemon-reload and the systemctl enable --now <escaped audit_store_dir>.mount — and not the subdirectory creation, which step 6 performs instead. bootroot cannot see that the writers are already down, so the stop-both-writers step appears in that list too; step 1 has satisfied it.
  4. Run those commands. The mount is now up and the store on it is empty.
  5. Run bootroot infra up --agent-config <path> again. Still migration incomplete and still starting no container, it now reports the capacity verdict against the mounted filesystem and renders the copy — or, where the contents do not fit, the two routes out instead.
  6. The type guard over the holding directory, and only where it exits 0, cp -a --one-file-system <audit_store_dir>.pre-mount/. <audit_store_dir>/ — one whole-subtree copy preserving attributes and hard links, and the step that brings records/ and openbao/ into being on the mounted filesystem.
  7. The three rendered verification comparisons — metadata, regular-file sizes, then content — after the guard has been run over both trees.
  8. mv <audit_store_dir>.pre-mount <audit_store_dir>.migrated. This closes the migration; deleting that directory is later, optional housekeeping and not part of the sequence.
  9. Run bootroot infra up --agent-config <path>. With no holding directory left it verifies the store on the mounted filesystem, reports the reserve as enforced and starts the Compose stack; then unseal OpenBao and start bootroot-registrar.service. The writers have been down since step 1, so nothing wrote into the store while the copy and its verification were in flight.

Replacing the image when the reserve changes

A changed audit_store_reserve_bytes moves the same records across filesystems, so it is held to the contract above — the migration-set refusals, the type guard, cp -a --one-file-system with the /. source suffix, the three staged NUL-delimited comparisons and the exit-status rule — reused as they are pinned there rather than re-spelled in a second form that can drift from them. It is documented rather than rendered, and that asymmetry is deliberate: a size mismatch is a failed outcome carrying no rendering, so there is no verdict that could emit these commands, and widening that outcome into a renderer is not this issue's work. bootroot is invoked at the ends, not through the middle: steps 2 to 5 are the operator's, while steps 1 and 6 are bootroot infra up passes that change nothing on the host.

  1. Update audit_store_reserve_bytes. bootroot infra up --agent-config <path> then fails with the size mismatch, naming both sizes and pointing at this procedure. A value below the installer's minimum reserve is refused by that check first, so the procedure never starts against a size that cannot be formatted.
  2. Stop both writers: the OpenBao container — a scheduled stop, needing a manual shamir unseal on restart — and bootroot-registrar.service. They stay down until step 5 completes.
  3. Create the new image at <audit_store_dir>.img.new — the same parent directory, so the closing rename is atomic within one filesystem — at the new size, fully allocated, uid 0, mode 0600, and mkfs.ext4 -m 0 -E nodiscard it. The free space needed is the new image's full size: the old image's bytes are not reclaimed until step 6.
  4. Mount the new image at a temporary mount point, then run the migration-set refusals against the live store and gate the copy on capacity, in that order — the figure describes the same entries as the copy only if the refusals hold. The capacity gate is the one part not already pinned above, because its two figures come from a live mounted store rather than a holding directory: du -s -x --block-size=1 <audit_store_dir> against df --block-size=1 --output=avail <temporary mount point>. -x is not optional — plain du descends into a nested mount and returns a figure for a larger set than the copy will move — and --apparent-size is excluded, reporting a different and smaller number. The copy proceeds only where the source figure plus the same margin the activation check adds is at or below the available figure, the manual stating that margin as a concrete number rather than naming the constant. Where it does not fit the procedure stops here with nothing copied, and the two routes out are a larger new reserve or pruning the OpenBao audit log first. A grow is gated by the same comparison and passes it.
  5. Run the type guard, the copy and the three comparisons exactly as pinned above, with <audit_store_dir> as the source and the temporary mount point as the destination, stopping at the first non-zero exit. Only once all four pass: unmount the temporary mount, stop the live mount unit, mv <audit_store_dir>.img <audit_store_dir>.img.old, rename <audit_store_dir>.img.new into place, start the mount unit, then start the OpenBao container, unseal it, and start bootroot-registrar.service.
  6. Run bootroot infra up --agent-config <path>. It verifies the image against the updated reserve and reports enforced. Only then does <audit_store_dir>.img.old become removable, and by the operator: bootroot renders no delete here either, reporting that file's path and size as reclaimable.

Resume and rollback, both idempotent and neither performed by bootroot. Resume re-runs the copy over a partial one, then the guard and the comparisons; the source is the live store and is never modified. Rollback is defined at both positions: before the rename in step 5, unmount the temporary mount and delete <audit_store_dir>.img.new, the live store never having been touched; after it, stop the mount unit, rename <audit_store_dir>.img.old back and start the unit — which is why .img.old survives until step 6 reports enforced. A failure at any step leaves either the old image or a verified new one in place, never neither.

Two facts that stop the procedure being mistaken for a different one. No mount-unit directive — What=, Where=, Type=, Options= — carries the size, so the re-rendered unit is normally byte-identical; where it differs, the outstanding install and systemctl daemon-reload are reported as ordinary artifact drift. And a changed audit_store_dir is not this procedure at all: it moves the mount point rather than the image.

Documentation

Document, in both docs/en/ and docs/ko/, on the existing audit-store pages with no mkdocs.yml nav change and no second audit-store section. Each subject below is specified above; the manual states it for an operator, and no passage claims a guarantee the work does not deliver. The two procedures share one passage and one set of pinned flags: where they would differ, the passage is corrected rather than the contract forked.

  • The maintenance window and what the procedures assume of it: both writers stopped throughout; the operator running the rendered commands in sequence; no privileged mount change between a rendering pass and the command it rendered; and no change under the source tree between a check and the command it authorises.
  • That a store already holding records is refused rather than activated, and why; the holding directory, what its presence means, and that it is never deleted automatically. And that such a store keeps the unattended-boot exposure until this procedure completes — the audit bind's create_host_path: false declaration governs creation only, so a pre-existing openbao/ is bound as it stands — with completion named as what retires it.
  • The capacity check, the three figures it reports, and the two routes out when the records do not fit.
  • The type guard and the three comparisons that gate the closing rename; that a symlink, like a device node or a FIFO, is refused rather than migrated, as this work's own stricter migration rule.
  • That the migration closes with a rename rather than a delete, and that removing <audit_store_dir>.migrated is later operator housekeeping, done only after confirming nothing is mounted under it.
  • Resume and rollback for the activation sequence, and the sequence's shape — that the two bootroot infra up passes before the copy are what render the activation and then the copy — with the whole rendered sequence written out as commands an operator runs, including that both writers are stopped for its duration and that restarting OpenBao costs a manual shamir unseal.
  • The replacement procedure for a changed reserve, written into the passage that already carries the replace-and-migrate rule and the size-mismatch refusal: all six steps with the privilege split explicit, the migration-set refusals and the type guard ahead of the capacity gate, du -s -x --block-size=1 against df --block-size=1 --output=avail with the margin as a concrete figure, -x documented as required and --apparent-size as excluded, the guard and three comparisons ahead of the image rename, rollback at both positions, and .img.old retained until the closing verification. Neither page carries a resize2fs, a truncate, a du without -x or any other in-place resize of the image, and neither documents a delete before a verified copy exists.

Acceptance criteria

Scope's text is the specification; these are the outcomes that prove it, and each is checked against the rule of the same name there.

  • A <audit_store_dir>.pre-mount holding directory — a derived sibling of the store outside the filesystem being mounted, with no new configuration key — is detected before every other verdict, in both enforcement modes, yields migration incomplete in filesystem mode, which fails and overrides an otherwise complete image, artifacts and mount, and is named as an unfinished migration in directory mode.
  • bootroot never deletes, moves or modifies a holding directory; the only sanctioned changes are the two rendered operator renames — the closing one after the guard and all three comparisons pass, and the rollback one while the migration is still open. bootroot refuses, naming the path, and renders no rename where a .pre-mount or .migrated path already exists.
  • While the mount is absent, a holding directory exists and the migration-set refusals hold, the run renders the outstanding activation commands under migration incomplete — the image commands, the artifact installation, the daemon-reload and the systemctl enable --now — from the existing phase-1 renderer rather than a second copy defined here, closing with the re-run step named for the command invoked.
  • With the mount absent and a migration-set refusal standing, the run reports the refusal and withholds the activation, so no rendered list brings a mount up that its own rollback cannot take down; a holding path that is not a directory withholds it under both mount states.
  • That rendering withholds the subdirectory step and nothing else, and while a holding directory exists the absence of records/ and openbao/ on the mounted store is not reported as remediation; the copy is a single whole-subtree operation that recreates both with their original ownership and modes, and no rendered step creates either by hand ahead of it.
  • While a holding directory exists, no bootroot command and no rendered step creates anything on the mounted store, and bootroot infra up refuses the bring-up under migration incomplete before starting a container. A container restarted by any other route, the boot path included, fails to start rather than writing into the half-populated store, the aside-rename having made the bind source absent. Both manuals say which half is enforced and which is procedural, and record that completing this procedure is what first makes the audit bind's creation-only guard effective on a host that carried openbao/ on the underlying filesystem.
  • The copy is capacity-checked against the mounted filesystem's actual available bytes (f_bavail × f_frsize from a statvfs of audit_store_dir), never against audit_store_reserve_bytes, and against the holding directory's allocated size plus a named margin constant. Where it does not fit, no copy command is rendered and the run reports migration incomplete naming the available figure, the source figure and the margin, with the replacement and pruning routes and the rollback.
  • The source-size walk is the allocated-size subtree walk the provisioning work already ships, called against the holding directory rather than reimplemented — symlink_metadata, allocated blocks rather than apparent length, each (st_dev, st_ino) counted once, no descent across a device boundary — computing no headroom, raising no alarm and touching no health response. A second traversal computing the same figure fails this criterion.
  • A mount point at or under the holding directory, and a symlink, device node, FIFO or socket in it, are each reported under migration incomplete naming the path with no copy rendered, both the mountinfo read and the entry-type walk being re-run on every pass rather than cached. The symlink refusal holds wherever the link points.
  • Mount detection is by mount identity read from /proc/self/mountinfo, not by comparing st_dev, with the mount-point field unescaped (\040, \011, \012, \134) and compared by path component, so a same-device mount --bind is caught and a sibling like <store>.pre-mount-other is not read as being underneath.
  • A rendered type guard closes the window between bootroot's refusal and the operator's copy: find <tree> -xdev -mindepth 1 ! -type d ! -type f -exec false {} +, over the source tree before the copy and over both trees as the first verification pass, a non-zero exit stopping the sequence before the closing rename.
  • The set-identity guarantee is stated as conditional, in the work and in both manuals: -xdev excludes a different-device mount from the measurement, the copy and the comparisons alike, while a same-device bind mount is caught only by the mountinfo read — so they cover the same entries provided no privileged mount change is made between a rendering pass and the operator running what it rendered, and provided nothing under the source tree changes between a check and the command it authorises.
  • No rendered or documented command contains a pipe, and a failed walk is a failed step. Each find, sort, xargs and cmp is its own command over its own file under a mktemp -d scratch directory outside both trees, and a find that cannot traverse or inspect part of a tree exits non-zero and stops the sequence.
  • The copy is cp -a --one-file-system <source>/. <destination>/, nothing dereferences a path, and hard links stay shared. The verification is the type guard over both trees then three comparisons, all of which must exit zero, driven from the same find . -xdev -mindepth 1 enumeration on both sides: metadata (%y %m %U %G %n %P), regular-file sizes (-type f, %s %P) and content (-type f -print0 fed to xargs -0 -r sha256sum --binary --zero from the sorted file), every stage its own command staging to its own file, each ordered by a separate sort -z and each pair compared with cmp. Directory sizes and modification times are excluded, a mismatch is decided by exit status, and the rendered output names its prerequisites: diffutils and coreutils 8.25 or newer.
  • Every manifest is NUL-delimited and compared byte-for-byte, no step emits or parses a line, the path is the last field of every record with no second variable-length field after it, and the content pass is restricted to the same device by find -xdev rather than driven from diff -r. A content pass missing --zero fails this criterion however its enumeration was delimited.
  • Hard links are preserved and the verification proves it: the metadata manifest carries %n, so a copy expanding one source inode into two destination files fails even though every path matches on type, mode, ownership, size and content.
  • No recursive delete is rendered or documented anywhere. The migration closes with mv <audit_store_dir>.pre-mount <audit_store_dir>.migrated as a separate step run only after the guard and all three comparisons exit zero, never chained onto the copy or the verification.
  • <audit_store_dir>.migrated clears migration incomplete and is thereafter reported under the normal outcome as reclaimable space, naming its path and size.
  • The rendered sequence stops both writers as its first step and starts neither again until the closing rename has run, and says plainly that restarting OpenBao costs a manual shamir unseal.
  • Resume and rollback are both rendered and both idempotent — resume re-runs the copy and the verification over a partial copy; rollback unmounts, removes the empty mount point with a non-recursive rmdir and renames the holding directory back, offered only while the holding directory still carries its .pre-mount name — and neither modifies the holding directory's contents.
  • bootroot performs no step of either procedure that changes the host — no rename, no copy, no verification, no unmount — whatever uid the run has; it renders or documents them, and reads metadata and /proc/self/mountinfo to decide what to render.
  • After the documented activation sequence completes on a store seeded with records, every record is readable on the mounted store, .pre-mount is absent, .migrated exists and is reported as reclaimable, and the run reports the reserve as enforced — reached without a recursive deletion.
  • Every byte figure the capacity verdict rests on is checked arithmetic and fails closed: f_bavail × f_frsize, st_blocks times the 512-byte named constant, the sum over unique (st_dev, st_ino) entries and the source-plus-margin total are checked_mul/checked_add on u64, and a None at any of them reports migration incomplete naming the figure and renders no copy. No as, no unwrap.
  • The replacement procedure for a changed reserve appears in both docs/en/ and docs/ko/, inside the existing audit-store passage carrying the replace-and-migrate rule and the size-mismatch refusal, carrying every element Documentation lists: all six steps with their privilege split, the migration-set refusals and the type guard ahead of the capacity gate, the pinned du -s -x --block-size=1 and df --block-size=1 --output=avail forms with the margin as a concrete figure, the image rename gated on the guard and all three comparisons, rollback at both positions, and .img.old retained until step 6 reports enforced.
  • No second copy or verification contract exists. The replacement procedure reuses the activation sequence's pinned guard, copy and comparisons rather than re-spelling them, the two passages agree on every step, and neither page contains a resize2fs, a truncate, a du without -x, an --apparent-size du, a diff -r, or any other in-place resize of the image.
  • Everything under Documentation is documented in both docs/en/ and docs/ko/, on the existing audit-store pages with no mkdocs.yml nav change and no second audit-store section, and ./scripts/check-docs.sh passes.
  • cargo clippy --all-targets is warning-free under -D warnings and cargo fmt -- --config group_imports=StdExternalCrate --check passes.

Constraints

Every rule here has its reason in Scope; this is the boundary list, not a second statement of the design.

  • Do not report the reserve as enforced while a holding directory exists, and do not let any other verdict be reached before that check runs.
  • Do not have bootroot delete, move or modify a holding directory, and do not let any rename — the aside-rename and the closing rename included — target a path that already exists.
  • Do not create anything on the mounted store while a holding directory exists, do not render a command that would, and do not report the absence of records/ or openbao/ as remediation for that window.
  • Do not define a second provisioning renderer, a second image-state rule or a second outcome vocabulary. The activation commands come from the existing phase-1 renderer, with one step withheld and nothing added.
  • Do not capacity-check against audit_store_reserve_bytes; the comparison is the destination filesystem's available bytes against the source's allocated size plus a named margin. Do not render or document a du without -x, or an --apparent-size one, for a figure that gates a --one-file-system copy.
  • Do not render a copy when the check says it will not fit, when a mount point exists at or under the tree, or when the tree holds a symlink or a special file. Do not detect a mount by comparing st_dev, and do not carry a symlink through either procedure.
  • Do not claim, in a message or either manual, that the measurement, the copy and the verification cover the same entries unconditionally. State the maintenance-window precondition, and re-read /proc/self/mountinfo on every pass rather than carrying an earlier verdict forward.
  • Do not rest the symlink guarantee on bootroot's render-time refusal alone: the rendered type guard runs before the copy and again over both trees ahead of the comparisons.
  • Do not render or document a pipeline, and do not place the scratch directory inside either tree.
  • Do not render the copy and the closing rename as one step, do not treat a zero exit from the copy as verification, do not verify by size and count alone, and do not compare directory sizes or modification times.
  • Do not leave a verification as a described contract: the tool and its flags are pinned, and a mismatch is decided by exit status rather than by parsing output. Do not use a line-delimited manifest anywhere, do not treat the delimiter of an enumeration as the delimiter of what consumes it, do not use diff -r, do not add a field after %P, and do not omit the hard-link count.
  • Do not render or document a recursive delete anywhere, do not drive a delete from a captured manifest, and do not document a delete before a verified copy exists.
  • Do not have bootroot perform any step that changes the host: the renames, the copy, the guard, the verification and any unmount are the operator's.
  • Do not widen the size-mismatch outcome into a renderer, do not render any command of the replacement procedure, and do not document any in-place resize, truncation or reformat of the image, in either direction. Do not document relocating a store because audit_store_dir changed; that is different work.
  • Do not add a configuration key for the holding path or for the replacement's temporary paths; they are derived.
  • Do not change the store's directory layout contract, its ownership rules, the derivation of audit_record_dir, the audit_store_* keys, or what the rendered Compose override mounts — the audit bind's creation-only guard included, whose residual this issue retires rather than re-specifies.
  • Do not compute headroom, raise a capacity alarm or touch the endpoint's health response.
  • Do not change behaviour for a deployment that has not enabled the registrar endpoint, and do not change behaviour on a store that has never been written to.
  • No unwrap() in production code; no [] indexing.

Out of scope

  • Defining the provisioning: the loopback image and its state table, the mkfs invocation, the mount unit, the ordering drop-ins, the escaping rules, the free-space preflight, the minimum-reserve check, the mount-identity proof and the enforcement outcomes. This issue writes no rule and no renderer for any of them; it adds one outcome ahead of them and calls the existing phase-1 renderer, withholding exactly one step of its output.
  • The size-mismatch refusal itself and the never-resize-in-place rule it enforces, and the bootroot infra up surface — its --agent-config, its phases, its bring-up refusal and the audit bind's create_host_path: false declaration. All of that already exists and is used here, not rebuilt or re-judged; this issue writes the procedure the refusal names.
  • The daemon's own refusal of the registrar verbs against an unmounted store. This procedure does not rely on it: step 1 stops bootroot-registrar.service for the whole window, which is the only mechanism that holds once the mount is up.
  • Relocating a store because audit_store_dir changed. That moves the mount point rather than the image, and is a different procedure.
  • Bounding the OpenBao audit device's own growth. Pruning it is offered as an operator route when the records do not fit; rotating or capping it is separate work.
  • The capacity probe that reports on the store's health — usage, headroom, the low-water alarm and any health member. The walk called here produces a one-shot migration figure and feeds nothing.
  • Automating either procedure. Every step that touches the store's contents stays the operator's.

Test plan

Each item names its fixtures; the rule being asserted is the criterion of the same name, and the regression each catches is the implementation Scope forbids.

  • Holding-directory precedence: with .pre-mount present and the image, both drop-ins and the mount all valid, the run reports migration incomplete and fails; the same inputs with .pre-mount absent report the reserve as enforced; a directory run names it. bootroot neither deletes nor moves it on any path.
  • Pre-existing holding path: bootroot fails naming the path and renders no rename, so no run can nest one store inside another. The same holds for a .migrated path that already exists.
  • Activation rendering, over a filesystem-mode host with a holding directory, the mount absent and no migration-set refusal standing: the run reports migration incomplete and its rendered list carries the image command for the image's state, the three artifact installs, the daemon-reload and the systemctl enable --now, and no mkdir, chmod or chown of records/ or openbao/. With the mount up and the subdirectories still absent it stays migration incomplete, renders the copy, and still renders no subdirectory remediation. Assert the whole list, and that the same host without a holding directory renders the subdirectory step as usual.
  • Withheld activation, over the same host with a migration-set refusal standing: with the mount absent the rendered list carries the refusal, the re-run step and the rollback, and no mkfs, artifact install or systemctl enable --now; with the mount up the same refusal keeps the activation. Assert both lists whole, and the invariant that no rendered list brings a mount up unless its own rollback can take it down.
  • Rendered sequence: the nine steps appear in order, with the aside-rename before the activation rendering, the activation before the capacity verdict, and the verification between the copy and the closing rename; each bootroot infra up pass is reachable from the state the previous step leaves; and a store never written to gets a sequence with no migration steps at all.
  • Capacity, over (source below available / above available / under audit_store_reserve_bytes but over the mounted filesystem's available bytes / within the margin): the first renders the copy, the rest report migration incomplete with all three figures named and no copy rendered. The third is the case a check against the reserve would wrongly pass.
  • Symlink refusal, over three fixtures. (1) A symlink anywhere under the holding directory — in records/, in openbao/, pointing inside the store and outside it — is reported under migration incomplete naming the path, nothing copied or moved, and re-refused on a later pass rather than answered from a cached verdict. (2) The collision case, a source holding a b linked to c against a destination holding a linked to b c: refused before any comparison runs, and no rendered find -printf format carries a field after %P. (3) A symlink planted after the last bootroot pass and before the copy reaches the destination and passes all three comparisons, and the post-copy type guard is what stops the sequence before the closing rename.
  • Source-size walk: two hard links to one file count once, and allocated blocks rather than apparent length are summed. The shared helper's own never-follow rule is tested where the helper lives.
  • Migration-set refusals, over a same-device mount --bind under the holding directory, a different-device mount, and a FIFO: each yields migration incomplete naming the path with no copy rendered, and a second pass re-reads /proc/self/mountinfo rather than reusing the first verdict. The same-device bind is the case an st_dev predicate passes.
  • mountinfo parsing: mount points whose paths contain a space and a newline are both matched after unescaping, and a sibling sharing a textual prefix with the holding directory is not matched.
  • Rendered commands, pinned exactly: the copy carries -a, --one-file-system and the /. source suffix; every manifest carries -xdev, -mindepth 1 and a NUL terminator, is staged to its own file and ordered by a separate sort -z before cmp compares the two sides; the metadata format is %y %m %U %G %n %P with no %s, no timestamp and no field after %P; the size pass is -type f; the content pass is -print0, then sort -z, then xargs -0 -r sha256sum --binary --zero reading the sorted file, with --zero asserted explicitly; no rendered command contains a |, and none is diff -r.
  • An incomplete walk fails closed, over two fixtures run under a non-root uid — the permission bits do nothing for root, so the same fixtures run as root pass vacuously, which is how this test is most likely to be written wrong. (1) A subdirectory at mode 0111, whose entries cannot be listed. (2) One at mode 0444, whose entries can be listed but not stated. Under both, the type guard and every manifest step exit non-zero and the sequence stops with nothing copied and no rename. Assert the piped forms directly: find … | sort -z exits 0 on both, a pipeline reporting only its last command's status.
  • Newline in a filename: a file so named, staged under openbao/, is compared correctly — a faithful copy verifies clean, and the same tree with that entry missing, renamed or modified fails. Run over all three manifests, asserting every content-pass record is \0-terminated and the filename unescaped. A line-delimited implementation passes only the faithful-copy case.
  • Hard-link preservation: a source with two paths to one inode verifies clean after cp -a, and a destination in which those paths are two independent files fails on the link-count field even though type, mode, ownership, size and content all match.
  • No recursive delete: no rendered command anywhere is a recursive delete, and the migration closes with the rename. A holding directory that acquires a mount after the copy has verified still closes safely.
  • Migration guard: while a holding directory exists, no bootroot path creates records/ or openbao/ on the mounted store, and bootroot infra up reports migration incomplete and starts no container. In the E2E suite a docker compose up attempted mid-window with openbao/ absent leaves the container failed to start and creates nothing under the mounted store — the first point at which the audit bind's guard bites on a host that carried openbao/ before. The daemon half is asserted procedurally through step 1, with a comment recording that nothing refuses its records once the mount is up.
  • Verification gate: a destination missing an entry, and one whose entry has the right size but different content, each make the verification exit non-zero, and the closing rename is not reached.
  • Resume and rollback: resume over a partial copy re-renders the copy and the verification and leaves the holding directory intact; rollback renders the unmount, a non-recursive rmdir of the empty mount point and the reverse rename, and is offered only while the holding directory still carries its .pre-mount name. bootroot performs neither.
  • Host-surgery containment: bootroot issues no rename, copy, verification, rmdir or unmount on any path, whatever uid the test runs under.
  • E2E migration on a store seeded with both record and OpenBao audit content: after the documented sequence the content is readable on the mounted store with its original ownership, .pre-mount is absent, .migrated is present and reported as reclaimable, the reserve accounts for the copied content, no recursive deletion was issued, and assert-openbao-audit-log still passes.
  • Checked arithmetic over the capacity verdict, on injected statvfs values and per-entry metadata in a debug build so an unchecked operation panics rather than passing quietly: an f_bavail/f_frsize product past u64::MAX, an st_blocks×512 product past it, an entry sum overflowing on its last entry, and a source figure one below u64::MAX with a non-zero margin each yield migration incomplete naming the figure with no copy rendered.
  • Replacement-procedure checklist, en and ko: each page carries all six steps with their privilege split, the mountinfo and type-guard refusals ahead of the capacity gate, the du -s -x --block-size=1 and df --block-size=1 --output=avail forms with the margin as a concrete figure, the guard and three comparisons ahead of the image rename, both rollback positions, .img.old retained until the closing verification, and the guard, copy and comparison forms reused from the activation passage rather than re-spelled — with the two passages contradicting each other on no step.
  • Forbidden-form scan over both manual pages, asserted rather than left to review: no resize2fs, no truncate, no du without -x, no --apparent-size or unit-less du, no diff -r, and no | in any command block belonging to either procedure.
  • Documentation: ./scripts/check-docs.sh passes, and a checklist confirms the en/ko pair covers the refusal, the holding directory, the two rendering passes before the copy, the capacity figures, the verification gate, the closing rename, the deferred and caveated reclamation of .migrated, resume and rollback, the maintenance-window preconditions, and the creation-only boundary of the audit bind's guard with this procedure named as what retires it.
  • Tests use tempfile::tempdir() and never a fixed path.

Dependencies

Depends on the two issues that build the ceiling, #923 and #924, which land first.

The first, #923, establishes, on the fresh-host provisioning path, the fully allocated loopback image at <audit_store_dir>.img, the generated systemd mount unit and its two ordering drop-ins, the three phases and the four reported outcomes. It is also what refuses a store that already holds records, leaving every byte in place, and it is where two things this issue calls rather than rebuilds already live: the phase-1 renderer that produces the outstanding activation commands as separately addressable steps, and the allocated-size subtree walk the capacity check measures with.

The second, #924, puts that whole bootroot-side pass behind bootroot infra up, which gains the --agent-config carrying the mode and the reserve and refuses the bring-up before any container starts; it declares the audit bind create_host_path: false, whose creation-only guard this procedure is what first makes effective on a host carrying openbao/ on the underlying filesystem; and it raises the size-mismatch refusal whose procedure this issue writes. It likewise renders no part of a relocation.

So the empty-underlying-directory invariant this procedure exists to satisfy, and the refusal that makes it necessary, both arrive before this issue starts. The daemon's own refusal of the registrar verbs against an unmounted store, #925, is neither a predecessor nor a successor: this procedure holds the daemon out by stopping its service in step 1, which is the only mechanism that survives the mount coming up. Part of #773, which is part of #775.

Pointers

  • src/commands/audit_store.rs — the install-side store module: plan_audit_store, apply_audit_store, preflight_audit_store, audit_override_path
  • src/commands/init/steps/orchestrator.rs:338 — where apply_audit_store is called, before any Docker call; diagnose_partial_init at :455, which is why a live host cannot re-run bootroot init
  • src/commands/infra.rs:176 (resolve_audit_override, inside run_infra_up) — where the holding-directory verdict must be raised before a container starts
  • src/commands/reinit.rs:145 — the pre-wipe preflight, which must raise the holding-directory refusal too
  • src/registrar/audit_store.rs — layout_of, create_layout, check_store_directory and the RECORDS_SUBDIR / OPENBAO_SUBDIR / STORE_DIR_MODE / PRODUCTION_UID constants; src/config.rs:111–118 — the four audit_store_* fields, read here and not changed
  • src/i18n/audit_store.rs, src/i18n/en.rs, src/i18n/ko.rs — the message catalog every new refusal costs an entry in, in both languages
  • scripts/impl/run-local-lifecycle.sh:1422 and scripts/impl/run-remote-lifecycle.sh:1125 (assert-openbao-audit-log) — the E2E assertions that must still pass
  • docs/en/operations.md:252 and docs/ko/operations.md:247 — "The shared audit store" sections, which carry both the activation sequence and the replacement procedure; docs/en/configuration.md:479 / docs/ko/configuration.md:455 — the audit_store_reserve_bytes entry that changing starts the replacement
  • ./scripts/check-docs.sh — the documentation build CI runs; mkdocs.yml inherits docs/theme/mkdocs-base.yml and needs no nav change here
  • docs/rfcs/0001-registrar-role-and-non-self-propagation.md §5.6 and §6

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