Document that the sandbox authenticates as the operator #80

Merged
branderson merged 2 commits from sandbox-identity into main 2026-08-05 22:48:53 -07:00
Owner

Summary generated by LLM agent

Stacked on #75, which is where docs/ci.md currently lives.

Came out of a question about Gitea's branch-protection merge whitelist: is it
safe to enable, given the agent reuses the operator's own account? Enabling it
is safe and it also buys nothing — and the reason generalises well past that
one setting, which is why this goes in docs/security.md rather than only in
the CI runbook.

The point

A session pushes with the operator's forwarded SSH key and a token issued
under their account. So at the forge, a push, a pull request, a comment or a
merge from inside the sandbox is not similar to one the operator made — it
is the same event.

Every forge control that answers "who did this" therefore admits the sandbox
exactly as it admits the operator. Putting yourself on a merge whitelist puts
the session on it.

Three consequences, each of which is easy to configure around without
noticing:

  • Required approvals are unusable, not merely weak. A forge will not let a
    pull request's author approve it, and the session opens them as you — so
    requiring one approval means nobody can give it.
  • The forge's audit log cannot attribute. "You pushed this" and "a session
    pushed this" are one recorded event. Same shape as the exit-time audit's
    time-window caveat, which the section points at rather than restating.
  • It compounds the SSH agent's reach, already documented further down: the
    agent signs for any repo the key can reach, and the identity is what makes
    the result land as yours everywhere it reaches.

The remedy ships with the problem

Configuration, not a missing feature — a dedicated forge account with write
access, named by [forge.gitea] token_file and [core] ssh_key. With the
identities separated, the same settings start enforcing what they appear to:

Setting What it becomes
Merge whitelist: you The session genuinely cannot merge
Push whitelist on main The session pushes feature branches and nothing else
Require approvals ≥ 1 Usable — the session authors, you approve

That last row is what turns "the operator merges, the agent does not" from a
convention into something the forge enforces.

Limits stated rather than left to be found

  • Nothing enforces which credential a push uses. [core] ssh_key is an
    independent path, so with an agent also forwarded a push may still be signed
    by the personal key — making the separation decorative for exactly the
    operation it was meant to constrain. The doc says to make a test push and
    read what the forge attributes it to, rather than assume.
  • It does nothing for CI. A write-capable account still pushes feature
    branches, and pushing a branch is what makes CI execute code. The token a
    forge injects into a job is a third principal, subject to none of those
    settings.
  • It does not reduce what a compromised session can do inside the container or
    to the mounted project. It changes what it can do to your forge, and who
    the forge thinks did it.

Why ci.md links here

That document tells an operator to protect main and calls it the
highest-value item in the file — without saying that on a default dejima setup
branch protection does not distinguish them from the session they are
protecting against. The prerequisite section now says so and links across.

Tests

test/static.bats, clean. Two new: security.md carries both halves — the
problem and the separation that fixes it, since stating the problem alone
leaves a reader with nothing to do — and ci.md carries the link.

The anchor was checked against the heading a renderer would actually derive
from it, rather than eyeballed; a dead cross-document anchor fails silently in
both files. Not worth a permanent test, but worth doing once.

