Skip to content

Store hardening: auth, TLS and snapshot integrity #374

Description

@jhrozek

Redis dials with no auth, no TLS and no keyspace scoping, and sessnap is plain JSON with no MAC.
It should land before any multi-tenant deployment, because it buys the same defence a signed chain
would buy against one specific adversary: someone holding a leaked store credential cannot forge
a MAC. It buys nothing against a compromised pod, and that qualifier is load-bearing.

mecatl, deployment work. Unblocked, independent of everything else in #377.

Hazards

Facts that were expensive to find and that nobody re-derives on the way to implementing. Each is verified against code.

  • The MAC key inherits Spike: can a model-spawned shell reach the key? #375. The MAC is applied where the snapshot bytes are produced and
    consumed, which is the per-turn hot path inside the harness process, and that process is the parent
    of the Bash tool (osfs.go:1167-1187 is a plain exec.CommandContext(ctx, r.shell, "-c", command),
    same uid, no namespace, no path allowlist; the hardened runners differ only in cmd.Env and
    cmd.Dir). The key does not have to be read out of memory to be stolen: a flag lands in
    /proc/<pid>/cmdline, an env var is readable at the parent's /proc/<harness-pid>/environ because
    envscrub strips names from the child's environment only, and a mounted Secret file is a fixed
    path that cat reaches under a read-only-by-verb classification with no path predicate, which an
    isolated child auto-approves. Only /proc/pid/mem needs PTRACE_MODE_ATTACH, and no manifest here
    sets ptrace_scope.
  • workspace is the field that escalates, not the owner label. The persisted string flows
    verbatim into the workspace factory at run entry (service.go:2311) and becomes the
    osfs.NewWorkspace root for Read/Edit/Write/Grep/Glob, the Bash cmd.Dir, and the base the escape
    classifier is constructed relative to (build.go:6784-6811). A stored "workspace":"/" is a
    session rooted at the filesystem root with no escape left to detect. The same string pins the child
    permission resolver (build.go:1757). profile is trusted the same way in rehydrateSession
    (service.go:2403-2423), so flipping it off no-fs escalates a deliberately file-less session onto
    the full FS tool set plus Bash.
  • The event log is a second authority path. approvalreplay.go:33-82 runs at every
    loadAndReopen: it reads the event log, filters EvApproval with AllowAlways, correlates each to
    a ToolCall in the loaded conversation, and re-drives policy.Learn. Both inputs live in the same
    open store. A writer forges a Bash call in the snapshot's messages plus a matching approval event,
    and at the next resume the harness learns a durable permission allow the user never granted. The
    replay code is fail-safe against every accident and defenceless against a writer.
  • A payload MAC does not survive the grpcdriver round trip. The driver server decodes and
    re-encodes (sessnap.Unmarshal on Save at server.go:65, a fresh sessnap.Marshal on Load at
    server.go:93), and sessnap.Of() rebuilds from the live session, so a stored MAC has nowhere to
    ride. Either the MAC is an envelope in the store adapter, in which case that backend has no MAC at
    all, or the driver protocol carries it as an opaque field. One caller cannot reach another's work #368 already carries the adjacent
    sentence about labels being dropped at the process boundary.
  • Auth, TLS and keyspace scoping stop the same adversary at the door and fix the confidentiality
    half a MAC cannot touch. If this issue gets split, those go first.
  • The event log stays uncovered by decision, and one attack survives because of it. A per-record
    MAC would have closed it, and the call is to ship the snapshot MAC first and close the event log
    separately in Event-log integrity: a forged approval record still grants a durable permission #385. What remains open: an existing benign tool call in the conversation plus a forged
    EvApproval{AllowAlways} naming it is enough for approvalreplay to run policy.Learn, because
    only the snapshot half is authenticated. Do not describe this issue as closing the approval-replay
    path.

Acceptance criteria

  • Auth, TLS and keyspace scoping are supported and used by the shipped manifest.
  • A shared store with no auth produces a startup warning.
  • The MAC covers the whole snapshot, not a chosen subset of fields. Anything a resume trusts is
    inside it, workspace, profile and mode included.
  • The MAC is an envelope in the store adapter, around the serialized snapshot. The remote driver
    backend is therefore not covered, and that is a decided exclusion rather than an oversight: the
    driver server decodes and re-encodes, so a payload MAC cannot survive it. The exclusion rests on
    One caller cannot reach another's work #368's deployment requirement that the driver endpoint is never tenant-reachable, which puts it out
    of reach of the same adversary this MAC defends against.
  • A verification failure reads as an integrity error, distinguishable from a transient one.
  • The boundary is stated where an implementer reads it, in these words or equivalent: this MAC
    detects tampering by an adversary who can write the store but cannot execute code in a mecatl pod.
    It provides no integrity against a compromised pod or a prompt-injected model with Bash, because the
    verifying key lives in the agent-loop process and is reachable from a child shell. It does not cover
    the event log or the tool-call audit log. It is not confidentiality either; the snapshot stays
    readable to anyone who can read the store.

Proof

The same properties, adversarially, with an actor and an ordering. A property-level criterion can go green while being false; these are what stop that.

  • Modify a stored row directly, then resume: refused. An unmodified snapshot still loads, so
    "refuse everything" cannot pass.
  • Rewrite workspace to / in the store, then resume: refused, rather than a session whose file
    tools are rooted at the filesystem root.
  • Flip profile off no-fs in the store, then resume: refused, rather than a file-less session
    that comes back holding Bash.
  • Forge a tool call in the snapshot's messages, then resume: refused, so the snapshot half of the
    approval-replay path is closed.
  • The same tampering is detected on both local backends. The remote driver backend is expected to
    fail this and the test says so, rather than the gap being discovered later.

Slices

Decomposition is the implementer's job; these are the natural seams, not a mandate.

  • Auth, TLS and keyspace scoping, with the manifest that uses them.
  • The MAC envelope over the whole snapshot, and where its key comes from.

Reasoning

The design this comes from, both on main:

Every hazard above is stated in full here rather than linked, so this issue stands on its own.

Activity

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

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions