Dev.to Security πŸ” Cybersecurity πŸ‘ 0 πŸ“– 18 min read

Your AI Agent Has Access to Everything. Who Is Controlling It?

You build an agent that helps your team with software projects. At first it reads GitHub issues and summarizes pull requests. Then it gets a cloud integration to inspect failed deployments. A month later, someone adds "r

You build an agent that helps your team with software projects. At first it reads GitHub issues and summarizes pull requests. Then it gets a cloud integration to inspect failed deployments. A month later, someone adds "restart service" because it saves a trip to the console.

Each step was reasonable. The product got better every time.

But look at what you now run: a system that interprets instructions, chooses tools at runtime, and calls real infrastructure using credentials your application hands it. Gartner has predicted that 40% of enterprise apps will embed task-specific agents by 2026, up from under 5% the year before. Most of those agents will be wired up the way yours was: fast, incrementally, and with whatever credential was nearby.

So ask yourself:

  • Which identity does the agent present to GitHub?
  • Can it reach every repo the developer who configured it can reach?
  • Does its cloud credential allow reading state, changing it, or both?
  • Can you tell which human started a given action?
  • If it misbehaves right now, can you cut its access in seconds?
  • If one component is compromised, what else becomes reachable?

None of these are prompt engineering questions. They are identity, authorization, and architecture questions. This post is a practical walkthrough of how to answer them, with the standards that already exist and code you can adapt.

An agent should not hold authority just because it is capable of using it.

1. Why agents strain the old assumptions

Software has always needed identities. A backend authenticates to a database, a CI job pulls from object storage. What changes with agents is that the execution path is partly decided at runtime.

You authorize an agent to "investigate a failed deployment." It reads logs, queries metrics, reads config, and proposes a fix. The task was narrow. The permissions behind it probably were not:

  • If it runs on a developer's personal token, it inherits everything that developer can do.
  • If it runs on a shared service account, nobody can tell its actions from other automation.
  • If it holds a long-lived API key, the key outlives the task, and so does the blast radius of a leak.

This is not a brand-new problem. IAM, OAuth, workload identity, and secrets management already exist. The hard part is applying them to a caller whose next step you cannot predict.

The standards bodies agree. In February 2026, NIST's National Cybersecurity Center of Excellence (NCCoE) published a concept paper proposing a project on applying existing identity standards to software and AI agents. It is a proposal, not a standard. NIST then followed with a two-part blog series: Why Agentic AI Needs a Strong Identity Foundation (August 27, 2026) and Design Tenets for Agent Identity Management Systems (October 27, 2026), which lists ten proposed tenets.

Gartner's guidance points the same way. As summarized by Descope, its January 2026 report recommends prohibiting the sharing of human credentials with agents and requiring a unique identity with an accountable human owner for every agent.

Three independent sources, one message: treat the agent as its own principal.

2. The model proposes. It must not decide.

Before designing permissions, separate the parts that people usually blur into "the agent":

Component Job
Model Interprets context, suggests the next action
Agent runtime Runs the loop, holds state, issues tool calls
Tool layer Exposes a fixed set of approved operations
Identity provider Establishes which workload is calling
Authorization layer Decides whether that identity may do this, here, now
Resource service Enforces access on the real data or operation
Audit system Records what happened and under whose authority

Take a tool called deleteRepository. The model may conclude that deleting a repo helps its goal. That conclusion is not a permission.

NIST's seventh tenet says it directly: the logic that decides what an agent attempts must be architecturally separate from the systems that decide what it is allowed to do. The post also states that guardrails alone are insufficient.

So a system prompt line like "never delete important data" is a hint, not a control. Enforcement belongs where access is actually granted.

The component that decides what to attempt must not be the only component deciding what is allowed.

3. An API key is a credential, not an identity

The default pattern looks like this:

const client = new SomeServiceClient({ apiKey: process.env.SERVICE_API_KEY });

Nothing is wrong with API keys where a provider is built around them. The trouble starts when that key becomes the agent's general-purpose authority.

NIST's first blog is blunt about why: static API keys and bearer tokens don't prove who is calling, they only prove the caller has the string. Anyone holding it can present it, and the post notes such credentials routinely end up in config files, markdown files, and logs.

Concretely, the key answers one question ("does the caller possess an accepted secret?") and leaves the rest open:

  • Which workload is calling?
  • Which human or process authorized this task?
  • Which resources and operations are in scope?
  • How long is the permission valid, and can it be withdrawn?
  • Can the action be attributed afterward?

