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.
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary generated by LLM agent
Stacked on #75, which is where
docs/ci.mdcurrently 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.mdrather than only inthe 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:
pull request's author approve it, and the session opens them as you — so
requiring one approval means nobody can give it.
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.
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_fileand[core] ssh_key. With theidentities separated, the same settings start enforcing what they appear to:
mainThat 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
[core] ssh_keyis anindependent 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.
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.
to the mounted project. It changes what it can do to your forge, and who
the forge thinks did it.
Why
ci.mdlinks hereThat document tells an operator to protect
mainand calls it thehighest-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.mdcarries both halves — theproblem and the separation that fixes it, since stating the problem alone
leaves a reader with nothing to do — and
ci.mdcarries 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.
1c70227163to799ffa0770