Not recorded yet. The lesson below is complete and runnable today — the video is added later without touching this page.
recordings/B1.7.mp4Attribution fails before the incident starts: you cannot say which agent acted, so you cannot revoke one without breaking all of them.
Per-workload identity with attestation (B2.1, B2.2) and a lifecycle that can revoke one (B2.5).
Have two agents share a credential, then try to work out which one made the call.
Four agents share one service account. The audit log answers "what happened" perfectly and cannot answer "which one" at all — and neither can the downstream service that was deciding whether to trust the caller.
cybertravels-svc. The payments API can see that CyberTravels called it and cannot see which of the four — so the refund and the itinerary lookup are indistinguishable to the thing deciding whether to trust the caller. R11.What it covers. Have two agents share a credential, then try to work out which one made the call.
Why a security engineer needs it. Attribution fails before the incident starts: you cannot say which agent acted, so you cannot revoke one without breaking all of them. The control it builds is: per-workload identity with attestation (B2.1, B2.2) and a lifecycle that can revoke one (B2.5).
This is a risk lesson: it shows the failure happening before anything tries to stop it, so the control that follows is answering something you have already watched go wrong.
You cannot say which agent acted, so you cannot revoke one without stopping all of them.
Per-workload identity with attestation, and a lifecycle that can revoke a single agent.
Agents with an attested, individually revocable identity, as a share of the fleet.
Measure this
| what to measure | denominator or target |
|---|---|
| agents with an attested, individually revocable identity | share of the fleet |
agent A --+
agent B --+---> one service account ---> downstream service
agent C --+ "svc-automation" |
agent D --+ v
"who called me?" -> unanswerable
the audit log is complete and useless: every row has the same subjectOWASP T9 — Identity Spoofing & Impersonation.
B1.6 was about an agent holding too much authority. This one is about the identity component being unable to say which agent is calling at all.
When several agents share one credential — the same service account, the same API key baked into the same image — they are, to every downstream system, the same principal. There is no spoofing step required. Impersonation is the default state, because there was never a distinction to defeat.
Three consequences follow, and the third is the one that hurts during an incident:
Authorization cannot differ. Every agent gets the union of what any of them needs, which is B1.6 again, arriving through a different door.
Attribution is impossible. "Which agent called this?" has no answer. Not a hard answer — no answer, because the information was never present.
Revocation is all-or-nothing. You have one misbehaving agent and one credential shared by forty. Rotating it stops the incident and stops the other thirty-nine, so the decision becomes a business call in the middle of a response, at whatever hour it is.
In a multi-agent topology this compounds. A peer's message is trusted because it came from a peer — but if identity cannot distinguish peers, "it came from a peer" is a claim anyone inside the perimeter can make.
Where this lands on the reference architecture. `` ingress -> orchestrator -> agent_runtime -> model | | messaging tools / mcp | | knowledge / memory egress identity + policy wrap every call · observability records it ``
Three agents, one credential, one incident.
Three CyberTravels agents share one API key, so the payments API records one caller on every line. The skill measures both costs: what the record can attribute, and what revoking the key would stop.
The skill — skills/threats/shared-credential-attribution-check/SKILL.md
name: shared-credential-attribution-check
description: >-
Find credentials held by more than one agent and show what sharing does to
attribution and to containment — who the downstream records, and what
revoking it stops. Use when several agents call the same API, or when asked
which agent did something and the record cannot say.
allowed-tools: Read, Grep, GlobA shared credential is usually adopted for convenience and paid for during an incident. It costs two things at once: the downstream cannot attribute an action to an agent, and the only containment available stops every holder.
Whenever more than one agent, job or replica authenticates downstream. Also before an incident: this is the check whose absence turns a contained problem into an outage.
1 — Enumerate holders per credential. Group by the secret, not by the service. Environment variables, mounted files, a shared secrets-manager path and a baked-in image layer are all the same credential when the value matches.
2 — Read a downstream record. Whatever the caller is identified by — an API key id, a client id, a service account — record what the downstream can print. If three agents map to one identifier, attribution ends there.
3 — Simulate the destructive call. Have one holder perform something irreversible. Ask, from the downstream record alone, which holder did it. Write down the answer even when it is "cannot be determined"; that sentence is the finding.
4 — Cost the containment. Revoke the credential on paper and list what stops. The count of unrelated things that stop is the number to report.
5 — Propose per-workload identity, and say what it costs: one credential per agent, issued by the platform rather than pasted, so revocation is per-agent and attribution is free.
Input — the fixture committed at the top of scripts/shared_credential_attribution_check.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:
what the downstream service recorded:
caller=svc-agent-7f3a1c read reports
caller=svc-agent-7f3a1c read reports
caller=svc-agent-7f3a1c read reports
caller=svc-agent-7f3a1c delete prod.customers
incident: delete on prod.customers
which agent did it? candidates: ['deploy-agent', 'patch-agent', 'triage-agent']
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.
{
"credentials": [{"id": "str", "holders": ["str"], "source": "env|file|manager|image"}],
"downstream_identifier": {"field": "str", "distinguishes_holders": false},
"destructive_probe": {"actor": "str", "recoverable_from_record": false},
"containment": {"revoking_stops": ["str"], "collateral": 0}
}
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.
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 B1.7Then restart your agent and open it in that folder. On Windows, if python3 says Python was not found, use python instead.
Then pick b1-7-identity-spoofing-and-impersonation:
| agent | install just for this agent | start it |
|---|---|---|
| Claude Code | python3 scripts/install_skills.py --tool claude --lessons B1.7 | type /b1-7-identity-spoofing-and-impersonation |
| GitHub Copilot (VS Code) | python3 scripts/install_skills.py --tool copilot --lessons B1.7 | type /b1-7-identity-spoofing-and-impersonation in Chat |
| Cursor | python3 scripts/install_skills.py --tool cursor --lessons B1.7 | type / in Agent chat and search for it |
| Codex | python3 scripts/install_skills.py --tool agents --lessons B1.7 | type $b1-7-identity-spoofing-and-impersonation, or run /skills |
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 B1.7Neither 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 B1.7 --out work/cybertravels
python3 scripts/checkpoint.py --at B1.7 --diffOn 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.
Three agents share one credential, so the downstream record shows a single caller on every line. When one deletes a production table the culprit is not recoverable from the record, and the only containment available stops all three.
Count the distinct credentials across your agents and divide by the number of agents. Any answer below one is this risk, and the number tells you how many innocent agents a revocation takes down.
Tools used
Skill source: skills/threats/shared-credential-attribution-check