The fix is not "swap the API key for a token." A broadly scoped token is still broad. The shift that matters is from "the agent has a credential" to "the agent has a verifiable identity and a narrow, expiring grant of authority."

4. Give the agent its own identity

Picture three actors in a team: a developer, a PR-review agent, and a release pipeline. If the review agent uses the developer's personal token, audit logs say the developer did things the developer never did. If it uses the shared deploy credential, it blends into other automation.

Give it a dedicated identity instead:

Identity Intended authority
Developer Whatever their role grants
PR-review agent Read approved repos, post review comments
Release agent Publish a pre-approved release via a controlled workflow
Monitoring agent Read selected metrics and logs

These are examples, not templates. The properties that matter: a distinct identity, a named human or team owner, constrained permissions, and an independent lifecycle. You should be able to disable the agent without disabling its owner, and when an owner leaves, you need a deliberate policy for the agents that depended on them.

For implementation, SPIFFE gives software workloads cryptographic identities (SPIFFE IDs) via SVIDs. Cloud-managed identities and workload identity federation serve the same purpose in single-cloud setups. A small app may need nothing heavier than a managed service identity.

A real example of the payoff: OpenAI supports exchanging a SPIFFE JWT-SVID for a short-lived access token, so the workload holds no long-lived API key at all. Two gotchas from their docs are worth knowing before you try it: the JWT-SVID must include iss, iat, and a kid header, and X.509-SVIDs are not supported as subject tokens. Setup details are provider-specific, but the pattern generalizes: prove identity through an identity system, receive a credential scoped and timed for the job.

5. Authorize the task, not the toolbox

Least privilege is old advice. What's new is applying it when the caller picks tools on the fly.

A PR-review agent needs to read the PR, read relevant files, maybe run approved tests, and post comments. It does not need to delete repositories, change org security settings, mint admin accounts, publish releases, or touch production.

Express the grant as data, scoped to one task, with an expiry, using an allowlist only. A deny-list next to an allowlist adds confusion without adding safety.

type Grant = {
  agentId: string;
  taskId: string;
  initiatedBy: string;           // human or scheduler that started the task
  resources: string[];           // exact IDs, e.g. "github:acme/payments-api"
  actions: Set<string>;          // e.g. "pull_request.read"
  expiresAt: number;             // epoch ms
};

type Verdict = { allow: true } | { allow: false; reason: string };

function decide(grant: Grant | undefined, action: string, resource: string, now = Date.now()): Verdict {
  if (!grant)                          return { allow: false, reason: "no_grant" };
  if (now >= grant.expiresAt)          return { allow: false, reason: "grant_expired" };
  if (!grant.resources.includes(resource)) return { allow: false, reason: "resource_out_of_scope" };
  if (!grant.actions.has(action))      return { allow: false, reason: "action_not_granted" };
  return { allow: true };
}

Everything not explicitly granted is denied. Missing, expired, or ambiguous means no.

Now the part people skip: where this runs. A policy object is not a boundary. It becomes one only when every tool call passes through it, and when the downstream service enforces its own permissions too.

async function callTool(req: { agentId: string; taskId: string; tool: string; rawArgs: unknown }) {
  const tool = toolRegistry.get(req.tool);
  if (!tool) {
    await audit({ ...req, decision: "deny", reason: "unknown_tool" });
    throw new Error("Tool not permitted");
  }

  const args = tool.schema.parse(req.rawArgs);       // validate BEFORE authorizing
  const resource = tool.resourceOf(args);            // derived by code, never taken from the model
  const grant = await grants.lookup(req.taskId, req.agentId);
  const verdict = decide(grant, tool.action, resource);

  if (!verdict.allow) {
    await audit({ ...req, action: tool.action, resource, decision: "deny", reason: verdict.reason });
    throw new Error("Action not authorized");
  }

  if (tool.risk === "high" && !(await approvals.granted(req.taskId, tool.action, resource))) {
    await audit({ ...req, action: tool.action, resource, decision: "pending_approval" });
    throw new Error("Approval required");
  }

  // Short-lived credential minted for this call: right audience, right scope.
  const credential = await tokens.mint({
    agentId: req.agentId,
    audience: tool.audience,
    scope: tool.action,
    resource,
    ttlSeconds: 60,
  });

  await audit({ ...req, action: tool.action, resource, decision: "allow", policy: grant!.taskId });
  try {
    const result = await tool.execute(args, credential);
    await audit({ ...req, action: tool.action, resource, outcome: "success" });
    return result;
  } catch (err) {
    await audit({ ...req, action: tool.action, resource, outcome: "error" });
    throw err;
  }
}

A few details worth noticing:

  • The agent never decides whether its own request is allowed. It submits a request; deterministic policy answers.
  • The resource comes from validated arguments and tool code, not from a field the model can write freely.
  • The credential is minted per call with a narrow audience and a short TTL, so a leak in one tool does not unlock the rest.
  • Denials are logged too. A pattern of denied requests is one of the best early signals you will get.

This is a sketch, not a drop-in library. A production version needs verified caller identity, a real policy engine, structured errors, and a guarantee that no tool can reach a downstream service by a side door.

If your target API supports finer-grained authorization than your gateway does, use it. Rich Authorization Requests (RFC 9396) exist precisely because static, coarse scopes were not enough.

6. Delegation: acting for a user is not being the user

A manager asks an agent to build a report from one approved dataset. The manager can read twenty datasets. Should the agent get all twenty?

Almost certainly not. "The user can access it" does not imply "every agent acting for the user should."

A delegation model needs to keep three parties distinct: the initiator (human or process), the agent, and the downstream service. OAuth 2.0 Token Exchange (RFC 8693) gives you vocabulary and mechanics for this, and the distinction it draws is worth internalizing:

  • Delegation: the agent keeps its own identity and acts with explicitly delegated authority. Both parties are visible in the token.
  • Impersonation: the downstream service treats the caller as the original principal. Convenient, but the agent disappears from your audit trail.

For agents, delegation is usually what you want. In RFC 8693 terms, you present the initiator's grant as subject_token and the agent's own identity as actor_token:

POST /oauth2/token
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&subject_token=<task grant issued on behalf of user-123>
&subject_token_type=urn:ietf:params:oauth:token-type:access_token
&actor_token=<agent's JWT-SVID>
&actor_token_type=urn:ietf:params:oauth:token-type:jwt
&audience=https://analytics.internal
&scope=dataset.read

The issued token can carry both parties, via the act claim:

{
  "sub": "user-123",
  "act": { "sub": "spiffe://acme.example/agent/reporting" },
  "aud": "https://analytics.internal",
  "scope": "dataset.read",
  "exp": 1794398460
}

The analytics service can now see who started this and which agent is acting, and it can enforce that the scope is only dataset.read.

Two cautions. First, token exchange is only as safe as your authorization server's policy, audience restrictions, and the downstream service's validation. Second, delegation must attenuate: when an agent spawns a sub-agent, the child can hold at most what the parent holds, never more. NIST's tenets state this explicitly.

Delegation should narrow authority at every hop, never multiply it.

7. Short-lived is not the same as revocable

Say an agent holds a token valid for months and it leaks through a compromised dependency or a stray log line. The task ended long ago; the token still works.

Short-lived credentials shrink that window. A healthier flow looks like this:

  1. Establish the workload identity.
  2. Verify the task and its scope.
  3. Obtain a credential for the specific resource and audience.
  4. Execute.
  5. Renew only if policy still allows it.
  6. Record each authorization and outcome.

Pair this with sender-constraining where you can. DPoP (RFC 9449) binds a token to a key held by the caller, which NIST's blog points to as a mitigation for many token-theft scenarios, since a stolen bearer token alone becomes much less useful.

Now the part that bites in practice:

Short-lived does not mean instantly revocable.

A signed token typically stays valid until it expires unless the receiving service also checks revocation (RFC 7009), introspects (RFC 7662), or evaluates access continuously. Flipping a switch in your own dashboard does not recall a token a downstream service already accepted. Even OpenAI's federation rule reference notes that edits to a rule apply to new exchanges, while tokens issued earlier can live out their existing TTL.

So design your kill switch by layers, and know what each one does not do:

Step Stops Does not stop
Suspend the agent identity New task starts, new token exchanges Tokens already issued
Deny in the tool gateway All new tool calls through your gateway Calls made directly with a leaked token
Revoke at the provider (if supported) The credential itself Changes already made
Kill the running process The active loop Side effects already committed
Restore from backup / roll back Damage from completed actions Nothing, this is recovery, not prevention

For destructive or high-impact operations, rely on prevention (tight scope, approvals, short TTLs, resource-side limits) rather than emergency revocation after the fact. And test the kill switch before an incident, not during one.

8. Prompt injection makes authorization more important

An agent reads issues, emails, web pages, PR comments, support tickets. Any of those can contain instructions aimed at the agent.

