Skip to content

hookprobe: a run can no longer rewrite the files configuration points at, and a secret's value is not printed by accident - #41

Merged
itswl merged 2 commits into
mainfrom
steering-files-and-secret-reads
Nov 3, 2026
Merged

itswl merged 2 commits into
mainfrom
steering-files-and-secret-reads

Conversation

@itswl

@itswl itswl commented Nov 3, 2026 •

Copy link
Copy Markdown
Owner

Two things a read-only investigator could still do, both found by asking a second deployment's audit questions of this code, and both closed here.

1. A run could rewrite the files configuration points at. The work compose kept the planner's and the watcher's MCP config on each node's own volume. The engine rereads that file at every run and starts the stdio servers it names, and the input guard protected only fixed names in the workdir. Measured on the probe image with that setting and a recording stand-in for the model API:

moment MCP config on the volume marker from the planted command
before any run no absent
after a run that wrote it with a shell step yes absent
after an unrelated run with no tool call yes present

The fix has three parts:

  • The input guard protects every file the operator names by path, wherever it lives, on all three runtimes, and a run's before-and-after digest covers them. That means the MCP config, roles, both allowlists, the declared radius, decline patterns, the codex and pi configs, and the appended methodology when its path is configured. A test fails if a new file setting is left unclassified.
  • At startup the posture record names any of the operator's files this node's own user could write or replace (writable_inputs), and the alarm channel says so once. It is reported, not refused, so a node configured from the old docs still starts.
  • The work compose reads both MCP configs from the read-only secrets mounts. The configuration doc no longer offers the volume.

Read back from this branch's image:

setup planted config writable_inputs a write by the service user
MCP config on the volume refused by the input guard; nothing ran /data/mcp.json, paged once refused by the guard
MCP config on a read-only mount not possible empty refused by the kernel

2. A read-only identity still reads secret values.

  • Posture now records reads_secrets beside the verdict and never lets it decide anything. The questions are kubectl auth can-i get|list secrets -A plus wildcard rules, and a second AWS simulation of Secrets Manager values, SSM parameters, KMS decryption and S3 object reads. Their failures go to read_errors, never to the errors that make a verdict unverifiable.
  • The bash guard refuses printing a secret's value under every posture: a Secret's data from kubectl, Secrets Manager values, decrypted SSM parameters, KMS plaintext, and AWS credentials. Whether a secret exists, which keys it holds and when it changed stays open.

This overturns the 2026-09-09 note that left kubectl get secret … -o yaml allowed on purpose. That note is corrected in place, and the output-guard test's premise now says which read is refused and why every other read stays open.

Shown. The console's posture card adds secret-read counts and names any config a run could rewrite, in both languages.

Checked.

  • Gate: STACK GATE GREEN locally, with 940 investigator tests.
  • Four new guard mutations are each caught: configured files unprotected, the writable report silenced, secret prints allowed, and the read questions dropped.
  • The offline engine check on the branch image still completes a scripted turn, refuses a protected write, and keeps prompts verbatim.

Not changed: codex keeps its hooks in a home on the run's volume, which is already its own containment row. Nothing is deployed. The two local MCP configs move to the read-only mounts after this merges, and the stack stays stopped.

CodeQL. The first run raised two high alerts, clear-text logging and clear-text storage of sensitive data, at the gate's output and the audit writer. The flow started at the guard's rule table, whose name held the word code scanning treats as secret data, and carried its fixed descriptions into the refusal text. The second commit renames the rule and question tables. Every field an operator reads keeps its name.

CI read back before merging.

itswl added 2 commits November 3, 2026 08:36
…onfiguration points at, and read secret values

A run could write its own MCP config when a deployment kept it on the run's
volume, and every later run started the command it named: measured on the
probe image with the work compose's setting and a recording stand-in model,
with no tool call for the guards or the audit to see. The input guard now
protects every configured file by its path on all three runtimes, the
before-and-after digest covers them, and the startup posture names any the
service user could rewrite (writable_inputs, paged once). The work compose
reads both MCP configs from the read-only secrets mounts, and the
configuration doc stops offering the volume.

A read-only identity still reads secret values. Posture records reads_secrets
beside the verdict (can-i get|list secrets, and a second simulation of
Secrets Manager, SSM, KMS and S3 reads) without letting it decide anything,
and the bash guard refuses printing a secret's value under every posture.
Whether a secret exists and when it changed stays open. The console's posture
card shows both.

Read back from the branch image: the planted config is refused and nothing
runs after it; with the config on a read-only mount the posture reports
nothing and the kernel refuses the write. Two decision notes, four guard
mutations; the 2026-09-09 note that left Secret reads allowed is corrected in
place.
…de scanning reads as secret data

CodeQL followed `_SECRET_RULES` into the refusal text, which the gate prints
and the audit stores, and raised clear-text logging and storage of sensitive
data on both (alerts 15 and 16). The table holds patterns and fixed
descriptions, never a value: its name was the only "secret" in the flow. The
guard's table is `_REVEAL_RULES` and its resource pattern `_RESOURCE_WORD`
(the bandit annotation the old name needed goes with it), and posture's
questions are `KUBE_READ_QUESTIONS`, `READ_VERBS` and `AWS_READ_ACTIONS`, with
a comment beside each saying why. The record fields an operator reads keep
their names.
@itswl
itswl merged commit be65a10 into main Nov 3, 2026
8 checks passed
@itswl
itswl deleted the steering-files-and-secret-reads branch November 3, 2026 00:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant