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

Privilege compromise

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.6.mp4
Risk

The agent acts with more authority than the person who asked it to act, and the log records the service account rather than the human.

Control

Delegation that narrows (B2.3), just-in-time grants (B2.4), and default-deny (B3.1).

The lab

Have an agent inherit a privileged token and reach something its requester never could.

◉Use case relevance

An engineer's agent inherits the engineer's standing permissions, because that is the fastest way to make it useful. It now holds production write access at three in the morning, when its principal is asleep and cannot be surprised by anything it does.

At CyberTravels. CyberTravels inherits Alex's standing permissions because that was the fastest way to make it useful. It now holds production write access at three in the morning, when Alex is asleep and cannot be surprised by anything it does. R1.

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

What it covers. Have an agent inherit a privileged token and reach something its requester never could.

Why a security engineer needs it. The agent acts with more authority than the person who asked it to act, and the log records the service account rather than the human. The control it builds is: delegation that narrows (B2.3), just-in-time grants (B2.4), and default-deny (B3.1).

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

The agent acts with more authority than the person who asked, and the log records a service account rather than either of them.

Day 1 — how

Delegation that narrows, grants that expire, and default-deny underneath both.

Day 2 — measure

Share of actions whose effective scope sits inside the requesting human's own ceiling.

Measure this

what to measuredenominator or target
actions whose effective scope sits inside the requesting human's ceilingshare of actions

▦The framework, and how it works

   human principal                      agent
   +-------------------+                +------------------+
   | repo:write        |  inherits ALL  | repo:write       |
   | deploy:prod       | -------------> | deploy:prod      |
   | secrets:read      |   standing     | secrets:read     |
   +-------------------+                +------------------+
     awake 8 hours/day                    awake 24 hours/day
     asked before acting                  acts on retrieved text

OWASP T3 — Privilege Compromise. LLM06 — Excessive Agency.

Tool misuse is about what a tool can do. Privilege compromise is about whose authority it does it with — the identity component rather than the tools component.

Three patterns produce it, and all three are things teams do for good reasons.

Inherited human credentials. The agent runs with the token of the user who started it. Convenient, and it means the agent holds every permission that user holds — including the ones irrelevant to the task, and including the ones they hold because of a role they were given three years ago.

Shared service accounts. Every agent authenticates as agent-svc. That account needs the union of everything any agent ever needs, so each agent holds the maximum of the set.

Standing scope. The grant is permanent because renewing it was operationally awkward. The authority is therefore present at the moment any injection lands.

What makes this distinct from ordinary over-permissioning is the direction of the audit trail. When a human has too much access and misuses it, the log names them. When an agent does it, the log names the service account — and the human who caused it is not in the record at all. The compromise is of privilege and of attribution, at the same time.

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

A read-only user asks for something, and the agent has more authority than they do.

3 · The check, as a skill

Everything turns on which principal the decision was evaluated against. The skill runs the asymmetric probe — a traveller holding reports:read asking for something that needs db:admin — and then reads one audit row to see whether a human can be recovered from it.

▶Real time execution as skill

The skill — skills/threats/authorization-subject-check/SKILL.md

name: authorization-subject-check
description: >-
  Establish which principal a tool call is actually authorised against — the
  requesting human, or the agent's own service account — and what the resulting
  audit row can name. Use when a user reaches a scope they do not hold, or when
  every log line shows the same caller.
allowed-tools: Read, Grep, Glob

Authorised as whom?

Privilege compromise in an agentic system is rarely a stolen credential. It is authorisation evaluated against the agent's identity while the request came from a user who does not hold the scope — and an audit trail that then names the agent on every row, so the human cannot be recovered at all.

When to use this

Any time an agent calls something on a user's behalf. Run it before designing delegation, because the answer decides whether delegation is missing or merely unenforced.

Procedure

1 — Find the authorisation decision point. The single place where a scope is compared against a holder. If there are several, list them all; they will disagree.

2 — Name the subject at that point. Is the compared identity the requesting user, or the process's own account? A service account with a union of every scope any user might need is the shape to look for.

3 — Run the asymmetric probe. Have a user holding a narrow scope request an action requiring a wider one. If it succeeds, authorisation is on the workload, not the requester.

4 — Read one audit row. Does it name a human? A row naming agent-svc is complete, well-formed and useless: it answers "what ran", never "who caused it".

5 — Separate the two fixes. Authorising on the requester and recording the requester are different changes with different owners; a report that merges them gets half-implemented.

Example

Input — the fixture committed at the top of scripts/authorization_subject_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:

requestertheir scopes                                asked for       allowed?
dana    ['reports:read']                            db:admin        True
priya   ['db:admin', 'reports:read', 'reports:write']db:admin        True

AUDIT TRAIL
   actor=agent-svc  tool=drop_table   allowed=True
   actor=agent-svc  tool=drop_table   allowed=True

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

{
  "decision_points": [{"site": "str", "subject": "requester|workload|both"}],
  "probe": {"user_scopes": ["str"], "required": "str", "succeeded": true},
  "audit_row": {"names_human": false, "fields": ["str"]},
  "fixes": {"authorize_on_requester": true, "record_requester": true}
}

Failure modes

  • Accepting that the user is "in the request". Present in the payload is not the same as compared at the decision point.
  • Testing with an admin. The probe needs a user who genuinely lacks the scope.
  • Reporting the audit gap as a logging bug. It is the reason the incident cannot be scoped later.

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

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-6-privilege-compromise:

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

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

A user holding only reports:read triggers a db:admin action, because authorization was evaluated against the shared agent service account rather than the requester — and the audit trail names agent-svc on every row, so the human who caused it cannot be recovered from it at all.

✎Your turn

Pick one agent and answer two questions: what identity does it authenticate as, and can you name the human behind any single action it took last week. If the second answer is no, you have this risk regardless of how the scopes are set.

Tools used

KeycloakSPIFFE/SPIRE

Skill source: skills/threats/authorization-subject-check

← PreviousB1.5 · Tool misuse