All work

Security & Response

Agent Identity

An agent's name used to be whatever it typed. Now it's the sandbox the dispatcher started it in, and a session that tries to borrow its manager's name is refused and reported.

Designed, built, and operatePythonsystemd scopesClaude Code agentsjournaldWazuhOpenBao (next)
  • A session that files work under another agent's name is refused before anything is saved. Tested live as the manager, the CEO, a peer and another manager.
  • No agent session can answer a question as me, so an agent can't approve its own request.
  • Every refused attempt is logged and forwarded to the security pipeline on a separate machine.

The problem

The org chart only means something if an agent can't lie about where it sits on it. The dispatcher decides whether to run a task by who filed it: me, or a manager above the assignee. Until this fix, who filed it was a word the agent typed. An agent that wrote --by ceo was, as far as the board knew, the CEO.

That's a hole whether or not anyone has used it. Task text becomes a prompt that runs on its own, so a confused or manipulated agent that could claim its manager's name could hand any agent in the company unattended work. The fix had to come from somewhere the agent can't type into.

Where the name comes from

Every session the dispatcher starts runs in its own systemd scope, and the dispatcher names that scope after the role and the task. An engineer working task 529 runs inside a scope called agent-software-engineer-t529. The kernel records which scope every process belongs to, and the agent never picks its own.

When a session runs a board command, the board asks the kernel which scope the caller is in, takes the role out of the scope's name, and compares it with the name on the command. They match, or the command is refused before anything is written. Manager rounds run in round scopes named after the manager and get the same check. My own shell, the scheduled jobs and the dispatcher run outside any agent scope. A scope that can't be read is treated as an agent with no role, so it fails closed.

How the board checks who an agent isThe dispatcher names each session's scope. When the agent claims a name, the board compares it with the role in the scope. A match runs; a mismatch is refused, saves nothing, and is logged off the machine.Dispatcher names the scopeagent-software-engineer-t529Agent claims a namefleet task … --by ceoBoard asks the kernelscope role: software-engineerSame name?claimed vs. scope roleyesRun itacting as itselfnoRefusednothing saved · deny-identity loggedSystem journal → security agentforwarded to a separate monitoring server
Steps in the agent identity check
1The dispatcher starts the session in a scope it names after the role
2The agent runs a board command and claims a name, for example the CEO
3The board asks the kernel which scope the caller is in and reads the role from it
4If the claimed name matches the scope's role, the command runs
5If not, it is refused, nothing is saved, and the attempt is logged and forwarded off the machine
The check, end to end. The only input that decides the outcome is the scope name, and the dispatcher set it before the agent said a word.

Five attempts, five refusals

These ran against the production board, on a task that was already closed, from sessions started the same way the dispatcher starts them.

{"event":"deny-identity","by":"head-of-engineering","caller":"agent-software-engineer-t529-….scope","cmd":"note","task":529}
{"event":"deny-identity","by":"ceo","caller":"agent-software-engineer-t529-….scope","cmd":"note","task":529}
{"event":"deny-identity","by":"tools-engineer","caller":"agent-software-engineer-t529-….scope","cmd":"note","task":529}
{"event":"deny-identity","by":"cio","caller":"round-cmo-….scope","cmd":"note","task":529}
{"event":"deny-owner-provenance","caller":"agent-qa-engineer-t529-….scope","cmd":"answer","task":495}
The five log lines those attempts produced, trimmed to the fields that matter. No command text and no task content goes into the log: ids, roles and the event.

Impersonation attempts on the live board

Each line is a session trying to act as someone it isn't, and what came back.

  1. Engineer as its managerrefused: --by head-of-engineering is refused from an agent session running as software-engineer. Nothing was saved.
  2. Engineer as the CEOrefused: --by ceo is refused from an agent session running as software-engineer. Nothing was saved.
  3. Engineer as a peerrefused: --by tools-engineer is refused from an agent session running as software-engineer. Nothing was saved.
  4. CMO round as the CIOrefused: --by cio is refused from a round session running as cmo. Nothing was saved.
  5. QA as the ownerrefused: --by owner is refused from a dispatched session. Only the owner's hub click or the owner's own shell can answer as the owner.
  6. Engineer as itselfallowed

Nothing was saved from any of the five. Each one left a line in the gate log and the system journal, which the security agent forwards off the machine.

Approvals that can't be forged

Pretending to be me is the attack I worry about most, because my answer is what releases anything a customer will read or that costs money. A dispatched session can't answer as me at all. Only my own click on the hub or my own shell can.

When I do approve something, the approval is bound to the question it answers and works once. A stale page, a double click or a replayed answer doesn't carry over to a different request.

What it doesn't stop yet

Every agent runs as root on the same machine. So this check stops the common case, a confused or prompt-injected agent typing the wrong name, and it records the attempt. A session that is deliberately attacking the machine it runs on has ways around it that never touch the board command at all, and a security committee went through them one by one.

Saying that out loud is the point of this page. A control that's described as stronger than it is gets trusted in places it shouldn't be.

Next: identity the kernel enforces, then keys

I asked for public and private keys first, and the committee's answer was blunt: keys don't help while every agent runs as root, because root can read any key on the box. So the plan, decided and nothing in it built yet, runs in order. First the board becomes a service that identifies callers by their Unix user, the dispatcher stops trusting any field it hasn't verified, orders from the hub get signed, and a separate machine watches the board for tampering. Then the agents that read outside text, and the managers, move off root.

Once each role is its own user, keys mean something. Each role gets its own signing key in OpenBao, the secrets vault that already holds the fleet's other keys, and the key never leaves the vault. Every board write is signed by the role's key and checked before anything acts on it, so a forged record fails a signature check instead of a name check.

Engineering decisions

The calls I made, and what each one cost.

Identity comes from where the session runs, not from what it says.

Anything an agent types can be wrong, by mistake or because a log line told it to. The scope is set by the dispatcher before the agent starts, and the agent never chooses it.

Refuse and log. Don't warn.

A warning in a session's output is something the next prompt can talk its way around. A refusal saves nothing, and a copy of the log leaves the machine.

Fail closed on an unreadable scope.

If the board can't tell who's calling, the caller is treated as an agent with no role, and an agent with no role can't act as anyone.

Ship the cheap check now, and design the strong one properly.

The scope check took an evening and closes the common case today. Signed identity changes how every agent runs, so it goes through a committee first, the same way the rest of the security model did.

More systems

I'm looking for Incident Commander, SRE and AIOps roles.

If your team is drowning in toil, alert noise, or incidents that never quite close. That is the work I do.

Get in touch