All lessons › Function B — Securing AI Architectures › CyberTravels' Architecture, and Every Risk It Carries
B1.7

Identity spoofing and impersonation

B1
▶
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/B1.7.mp4
Risk

Attribution fails before the incident starts: you cannot say which agent acted, so you cannot revoke one without breaking all of them.

Control

Per-workload identity with attestation (B2.1, B2.2) and a lifecycle that can revoke one (B2.5).

The lab

Have two agents share a credential, then try to work out which one made the call.

◉Use case relevance

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.

At CyberTravels. All four agents share one service account, 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 this lesson is — Day 0, Day 1, Day 2

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.

Day 0 — why

You cannot say which agent acted, so you cannot revoke one without stopping all of them.

Day 1 — how

Per-workload identity with attestation, and a lifecycle that can revoke a single agent.

Day 2 — measure

Agents with an attested, individually revocable identity, as a share of the fleet.

Measure this

what to measuredenominator or target
agents with an attested, individually revocable identityshare of the fleet

▦The framework, and how it works

   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 subject

OWASP 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 ``

2 · The risk, realised

Three agents, one credential, one incident.

3 · The check, as a skill

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.

▶Real time execution as skill

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, Glob

One credential, three agents, one line in the log

A 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.

When to use this

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.

Procedure

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.

Example

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.

Output contract

{
  "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}
}

Failure modes

  • Grouping by service instead of by secret value. Two names, one key, is still one credential.
  • Assuming the log will disambiguate. Read an actual row before assuming a field exists.
  • Reporting only attribution. The containment cost is what makes it urgent.

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 B1.7

Then 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:

agentinstall just for this agentstart it
Claude Codepython3 scripts/install_skills.py --tool claude --lessons B1.7type /b1-7-identity-spoofing-and-impersonation
GitHub Copilot (VS Code)python3 scripts/install_skills.py --tool copilot --lessons B1.7type /b1-7-identity-spoofing-and-impersonation in Chat
Cursorpython3 scripts/install_skills.py --tool cursor --lessons B1.7type / in Agent chat and search for it
Codexpython3 scripts/install_skills.py --tool agents --lessons B1.7type $b1-7-identity-spoofing-and-impersonation, 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 B1.7

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 B1.7 --out work/cybertravels
python3 scripts/checkpoint.py --at B1.7 --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

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.

✎Your turn

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

SPIFFE/SPIRE

Skill source: skills/threats/shared-credential-attribution-check

← PreviousB1.6 · Privilege compromise