This is not hypothetical. Security write-ups of the GitHub MCP "toxic agent flow" attack point to broadly scoped personal access tokens as the root cause: untrusted issue content steered an agent that held far more access than the task needed. The OWASP Secure Coding with AI cheat sheet lists the same class of risk, including malicious PR comments instructing an agent to modify unrelated files or exfiltrate code.

NIST frames this as a confused deputy problem: a user asks the agent to summarize a document, the document says "email the contents to this address," and an agent with drive-read and email-send happily does both. No single control stops that. Distinct identity, scoped grants, separated policy, and deterministic checks have to work together.

The important property: text the agent reads must never be able to rewrite what the agent is permitted to do. If injected content makes the model propose email.send, the gateway still denies it, because the grant never included it.

Be honest about limits, though. This does not make prompt injection go away, and least privilege does not stop leakage through channels the agent is legitimately allowed to use. If an agent can read secrets and post to an external channel, you have a data path regardless of identity. So also:

  • Keep trusted instructions and untrusted content separate wherever you can.
  • Validate tool arguments and destinations (including allowlisted egress).
  • Don't expose secrets the task doesn't need.
  • Require approval for sensitive external actions.
  • Treat repeated denials as a signal worth investigating.

9. "The agent did it" is not an incident report

When a production record disappears, you need answers fast: which agent, which task, which human or schedule started it, which tool, which resource, which policy allowed it, which credential, what outcome.

A useful audit event captures the authorization path without copying sensitive payloads:

{
  "event_id": "evt-789",
  "timestamp": "2026-11-11T12:00:00Z",
  "agent_id": "spiffe://acme.example/agent/deploy",
  "task_id": "task-456",
  "initiated_by": "user-123",
  "delegation_chain": ["user-123", "agent/deploy"],
  "action": "deployment.restart",
  "resource": "service/staging-api",
  "decision": "allow",
  "policy_version": "deploy-policy-v3",
  "outcome": "success",
  "prev_hash": "9f2c..."
}

Generate timestamps and IDs in a trusted component, never from model output. Keep credentials and raw request bodies out of logs.

Make the log tamper-evident and independent of the agent. NIST's tenets call for an append-only record stored separately from the agent, because a compromised agent that can touch its own logs can erase its tracks. A hash chain (prev_hash above) or a write-once store gets you most of the way.

One more reminder: an audit log is evidence, not prevention. You need both.

10. Human approval: risk-based, not constant

The reflex is "ask a human every time." It backfires. NIST's first blog warns about consent fatigue and compares it to MFA bombing: when people get flooded with prompts they start clicking "allow" without reading, and the accountability you wanted disappears.

Classify by impact instead:

Action Control
Read an approved doc Automatic, scoped grant
Read internal project data Identity plus resource check
Open a draft PR Automatic or policy-based
Merge to a protected branch Required review and branch policy
Modify production infrastructure Explicit approval or a tightly governed pipeline
Delete critical data Strong confirmation, restricted authority, tested recovery

Approval should add judgment, not substitute for access control. A human clicking "approve" cannot compensate for an agent that can already reach everything.

11. A reference architecture

Human / Scheduler
        |
        v
   Task authority  ---- records who started the task and why
        |
        v
   Agent runtime   ---- own workload identity (e.g. SPIFFE ID)
        |
        | proposes action
        v
    Tool gateway   ---- validate args, derive resource
        |
        v
 Policy decision   ---- allow / deny / require approval
        |
        v
 Credential mint   ---- short-lived, audience- and scope-bound
        |
        v
 Resource service  ---- enforces its OWN access rules
        |
        v
 Audit + monitoring ---- append-only, independent of the agent
        |
        v
 Suspend / revoke / recover

Real deployments will merge some boxes, or use cloud-native services for several. That's fine. What matters is that no path exists from a model-generated tool call to a sensitive resource that skips policy.

12. What to do first

You don't need a full enterprise identity platform for a prototype. You do need the model in place from day one.

Inventory

  • List every agent, its owner, its tools, and its data sources.
  • Find long-lived secrets and shared credentials.

Constrain

  • Give each agent its own identity and replace personal tokens.
  • Separate read-only from write-capable operations.

Enforce at runtime

  • Route all tool calls through one gateway with deny-by-default.
  • Make downstream services enforce their own rules too.

Fix credentials

  • Prefer managed identities or federation.
  • Use short-lived, audience-restricted tokens, and sender-constrain them where possible.

Make it auditable

  • Log identity, task, action, resource, decision, outcome, including denials.

Test the boundaries. Most teams only test the happy path. Test the "no":

