All lessons › Function B — Securing AI Architectures › Securing the Architecture — Identity and Ingress
B2.1

Agent identity — user, workload, agent

B2

This starts chapter B2. Its course guide is B2 in the Function B course guide →

▶
Lightboard recording

Not recorded yet. The lesson below is complete and runnable today — the video is added later without touching this page.

Video placeholderpublishes automatically from recordings/B2.1.mp4
Risk

A shared service account answers 'what ran' and destroys 'for whom' — so no later control can be conditioned on the caller.

Control

A distinct identity per workload, carrying the human principal alongside it, asserted on every call.

The lab

Separate the three identities and show a downstream service authorising on the agent while attributing to the human.

◉Use case relevance

Three identities are present every time an agent acts: the person who asked, the workload that runs, and the agent instance doing the work. Collapse any two of them and you lose the ability to answer the only question that matters after an incident.

At CyberTravels. A1.3 put four SPIFFE names in a set in cybertravels/config.py. You replace it with cybertravels/registry.py: identities as records with an approver, a registration time and a state. identity.py stops asking "is this one of ours" and starts asking "is this still active".

◴What this lesson is — Day 0, Day 1, Day 2

Replacing a hand-edited set of agent names with a registry: identities as records that carry an approver, a registration time and a state, so the system can answer when an identity started existing and whether it still should.

Day 0 — why

A shared service account answers "what ran" and destroys "for whom", so no later control can be conditioned on the caller.

Day 1 — how

One identity per workload, carrying the human principal alongside it, asserted on every call.

Day 2 — measure

Workloads holding a distinct identity, as a share of the fleet.

Measure this

what to measuredenominator or target
workloads holding a distinct identityshare of the fleet

▦The framework, and how it works

   who asked        what runs         what acted
   +----------+     +-----------+     +--------------+
   |  human   | --> | workload  | --> | agent        |
   | dana@..  |     | pod/task  |     | instance #7  |
   +----------+     +-----------+     +--------------+
        |                |                   |
      consent         attestation        the actor in the log

   collapse any two and the post-incident question loses its answer

Mitigates: T9 Identity Spoofing · T3 Privilege Compromise · T1 Memory Poisoning.

Three identities are in play whenever an agent acts, and B1.6 and B1.7 were both caused by collapsing them into one.

The user. The human who asked. Carries the business authority and is the answer to "on whose behalf".

The workload. The process that runs — a container, a function, a pod. It has its own identity, derived from the platform, not from a secret someone pasted.

The agent instance. This particular run, of this particular agent, for this particular task. It is what you revoke when one agent misbehaves.

The control is to keep all three, and to use them for different things:

  • Authorize on the workload. What may this agent ever do? That is its ceiling, and it does not change per request.
  • Attribute to the user. Who caused this? That is what the audit trail needs and what B1.14 could not answer.
  • Scope memory and state to the instance or the user, never to the workload alone — which is the write that made B1.4 spread across sessions.

The failure mode to watch for is a system that authenticates the agent and then forgets the human, because it produces logs that are complete and useless.

What this control closes. Answers who is calling, so every later control has a subject. Without it, default-deny has nothing to deny and the audit trail has nobody to name.

2 · The control

3 · Separating the three, as a skill

Keeping the three identities apart is a review you will run against every agent CyberTravels ships, not a decision you make once. So it is written down as a procedure: which principal authorises, which one is attributed, which one scopes memory — and the two narrowing rules a delegated token has to satisfy before any of it means anything. This is the file in this repository, embedded verbatim:

▶Real time execution as skill

The skill — skills/identity/agent-identity-review/SKILL.md

name: agent-identity-review
description: >-
  Review how an agent authenticates and how a user's authority is delegated to
  it, including token exchange, on-behalf-of chains, service accounts and
  non-human identity. Use when asked who an agent is calling as, whether a
  delegation is auditable, why a downstream system sees the wrong principal, or
  how to scope an agent's credentials.
allowed-tools: Read, Grep, Glob

Agent identity and delegation review

Three identities are in play whenever an agent acts, and most incidents come from collapsing them:

  • the user whose request started the work
  • the agent doing the work — a workload identity, not a person
  • the actor chain connecting them, which is what an auditor needs

A service account shared by every agent answers "what ran" and destroys "for whom". That is the gap non-human identity exists to close.

When to use this

Reviewing an agent's auth design, an OBO/token-exchange implementation, a gateway that fronts an agent, or any log where you cannot tell which user caused an action.

Procedure

1 — Name the three identities. For the flow under review, write down the user principal, the workload identity, and where each is asserted. If the workload identity is a long-lived shared secret, that is finding one.

2 — Check the delegation narrows. In RFC 8693 token exchange the issued token must satisfy both rules:

  • subset of presented — never more scope than the incoming token carried
  • within the actor's ceiling — never more than the agent is itself allowed

Either rule alone is insufficient. Subset-only lets a highly privileged user hand an agent authority the agent should never hold; ceiling-only lets an agent exceed the user who asked. Test both directions explicitly.

3 — Check the chain is preserved and not duplicated. The act chain should read user → agent, once. A chain that repeats the principal (dana → dana → agent) usually means the head was appended twice, and it breaks any audit query that counts hops.

