Back to Blog
Abstract security imagery representing least-privilege access control for AI workflow service accounts

AI Workflow Service Account Permissions: A Least-Privilege Design

10 min read

One shared service account that can read the CRM, update tickets, send messages, and edit documents works in a demo. In production, every workflow run gets the same authority, including runs started by employees who could not perform the action themselves. The audit log then says which service ran the call, when you need to know who requested it, which workflow acted, and what policy allowed.

This guide covers how to pick an identity model per trigger, how to issue a short-lived credential for one tool, where the common advice on token chains stops being safe, and which denial tests to run before launch.

Why one shared service account fails

A model never needs a credential. The deterministic code that runs a model-selected tool does. When that code holds one broad credential, a bad tool choice, a prompt injection, or an ordinary bug can reach everything the account can reach.

Reuse across jobs compounds this. A nightly invoice importer needs to create drafts. A support assistant needs to read customer history and add a private note. A refund flow needs to change a payment record. Put all three under one account and each workflow inherits the other two workflows' powers.

The audit trail also blurs. The target system sees the shared account as the caller. Your workflow database may record the requesting employee, but an investigator has to join two logs and trust that the application did not edit one of them. Carry two identities in every authorization decision: the subject who asked, and the workload that acted.

The OWASP agentic threat guide recommends a threat-model-based approach for autonomous systems. Draw the trust boundaries around the trigger, orchestrator, model, tool gateway, credential issuer, and target system, then mark every point where untrusted text can steer an action.

Choose delegated or workload identity first

Use delegated identity when a signed-in person starts the workflow and the action should never exceed what that person may do. Searching customer records, drafting a reply in the user's queue, and editing a document the user can already edit fit here.

Use workload identity when the job has its own approved authority. Scheduled reconciliation, inbound document processing, and queue cleanup usually fit. Give each job or tightly related family of jobs a dedicated identity.

Use an explicit approval transition when a delegated run needs a stronger action. The approval creates a new authorization decision, separate from any flag inside the original run. Record the approver, action, resource, expiry, and policy version, and issue a new credential only after that decision passes.

Prefer delegation to impersonation. OAuth 2.0 Token Exchange (RFC 8693) separates the two: with impersonation the actor becomes the subject, with delegation the actor acts on the subject's behalf and both identities stay visible. Impersonating a user to make downstream logs look familiar throws away the second identity.

Trigger and action Identity model Permission ceiling
Employee asks to summarize a customer account Delegated Requesting employee's read access
Employee asks to update an assigned ticket Delegated Employee access plus an update-ticket scope
Nightly importer creates draft invoices Workload Dedicated importer identity, draft creation only
Workflow submits a refund after manager approval Approval transition Exact refund action and approved amount
Model proposes deleting records Neither by default Separate human-controlled administrative path

If a team cannot pick a row for a proposed tool, the tool is not ready for production.

When delegation alone is too wide

Delegation caps the agent at the user's rights. That ceiling is often far higher than the task needs. An administrator who asks an assistant to tidy a ticket queue hands the assistant administrator reach. Red Hat's write-up of delegation versus impersonation states the fix as an intersection: effective permissions are what the user may access combined with what the agent is configured to access, so agents "can only reduce your access, never expand it."

Build that intersection into the credential broker. Each tool declares its own maximum scopes, and the broker issues the overlap of those scopes and the subject's current rights. Delegation for who is asking, a per-tool ceiling for what the workflow may do.

An audit chain is not an authorization chain

When an orchestrator calls a sub-agent that calls a tool, token exchange can nest the actors. The nesting is good evidence for an investigation and grants nothing. RFC 8693 section 4.1 says a token's consumer "MUST only consider the token's top-level claims and the party identified as the current actor," and that prior actors in nested act claims are "informational only."

The practical consequence: the tool gateway must authorize on the subject plus the current actor, never on "an upstream agent in the chain was trusted." Run a fresh exchange and a fresh policy check at every hop. A read-only research agent that asks a more privileged agent to make a change gets nothing from the chain. The privileged agent's own ceiling for that tool decides.

Define the authorization envelope

Do not pass the user's web session token through every step. Exchange it at a trusted backend for a narrower credential. RFC 8693 defines request fields for the subject token, audience, resource, and scope, plus an optional actor token for delegation. Those fields map directly onto an internal workflow authorization record:

{
  "run_id": "wf_7f3c",
  "subject": "employee:1842",
  "actor": "workflow:support-assistant",
  "tool": "ticketing.update_private_note",
  "resource": "ticket:93815",
  "scopes": ["ticket.note:create"],
  "policy_version": "support-tools-v4",
  "approval_id": null,
  "expires_at": "2026-07-29T10:05:00Z"
}

This is an illustrative application record, not a claim that every identity provider emits these fields. The parts that matter are a separate subject and actor, one intended tool, a resource constraint, narrow scopes, policy provenance, and expiry.

Bind the credential to an audience that only the relevant tool gateway accepts. A token issued for ticket updates should fail at the document service. Where the authorization system supports resource-level grants, bind to one resource. Otherwise the gateway checks the resource before it calls the target API.

Keep raw provider credentials out of model context and workflow state. The model emits a typed tool request. Trusted code validates it, obtains the credential, calls the target, and records the result. Never put a refresh token, service-account key, or bearer token in a prompt so the model can assemble its own HTTP request.

Implement the permission layer

Start with the tool catalog, not the model. List every callable operation with its input schema, side effects, identity model, permission, allowed resource pattern, and approval rule. Split broad operations: crm.execute cannot be authorized safely, while crm.contact.read and crm.contact.add_note can carry different policies.

Next, create dedicated workload identities. The Google Cloud service-account guidance recommends dedicated service accounts per application, no automatic grants on default accounts, and temporary credentials where possible. The same rules hold outside Google Cloud.

Then put a credential broker between orchestration and tools. The broker takes authenticated workflow context, evaluates policy, and returns a short-lived credential or a denial. The orchestrator cannot mint arbitrary scopes. It asks for a named tool action, and the broker maps that action to the maximum audience and scopes.

def authorize_tool_call(run, request):
    tool = catalog.require(request.tool_name)
    policy = policies.for_tool(tool.name)

    require(run.actor == policy.allowed_actor)
    require(request.arguments.matches(tool.input_schema))
    require(policy.subject_can_act(run.subject, request.arguments))
    require(policy.resource_allowed(run.subject, request.arguments))

    if policy.requires_approval(request.arguments):
        require(approvals.matches(run.approval_id, request))

    return credential_broker.issue(
        actor=run.actor,
        subject=run.subject,
        audience=tool.audience,
        scopes=tool.scopes,
        resource=tool.resource_from(request.arguments),
        ttl=policy.credential_ttl,
    )

The gateway repeats the important checks. It does not trust the orchestrator's claim that authorization already passed. It verifies the credential's audience, expiry, actor, scope, and resource, and validates arguments again after the model output is parsed. That contains orchestration bugs and stops a request from changing between policy evaluation and execution.

Log the authorization decision separately from the API result: subject, actor, tool, resource, granted scopes, policy version, approval reference, decision, and reason code. Leave the token out. Denied calls belong in the trail because they show whether policy is catching unexpected behavior.

A public case: one token, two repositories

In May 2025 Invariant Labs showed an attack on the official GitHub MCP server used from Claude Desktop. The victim had a public repository and a private one. An attacker opened an issue in the public repository carrying an injected instruction. When the user asked the agent to look at open issues, the agent read it, pulled data from the private repository and committed it to a new pull request in the public one, where the attacker could read it. The researchers stressed that the GitHub server code was not flawed. The agent simply held one user's credential that reached both repositories, with no boundary between them.

