Chapter 7: Least Privilege and Never Running as Root

Learning Objectives

Pre-Reading Check — The Principle of Least Privilege

1. What does the principle of least privilege (PoLP) state?

Every process should be granted only the minimum permissions required for its authorized function, and nothing more.
Every process should be granted broad permissions up front to avoid runtime failures.
Permissions should be assigned based on the seniority of the human who owns the process.
A process should hold at least one administrative credential as a fallback.

2. Why does least privilege matter specifically for an agent that reads untrusted inbound messages?

It makes the agent respond faster to legitimate requests.
It bounds what a hijacked agent can actually do to exactly the capabilities its legitimate tasks require.
It prevents injected instructions from ever reaching the model.
It encrypts the untrusted messages before the agent reads them.

3. In OAuth 2.0, what is the role of a scope?

It sets how long a session cookie remains valid in the browser.
It defines exactly which operations a client may request, limiting a token to what an operation needs.
It encrypts the access token so it cannot be read in transit.
It maps a machine identity to a human employee's login.

4. What is just-in-time (JIT) access?

Granting the agent standing admin rights so it never waits for a permission.
A broker issuing an ephemeral, task-scoped credential only when policy allows, valid for minutes.
Caching a long-lived token locally to avoid repeated network calls.
Sharing one admin token across every environment for consistency.

5. According to the chapter, an agent's blast radius is a function of which three variables?

CPU limit, memory limit, and disk quota.
Scope width, credential duration, and environment isolation.
Model size, prompt length, and temperature.
Number of tools, number of users, and log retention.

The Principle of Least Privilege for Agents

Key Points

Isolation (Chapter 6) decides where the agent can act; least privilege decides how much authority it carries when it does. An agent granted exactly the capabilities its legitimate tasks require will, when hijacked, be able to do exactly those tasks and no more. An agent granted "everything, just in case" becomes an attacker's Swiss Army knife.

The design rule is that permissions align with specific workflows, not broad team roles. In the API world the primary mechanism is the scope: if the assistant only ever needs to read your schedule, requesting write access is a self-inflicted wound. The same discipline applies to the filesystem — a scoped storage token, a working directory the agent can write to instead of the whole disk, and read-only mounts for anything it only needs to consult.

A permission that exists forever is a permission an attacker can use forever. Short-lived access tokens (on the order of 15–60 minutes) drastically reduce the window of opportunity. The strongest form is just-in-time (JIT) access: instead of the agent holding standing permissions, a broker mints an ephemeral, task-scoped token only when policy allows and withdraws it on completion.

Figure 7.1: Just-in-time access lifecycle

stateDiagram-v2 [*] --> NoStandingAccess NoStandingAccess --> RequestingAccess: Agent needs to run a task RequestingAccess --> Denied: Policy check fails RequestingAccess --> Issued: Policy allows Denied --> NoStandingAccess Issued --> InUse: Broker mints ephemeral, task-scoped token InUse --> Expired: Lifetime elapses (15-60 min) InUse --> Withdrawn: Task completes Expired --> NoStandingAccess Withdrawn --> NoStandingAccess NoStandingAccess --> [*]

These three levers together determine an agent's blast radius — the total damage a compromise can cause. A long-lived, cross-environment, administrative credential has a blast radius covering the whole organization; an agent with a 15-minute, task-scoped token confined to staging has a blast radius bounded by a handful of API endpoints.

Figure 7.2: The three levers that determine an agent's blast radius

graph TD BR["Blast radius (damage a compromise can cause)"] S["Scope width"] D["Credential duration"] E["Environment isolation"] S --> BR D --> BR E --> BR S -.->|"Admin / all APIs"| SW["Wide blast radius"] S -.->|"One workflow, one resource"| SN["Narrow blast radius"] D -.->|"Non-expiring static key"| SW D -.->|"15-60 min, JIT-issued"| SN E -.->|"Shared across dev/staging/prod"| SW E -.->|"One credential per environment"| SN
LeverBroad (dangerous)Narrow (least privilege)
Scope widthAdmin / all APIsOne workflow, one resource
Credential lifetimeNon-expiring static key15–60 min, JIT-issued
Environment isolationShared across dev/staging/prodOne credential per environment
Resulting blast radiusWhole organizationA few endpoints, briefly

Visual animation — coming soon

Post-Reading Check — The Principle of Least Privilege

1. What does the principle of least privilege (PoLP) state?

Every process should be granted only the minimum permissions required for its authorized function, and nothing more.
Every process should be granted broad permissions up front to avoid runtime failures.
Permissions should be assigned based on the seniority of the human who owns the process.
A process should hold at least one administrative credential as a fallback.

2. Why does least privilege matter specifically for an agent that reads untrusted inbound messages?

It makes the agent respond faster to legitimate requests.
It bounds what a hijacked agent can actually do to exactly the capabilities its legitimate tasks require.
It prevents injected instructions from ever reaching the model.
It encrypts the untrusted messages before the agent reads them.

3. In OAuth 2.0, what is the role of a scope?

It sets how long a session cookie remains valid in the browser.
It defines exactly which operations a client may request, limiting a token to what an operation needs.
It encrypts the access token so it cannot be read in transit.
It maps a machine identity to a human employee's login.

4. What is just-in-time (JIT) access?

Granting the agent standing admin rights so it never waits for a permission.
A broker issuing an ephemeral, task-scoped credential only when policy allows, valid for minutes.
Caching a long-lived token locally to avoid repeated network calls.
Sharing one admin token across every environment for consistency.

5. According to the chapter, an agent's blast radius is a function of which three variables?

CPU limit, memory limit, and disk quota.
Scope width, credential duration, and environment isolation.
Model size, prompt length, and temperature.
Number of tools, number of users, and log retention.
Pre-Reading Check — Never Run as Root

6. Why is running a container as root especially dangerous compared to a virtual machine?

Containers are slower, so an attack has more time to run.
Containers share the host's kernel, so a breached root container can escalate and escape to the host.
Containers cannot use scoped tokens, unlike virtual machines.
Root inside a container automatically disables all logging.

7. What does the Dockerfile USER directive accomplish?

It makes the process run as a specified non-root user instead of the default root.
It grants the process additional Linux capabilities at startup.
It sets the password required to enter the container.
It mounts the host's Docker socket into the container.

8. Why add --cap-drop ALL and no-new-privileges even when the container already runs as a non-root user?

They speed up container startup time.
They prevent a non-root process from regaining elevated privileges through setuid binaries or capability assignment.
They are required before the USER directive will take effect.
They automatically rotate the agent's credentials.

9. In a Kubernetes securityContext, which field does the chapter call "the one to never get wrong," and why?

readOnlyRootFilesystem, because it stops all disk writes.
runAsUser, because it picks the UID number.
allowPrivilegeEscalation, because leaving it true can give an attacker root on the host.
capabilities.add, because it grants network access.

10. What is the chapter's rule about privileged containers and the Docker --privileged flag for an agent that reads untrusted input?

Use privileged mode by default so tools always work, then remove it later.
Privileged mode is fine as long as the container also runs as a non-root user.
Never use privileged mode or a mounted host socket; add a specific capability back only when a concrete task provably requires it.
Privileged mode should be used whenever the container needs to write to /tmp.

Never Run as Root

Key Points

Running an agent as root violates least privilege — it grants far more capability than any well-defined task needs. Containers share the host's kernel, so if a container running as root is breached, any flaw in the runtime or the container itself may let an attacker break out and take control of the entire host. A root container also has unrestricted access to the filesystem and raises the odds of exploiting zero-day vulnerabilities.

It helps to know about Linux capabilities: granular permissions that grant specific privileged operations normally reserved for root, such as CAP_CHOWN or CAP_NET_RAW. Even a non-root process can regain elevated privileges through setuid binaries or capability assignment — ping, for instance, typically carries CAP_NET_RAW — which is why we close both the "starts as root" and the "climbs back to root" paths.

Figure 7.3: How a single bug in a root container escalates to full host compromise

flowchart TD A["Injection or bug in agent process"] --> B{"Running as root?"} B -->|"Non-root, capabilities dropped"| C["Damage bounded to the process"] B -->|"Root inside container"| D["Full container control"] D --> E["Read/modify/delete any container file"] D --> F["Abuse volume mounts and Docker socket"] D --> G["Exploit runtime flaw to break out"] F --> H["Container escape to host"] G --> H H --> I["Full host compromise"]

A minimal non-root Dockerfile

FROM python:3.12-slim

# Create a dedicated, unprivileged user and group
RUN groupadd --system agent && \
    useradd --system --gid agent --home /app agent

WORKDIR /app
COPY --chown=agent:agent . /app
RUN pip install --no-cache-dir -r requirements.txt

# Drop from root to the unprivileged user for everything that follows
USER agent

CMD ["python", "assistant.py"]

At run time, add the belt-and-suspenders layer:

docker run \
  --cap-drop ALL \                 # remove every Linux capability
  --read-only \                    # root filesystem is read-only
  --tmpfs /tmp \                   # writable scratch space, not the whole disk
  --security-opt no-new-privileges \  # block setuid/capability re-escalation
  personal-assistant:latest

Figure 7.4: Privilege-dropping flow

flowchart TD A["Image build: default UID 0 (root)"] --> B["Create dedicated unprivileged user"] B --> C["Dockerfile USER agent: drop from root at startup"] C --> D["Runtime: --cap-drop ALL removes every capability"] D --> E["--security-opt no-new-privileges blocks setuid re-escalation"] E --> F["--read-only root FS plus --tmpfs scratch"] F --> G["Process runs with least privilege"]

In Kubernetes, the same intent is expressed declaratively through the pod or container securityContext:

FieldEffect
runAsNonRoot: trueRefuses to start any container that would run as UID 0
runAsUser / runAsGroupPins a specific non-root UID/GID
allowPrivilegeEscalation: falseBlocks a process from gaining more privileges than its parent (stops setuid/capability escalation)
readOnlyRootFilesystem: truePrevents writes to the root filesystem
capabilities.drop: ["ALL"]Removes every Linux capability, minimizing attack surface
securityContext:
  runAsNonRoot: true
  runAsUser: 10001
  allowPrivilegeEscalation: false
  readOnlyRootFilesystem: true
  capabilities:
    drop: ["ALL"]

The field to never get wrong is allowPrivilegeEscalation. When left enabled (true), it can potentially give an attacker root access to the host machine, so it should always be disabled. Beyond the default, some deployments deliberately grant more than root — the --privileged flag, added host capabilities, or a mounted Docker socket. For an agent that reads untrusted input these are precisely the wrong direction: non-root by default, all capabilities dropped, no privileged mode, and no host socket.

Visual animation — coming soon

Post-Reading Check — Never Run as Root

6. Why is running a container as root especially dangerous compared to a virtual machine?

Containers are slower, so an attack has more time to run.
Containers share the host's kernel, so a breached root container can escalate and escape to the host.
Containers cannot use scoped tokens, unlike virtual machines.
Root inside a container automatically disables all logging.

7. What does the Dockerfile USER directive accomplish?

It makes the process run as a specified non-root user instead of the default root.
It grants the process additional Linux capabilities at startup.
It sets the password required to enter the container.
It mounts the host's Docker socket into the container.

8. Why add --cap-drop ALL and no-new-privileges even when the container already runs as a non-root user?

They speed up container startup time.
They prevent a non-root process from regaining elevated privileges through setuid binaries or capability assignment.
They are required before the USER directive will take effect.
They automatically rotate the agent's credentials.

9. In a Kubernetes securityContext, which field does the chapter call "the one to never get wrong," and why?

readOnlyRootFilesystem, because it stops all disk writes.
runAsUser, because it picks the UID number.
allowPrivilegeEscalation, because leaving it true can give an attacker root on the host.
capabilities.add, because it grants network access.

10. What is the chapter's rule about privileged containers and the Docker --privileged flag for an agent that reads untrusted input?

Use privileged mode by default so tools always work, then remove it later.
Privileged mode is fine as long as the container also runs as a non-root user.
Never use privileged mode or a mounted host socket; add a specific capability back only when a concrete task provably requires it.
Privileged mode should be used whenever the container needs to write to /tmp.
Pre-Reading Check — Dedicated, Non-Sensitive Credentials

11. Why is authenticating the agent with the operator's own personal login a mistake?

Personal logins are technically incompatible with automated software.
It hands the agent the human's entire standing footprint, making the blast radius the operator's whole access.
Personal logins cannot be used with OAuth scopes.
It forces the agent to run as root.

12. What is a service account?

A shared team login used by everyone in an organization.
A non-human identity created for a program rather than a person, with its own documented ownership and purpose.
A backup copy of the operator's personal credentials.
A root account reserved for administrative tasks.

13. Beyond shrinking blast radius, what second reason does the chapter give for a dedicated agent identity?

Attribution: shared accounts make agent actions indistinguishable from the human's in the logs.
It lets the agent run faster by skipping login checks.
It removes the need to sandbox the agent.
It encrypts all of the agent's network traffic automatically.

14. In the OAuth 2.0 client-credentials flow, why is a self-expiring JWT preferable to a long-lived opaque secret?

JWTs cannot be stolen because they are encrypted end to end.
The token expires automatically without requiring explicit revocation, and being a JWT it can be validated locally.
Opaque secrets are illegal for machine-to-machine access.
JWTs grant the agent broader permissions, which is more convenient.

15. When an agent acts on behalf of a human, what is the recommended way to handle the user's token?

Pass the user's original token straight to each downstream service.
Exchange it for a new, narrowly scoped token carrying the delegation context, rather than passing the user's own credential downstream.
Store the user's token in the container image so it is always available.
Grant the agent admin rights so it never needs the user's token.

Dedicated, Non-Sensitive Credentials

Key Points

It is tempting to authenticate the agent with your own Google login or GitHub token because those already exist and work. This is a mistake with the same shape as running as root: it hands the agent your entire standing footprint. Agents should have distinct, managed identities — each with its own dedicated service account, client ID, or X.509 certificate tailored to its specific role. Reusing a human's personal account gives an automated agent the full breadth of that human's standing permissions, defeating least privilege.

There is a second reason beyond blast radius: attribution. If an agent shares your account, its actions are indistinguishable from yours in every log. All agent API calls should be logged under individual agent identities so anomaly detection and audit trails can be specific to each agent.

The credential the agent holds should itself be low-value: issued just in time, scoped to the task, and expired immediately after use. A clean pattern for machine-to-machine access is the OAuth 2.0 client-credentials flow — the agent authenticates with a client ID and secret to receive a short-lived JWT that expires on its own, rather than holding a long-lived opaque secret. When the agent acts on behalf of a human, it should exchange the user's token for a new, appropriately scoped token that carries the delegation context, keeping effective privilege narrow.

Figure 7.5: Separating the agent's dedicated service account from the operator's personal login

flowchart LR subgraph Wrong["Borrowed personal identity"] H1["Operator's personal login"] --> H2["Agent authenticates as you"] H2 --> H3["Full standing footprint: whole inbox, all calendars"] H3 --> H4["If hijacked: attacker inherits your digital life"] end subgraph Right["Dedicated service account"] S1["bot@ service account you own"] --> S2["Scoped, short-lived, JIT-issued token"] S2 --> S3["One shared calendar, one low-value mailbox"] S3 --> S4["If hijacked: attacker gets a small, attributed slice"] end

The final layer is architectural: keep the agent's world separate from your most valuable assets. Consider a concrete comparison for a personal assistant that manages your calendar and drafts email:

Design choicePersonal-account approachDedicated service-account approach
IdentityYour Google loginA bot@… service account you own
Calendar accessFull read/write to all calendarsRead/write to one shared "Assistant" calendar
Email accessYour full inbox and send-as-youA dedicated address; drafts to you for approval
TokenLong-lived personal OAuth tokenScoped, short-lived, JIT-issued token
If injected and hijackedAttacker inherits your whole digital lifeAttacker gets one calendar and a low-value mailbox
In the audit logIndistinguishable from youClearly attributed to the bot

The second column costs a little more to set up. It is the difference between an incident and a catastrophe.

Visual animation — coming soon

Post-Reading Check — Dedicated, Non-Sensitive Credentials

11. Why is authenticating the agent with the operator's own personal login a mistake?

Personal logins are technically incompatible with automated software.
It hands the agent the human's entire standing footprint, making the blast radius the operator's whole access.
Personal logins cannot be used with OAuth scopes.
It forces the agent to run as root.

12. What is a service account?

A shared team login used by everyone in an organization.
A non-human identity created for a program rather than a person, with its own documented ownership and purpose.
A backup copy of the operator's personal credentials.
A root account reserved for administrative tasks.

13. Beyond shrinking blast radius, what second reason does the chapter give for a dedicated agent identity?

Attribution: shared accounts make agent actions indistinguishable from the human's in the logs.
It lets the agent run faster by skipping login checks.
It removes the need to sandbox the agent.
It encrypts all of the agent's network traffic automatically.

14. In the OAuth 2.0 client-credentials flow, why is a self-expiring JWT preferable to a long-lived opaque secret?

JWTs cannot be stolen because they are encrypted end to end.
The token expires automatically without requiring explicit revocation, and being a JWT it can be validated locally.
Opaque secrets are illegal for machine-to-machine access.
JWTs grant the agent broader permissions, which is more convenient.

15. When an agent acts on behalf of a human, what is the recommended way to handle the user's token?

Pass the user's original token straight to each downstream service.
Exchange it for a new, narrowly scoped token carrying the delegation context, rather than passing the user's own credential downstream.
Store the user's token in the container image so it is always available.
Grant the agent admin rights so it never needs the user's token.

Your Progress

Answer Explanations