**Summary generated by LLM agent** Stacked on #75, which is where `docs/ci.md` currently lives. Came out of a question about Gitea's branch-protection merge whitelist: is it safe to enable, given the agent reuses the operator's own account? Enabling it is safe and it also buys nothing — and the reason generalises well past that one setting, which is why this goes in `docs/security.md` rather than only in the CI runbook. ## The point A session pushes with the operator's forwarded SSH key and a token issued under their account. So at the forge, a push, a pull request, a comment or a merge from inside the sandbox is not *similar to* one the operator made — it is the same event. Every forge control that answers "who did this" therefore admits the sandbox exactly as it admits the operator. Putting yourself on a merge whitelist puts the session on it. Three consequences, each of which is easy to configure around without noticing: - **Required approvals are unusable, not merely weak.** A forge will not let a pull request's author approve it, and the session opens them as you — so requiring one approval means nobody can give it. - **The forge's audit log cannot attribute.** "You pushed this" and "a session pushed this" are one recorded event. Same shape as the exit-time audit's time-window caveat, which the section points at rather than restating. - **It compounds the SSH agent's reach**, already documented further down: the agent signs for any repo the key can reach, and the identity is what makes the result land as yours everywhere it reaches. ## The remedy ships with the problem Configuration, not a missing feature — a dedicated forge account with write access, named by `[forge.gitea] token_file` and `[core] ssh_key`. With the identities separated, the same settings start enforcing what they appear to: | Setting | What it becomes | | --- | --- | | Merge whitelist: you | The session genuinely cannot merge | | Push whitelist on `main` | The session pushes feature branches and nothing else | | Require approvals ≥ 1 | Usable — the session authors, you approve | That last row is what turns "the operator merges, the agent does not" from a convention into something the forge enforces. ## Limits stated rather than left to be found - **Nothing enforces which credential a push uses.** `[core] ssh_key` is an independent path, so with an agent also forwarded a push may still be signed by the personal key — making the separation decorative for exactly the operation it was meant to constrain. The doc says to make a test push and read what the forge attributes it to, rather than assume. - **It does nothing for CI.** A write-capable account still pushes feature branches, and pushing a branch is what makes CI execute code. The token a forge injects into a job is a third principal, subject to none of those settings. - It does not reduce what a compromised session can do inside the container or to the mounted project. It changes what it can do *to your forge*, and who the forge thinks did it. ## Why `ci.md` links here That document tells an operator to protect `main` and calls it the highest-value item in the file — without saying that on a default dejima setup branch protection does not distinguish them from the session they are protecting against. The prerequisite section now says so and links across. ## Tests `test/static.bats`, clean. Two new: `security.md` carries both halves — the problem *and* the separation that fixes it, since stating the problem alone leaves a reader with nothing to do — and `ci.md` carries the link. The anchor was checked against the heading a renderer would actually derive from it, rather than eyeballed; a dead cross-document anchor fails silently in both files. Not worth a permanent test, but worth doing once.
dejima-agent added 1 commit 2026-08-05 21:56:30 -07:00
docs: the sandbox authenticates as the operator, so identity controls are guards
tests / sandbox (proxy) (pull_request_target) Successful in 2m38s
tests / sandbox (providers) (pull_request_target) Successful in 4m24s
tests / runner-env (sandbox) (pull_request_target) Successful in 1m28s
tests / sandbox (project) (pull_request_target) Successful in 6m4s
tests / sandbox (static) (pull_request_target) Successful in 2m56s
tests / sandbox (config) (pull_request_target) Successful in 6m9s
tests / sandbox (remote) (pull_request_target) Successful in 5m6s
tests / host-docker (pull_request_target) Successful in 5m40s
tests / sandbox (audit) (pull_request_target) Successful in 8m31s
tests / runner-env (host-docker) (pull_request_target) Successful in 1m35s
799ffa0770
A session pushes with the operator's forwarded SSH key and a token issued under
their account, so at the forge a push, a pull request, a comment or a merge
made from inside the sandbox is not similar to one the operator made - it is
the same event. Every forge control that answers "who did this" therefore
admits the sandbox exactly as it admits the operator, and there is no way to
configure around that while the identity is shared.

That matters most where someone would reasonably believe they had fixed
something. Putting yourself on a branch's merge whitelist puts the session on
it too. Required approvals are not merely weak but unusable, since a forge
will not let a pull request's author approve it and the session opens them as
you. And the forge's own audit log cannot attribute - the same shape as the
exit-time audit's time-window caveat, which this section points at rather than
restating.

The remedy is configuration rather than a missing feature, and it ships
alongside the problem so a reader is not left with nothing to do: a dedicated
forge account with write access, named by [forge.gitea] token_file and
[core] ssh_key, at which point the merge whitelist, the push whitelist and an
approval requirement all start enforcing what they appear to.

Two limits are stated rather than left to be discovered. Nothing enforces
which credential a push actually uses - [core] ssh_key is an independent path,
so a forwarded agent may still sign, making the separation decorative for the
one operation it was meant to constrain, which is worth a test push rather
than an assumption. And a separate identity does nothing for CI: a
write-capable account still pushes feature branches, which is what makes CI
execute code, and the token a forge injects into a job is a third principal
subject to none of those settings.

docs/ci.md's branch-protection prerequisite links here, because that section
told an operator to protect main without saying that on a default dejima setup
branch protection does not distinguish them from the session they are
protecting against.

static.bats pins both halves in security.md - the problem and the separation
that fixes it - and the link from ci.md. The anchor was checked against the
heading a renderer would derive, since a dead cross-document anchor is silent
in both files.
dejima-agent force-pushed sandbox-identity from 1c70227163 to 799ffa0770 2026-08-05 21:56:30 -07:00 Compare
branderson added 1 commit 2026-08-05 22:03:47 -07:00
Merge branch 'main' into sandbox-identity
tests / runner-env (sandbox) (pull_request_target) Successful in 1m30s
tests / sandbox (proxy) (pull_request_target) Successful in 2m16s
tests / sandbox (static) (pull_request_target) Successful in 3m6s
tests / sandbox (providers) (pull_request_target) Successful in 4m18s
tests / sandbox (project) (pull_request_target) Successful in 5m52s
tests / host-docker (pull_request_target) Successful in 6m13s
tests / sandbox (remote) (pull_request_target) Successful in 5m9s
tests / sandbox (config) (pull_request_target) Successful in 6m1s
tests / runner-env (host-docker) (pull_request_target) Successful in 1m26s
tests / sandbox (audit) (pull_request_target) Successful in 8m52s
14bb297dbc
branderson merged commit 5dc158f3a1 into main 2026-08-05 22:48:53 -07:00
Sign in to join this conversation.