The mitigation they recommend is a scoping rule, not a better prompt: a policy that limits each agent session to one repository, so a session that has read untrusted public text cannot also touch private data. Apply the same shape to your own service accounts. Give the credential for a workflow the narrowest set of resources that one task needs, issue a new one per task or session, and put a check outside the model that refuses a session to cross from an untrusted source to a private one. The same report notes that users on an "Always Allow" confirmation policy were the most exposed, so approvals do not replace scoping.

Work through a support-ticket example

An employee asks an assistant to summarize ticket 93815 and add a private note. The workflow has two tools, ticket.read and ticket.note.create, and the employee can see the ticket because it sits in their queue.

For the read step, the broker exchanges the employee's session identity for a credential whose audience is the ticket gateway and whose scope allows reads. The gateway confirms ticket 93815 is visible to that subject. The ticket text goes to the model. The access token does not.

The model drafts a note and proposes ticket.note.create, which is a separate authorization request. The gateway verifies the subject still has access, validates the note schema and size, and obtains a credential for note creation only. The read credential cannot create the note, and the note credential cannot read another ticket.

Now suppose the ticket body contains hostile text asking the assistant to export all customer records. The catalog rejects unknown tools before any credential is issued. If the model names a known export tool, policy denies it because the support assistant actor may not request it. The denial is evidence worth keeping, and it never produces a token.

Handle failures without widening access

Credential expiry is routine. Return to the broker with the original run identity and authorization context. Do not fall back to a long-lived key when temporary issuance fails, because that turns an availability problem into a security bypass.

A target API may reject a token for the wrong audience, scope, or resource. Classify that as an authorization error. Retrying will not fix it and will flood the audit log. Stop the action, keep the denial reason, and route the run to an operator if it cannot continue safely.

If a subject loses access during a long job, re-evaluate before each consequential call. For workload jobs, check role grants at credential issuance and keep lifetimes short enough that revocation takes effect quickly.

If the broker is down, fail closed for writes. A documented read-only degradation path is acceptable when the data classification allows it, but define and test it before launch.

Verify both permissions and denials

A happy-path test shows the workflow can act. It says nothing about whether the boundary holds. Build a matrix of allowed and denied combinations across subject, actor, tool, resource, scope, approval, and time. At minimum:

  • An authorized employee can read one permitted record.
  • The same employee cannot read a record outside their assigned boundary.
  • A read credential cannot perform a write.
  • A credential for one tool is rejected by another tool's audience check.
  • An expired credential is rejected without falling back to a static key.
  • A scheduled workload cannot use delegated user-only tools.
  • A missing, expired, or mismatched approval blocks the stronger action.
  • A model-proposed unknown tool never reaches the credential broker.
  • A sub-agent holding a read-only credential cannot get a write by calling a more privileged agent.
  • Revoking a subject or workload role blocks later credential issuance.
  • Logs show subject, actor, resource, decision, and policy version without exposing the token.

Run the denial suite in continuous integration against every policy change. In staging, match target-system audit events to workflow decision records: both should name the same actor, action, resource, and time window. Investigate mismatches before rollout.

In production, watch denials by reason code. A rise in unknown-tool or out-of-scope requests points to prompt manipulation, a model regression, catalog drift, or a user need that deserves a deliberate design. Do not grant the missing scope to silence the alert.

Put one workflow through the design this week

Pick the workflow with the broadest credential today. Inventory its tool calls, choose delegated or workload identity for each, and replace one shared credential with a short-lived, audience-bound credential for one operation. Add a test that the credential fails against a second tool and an unauthorized resource.

Hold the rollout until the audit record answers four questions for every call: who requested it, which workflow acted, which exact resource it touched, and why policy allowed or denied it. Then repeat the pattern for the next workflow.

References


About Fire In Belly: Independent senior engineering from Tallinn, Estonia. We design and build internal AI tools and workflows with the delegated identity, short-lived credentials, and least-privilege controls described above, at published fixed prices. Schedule a call to discuss your next project.