it("cannot read a repo outside the grant", async () => {
  await expect(
    callTool({ agentId: "review-agent", taskId: "t1", tool: "pr.read", rawArgs: { repo: "acme/billing" } })
  ).rejects.toThrow("Action not authorized");
});

it("cannot act after the grant expires", async () => {
  vi.setSystemTime(afterExpiry);
  await expect(
    callTool({ agentId: "review-agent", taskId: "t1", tool: "pr.comment", rawArgs: { repo: "acme/payments-api", pr: 42 } })
  ).rejects.toThrow("Action not authorized");
});

Also check: Can a child agent receive more authority than its parent? Does disabling the agent stop new operations? What happens to tokens already issued? Can untrusted document text trigger a privileged call? Do logs separate the agent from the initiating user?

Capability is not authority

A model can write a SQL query, craft a deploy command, or choose the next tool. That capability does not mean the system should allow the action.

Operating systems learned this long ago: a process may be able to open a file, but permissions decide whether it may. Agents make the lesson more visible because the sequence of actions isn't hardcoded. So the architecture has to bound authority even when the plan changes.

Model-level safeguards and infrastructure-level controls are complements. Instructions guide behavior; runtimes can gate sensitive steps. But permissions, credentials, resource boundaries, and authorization checks belong to the systems that control access.

Conclusion

A shared API key will get a prototype working. It will not answer the questions production asks: who is acting, on whose behalf, against what, under what authority, for how long, and how do we stop it?

Answering them takes distinct agent identities, least-privilege grants, careful delegation, short-lived credentials, runtime policy enforcement, tamper-evident audit trails, and a revocation path you have actually tested. Not every system needs every piece on day one, but every team should know what authority its agents hold and what happens if that authority is abused.

My rule is simple:

Give the agent enough access to finish the task, not enough to control everything it can reach.

A trustworthy agent is not one that usually decides well. It is one whose authority stays bounded even when it decides badly.

References

  1. NIST NCCoE: New Concept Paper on Identity and Authority of Software Agents (Feb 2026)
  2. NIST: Back to the Future: Why Agentic AI Needs a Strong Identity Foundation (Aug 27, 2026)
  3. NIST: Design Tenets for Agent Identity Management Systems (Oct 27, 2026)
  4. IETF: RFC 8693, OAuth 2.0 Token Exchange
  5. IETF: RFC 9449, DPoP and RFC 9396, Rich Authorization Requests
  6. SPIFFE: Overview
  7. OpenAI: Workload identity federation with SPIFFE
  8. OWASP: Secure Coding with AI Cheat Sheet
  9. Descope: Summary of Gartner's "IAM Adapts to Secure and Enable AI Agents"

About the Author

RAJΰ€Άΰ₯ΰ€°ΰ₯€

Software Engineer Β· AI/ML Researcher Β· Founder, Shree Labs

I’m Rajshree, a Software Engineer and AI-focused builder interested in the intersection of software engineering, artificial intelligence, machine learning, and modern web technologies.

I write about the engineering behind emerging technologiesβ€”not simply how to use a tool, but how the systems behind it work, how they should be designed, and what it takes to build software that remains reliable, secure, and responsible as technology evolves.

My work and writing explore AI engineering, software architecture, cybersecurity, machine learning, emerging technologies, and the responsibility of building systems that increasingly influence the world around us.

πŸ—οΈ Founder β€” Shree Labs

Shree Labs is a technology and knowledge initiative founded by Rajshree, bringing together engineering, research, education, technology projects, and creative expression.

Its areas of exploration include:

  • Technical articles and engineering insights
  • Software engineering and programming tutorials
  • Technology and AI projects
  • Artificial intelligence and machine learning research
  • Developer resources and learning initiatives
  • Poetry and creative writing

The idea is simple:

Learn. Build. Experiment. Share.

The broader vision is to create a space where technology, engineering, learning, research, and creativity can coexistβ€”and where ideas can move from curiosity to practical work.

Connect & Explore

🌐 Shree Labs: https://rjshree.com/

πŸ’Ό LinkedIn: linkedin.com/in/rjshree

πŸ’» GitHub: github.com/itsrjshree

If you find value in this work, follow along for more perspectives on AI engineering, software architecture, machine learning, cybersecurity, modern web development, and the evolving relationship between humans and intelligent systems.

Because engineering is not just about making things work. It is also about understanding what we build, why we build it, and the responsibility that comes with it.

Thanks for reading.

Β© RAJΰ€Άΰ₯ΰ€°ΰ₯€ | Shree Labs

πŸ“° Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.