4 — Find where OBO stops. Some downstream systems cannot consume a delegated token — legacy databases, vendor APIs, anything with a static credential. Identify each, and require a choke point: a gateway that holds the credential, enforces per-user authorisation before the call, and logs the original principal. The credential must not be reachable by the agent directly.

5 — Check expiry and revocation. How long is the delegated token valid, and what stops it being replayed after the user's session ends? Just-in-time authority that outlives the task is standing authority with extra steps.

6 — Check the log answers the audit question. Pick a real question — "which user caused this row to be deleted?" — and try to answer it from the logs alone. If you cannot, the delegation is not auditable regardless of how it is built.

Example

Input — the fixture committed at the top of scripts/agent_identity_review.py. Edit it and re-run: the buckets, counts and verdicts below are derived from it, not hard-coded.

Output — the opening lines of a real run:

request                     authorized?  attributed to
dana@corp -> reports:read   True         dana@corp via reports-agent
dana@corp -> db:admin       False        dana@corp via reports-agent

memory keys - the same workspace, two users:
   dana  -> acme:dana@corp
   priya -> acme:priya@corp
   shared? False

The run continues past this. test_skills.py executes the script on every build, with no model configured — so what CI proves is that it runs and refuses legibly, not that it prints these lines. The output above is a recorded run against a real model, and nothing re-diffs it: a model's answer is not reproducible, and this repository does not claim otherwise anywhere it can be checked.

Output contract

{
  "identities": {"user": "str", "workload": "str", "assertion": "str"},
  "delegation": {"mechanism": "obo|impersonation|shared_service_account|none",
                 "subset_of_presented": true, "within_actor_ceiling": true,
                 "chain": ["principal", "..."], "chain_wellformed": true,
                 "ttl_seconds": 0, "revocable": true},
  "chokepoints": [{"downstream": "str", "reason": "str",
                   "enforced_at": "gateway|service|none",
                   "credential_reachable_by_agent": false}],
  "audit": {"question": "str", "answerable_from_logs": true},
  "findings": [{"issue": "str", "severity": "critical|high|medium|low", "fix": "str"}]
}

Failure modes

  • Checking only one narrowing rule. Both, every time.
  • Accepting a shared service account because it is "internal". Internal is a network property; it says nothing about attribution.
  • Treating the gateway as optional where the downstream cannot do OBO. It is the only place authorisation can happen.
  • Measuring delegation by whether the call succeeded. A call that succeeds with too much scope is the failure being looked for.

Three ways to do this lesson. Pick one. They run the same procedure against the same code — what changes is who drives it, and how much you see.

1In your AI agent — you are taught

The agent reads the lesson, explains the idea, fetches the code, runs the check and answers your questions as you go. This is the one to take if you are here to learn.

Install it, once. From the folder you cloned. --all does every agent it finds on your machine:

python3 scripts/install_skills.py --all --lessons B2.1

Then restart your agent and open it in that folder. On Windows, if python3 says Python was not found, use python instead.

Then pick b2-1-agent-identity:

agentinstall just for this agentstart it
Claude Codepython3 scripts/install_skills.py --tool claude --lessons B2.1type /b2-1-agent-identity
GitHub Copilot (VS Code)python3 scripts/install_skills.py --tool copilot --lessons B2.1type /b2-1-agent-identity in Chat
Cursorpython3 scripts/install_skills.py --tool cursor --lessons B2.1type / in Agent chat and search for it
Codexpython3 scripts/install_skills.py --tool agents --lessons B2.1type $b2-1-agent-identity, or run /skills

2As one Python command — you just run it

No agent, no conversation. The same lesson executes and prints the same readback. Take this one if you have no assistant, if you are reviewing rather than learning, or if you want the result without the teaching around it.

python3 scripts/lesson.py B2.1

3Read the code, and the difference — nothing runs

Neither of the above, and no model needed. The first command writes CyberTravels exactly as it stood at the end of this lesson; the second prints the diff — the lines this lesson added or changed, and nothing from any later one.

python3 scripts/checkpoint.py --at B2.1 --out work/cybertravels
python3 scripts/checkpoint.py --at B2.1 --diff

On Windows, type python wherever these say python3 — and use \ instead of / in the paths. Everything else is identical. A0.0 step 2 explains why.

Ways 1 and 2 need a model; way 3 does not. A signed-in Claude Code CLI needs no API key, and any OpenAI-compatible endpoint works too. With neither, the audit exits 2 and says so rather than inventing an answer. A0.0 sets this up.

Ways 1 and 2 both end with the same readback: What I did, What changed, The number and Read this next. A step that was skipped or refused is reported as exactly that, never as a pass.

If you did the last lesson’s challenge, expect this to stop. Most challenges ask you to edit the code, and ways 1 and 2 will not overwrite your edits: you get stopped, not a failure, with two options — --out for a fresh folder that keeps your work, or --force to discard it and start this lesson clean.

✓What you just proved

The skill loads and reports its own shape: a routing description an agent reads to decide whether this review applies, the tools it is allowed to use, and a procedure long enough to separate user, workload and agent instance and to check both narrowing rules — scope is a subset of what was presented, and within the actor's own ceiling.

✎Your turn

For one agent, write down its three identities. If the workload and the user are the same value, you have inherited credentials; if the instance does not exist, you cannot revoke one run.

Tools used

SPIFFE/SPIREKeycloak

Skill source: skills/identity/agent-identity-review

← PreviousB1.19 · The control index — every control CyberTravels needs, not only the new ones