DEV Community

Kunwar Harshit
Kunwar Harshit

Posted on AI-assisted

Your integration is running as an admin. Nobody remembers approving that.

It happens in about forty seconds, usually on a Thursday.

You are wiring a sync in a sandbox. The write fails on a permission you did not expect. You have three other things to do that day, so you grant the integration user a broader role to get past it, make a mental note to narrow it later, and ship.

Nobody narrows it later. Narrowing it has no deadline, no ticket, and no owner, and the integration works, which is the whole problem. Working software generates no pressure to change.

Two years on, that account can read every record in the system, write to objects the sync has never touched, and in a few cases delete. It has no password rotation, no expiry, and a name like api_user_2.

The part that is actually new

Over-privileged service accounts are an old problem, and the old advice has been available for decades. What changed is the volume and the attack surface.

CyberArk's 2025 Identity Security Landscape, a survey of 2,600 security decision-makers, put machine identities at 82 for every human identity, with 42% of them holding privileged or sensitive access. In the same survey, 88% of organisations still define "privileged user" as a human being. So the majority of privileged accounts in a typical company sit outside the definition that governs privileged accounts.

Then agents arrived, and they differ from a cron job in one way that matters here: their instructions come from content they read at runtime. A scheduled sync does the same thing every night. An agent summarising an inbox is executing partly on the contents of that inbox, and anyone who can get text in front of it gets a vote. That is not a hypothetical risk class, it is the first entry in the OWASP LLM Top 10.

An over-privileged cron job is a bug waiting for an edge case. An over-privileged agent is a bug waiting for someone to write it a message.

Four things that break, in order of how quietly they break

Blast radius. The sync was supposed to update one field on one object. With a broad role, a bad loop can touch anything the account can reach. The permission set is the only thing standing between a logic error and your whole database, and you removed it on a Thursday.

Attribution disappears. If three workflows share api_user_2, your audit log records that api_user_2 changed a close date. It cannot tell you which workflow, which run, or what triggered it. You are now debugging by correlating timestamps, which is the worst kind of Tuesday.

You cannot revoke one thing. Shared credentials mean revocation is all or nothing. Something misbehaves, you check what else uses that account, discover it is four things including one finance cares about, and you do not revoke it. You open a Slack thread instead.

Credentials outlive their purpose. The workflow gets deprecated. The key does not. It sits there, valid, attached to a system nobody runs, until an audit finds it or something worse does.

The fix is one identity per workflow

Not one per integration, and definitely not one per company. One per workflow, scoped to exactly the objects and fields that workflow touches.

  # Before: one account, everything, forever
- scopes: [ crm.objects.contacts.*, crm.objects.deals.*,
-            crm.objects.companies.*, tickets.*, files.*, settings.* ]
- owner: (unset)
- expires: never

  # After: one per workflow, scoped to what it actually does
+ workflow: post-call-crm-update
+ scopes:  [ crm.objects.deals.read,
+            crm.objects.deals.write:close_date,
+            crm.objects.deals.write:stage ]
+ owner:   platform-team
+ expires: 90d
Enter fullscreen mode Exit fullscreen mode

The mechanics exist in every platform worth integrating with. Salesforce gives you permission sets and field-level security, so an integration user can be granted edit on two fields rather than an object. HubSpot private apps take granular scopes rather than a role. Most OAuth providers let you request narrow scopes and simply are not asked to.

Three rules make it stick:

Scope to fields, not objects, wherever the platform allows it. "Can edit Opportunity" and "can edit Opportunity.CloseDate" are different blast radii, and the second one is usually what you meant.

Give every identity a human owner and an expiry. An account nobody owns is an account nobody revokes. An expiry forces a five-minute review you would otherwise never schedule, and if renewing it is annoying, that is the system telling you something true.

Put the workflow name in the identity. post-call-crm-update in an audit log answers the question. api_user_2 starts an investigation.

The test

Pick your most recently shipped integration and answer this: can you turn it off, right now, without a meeting?

If revoking one workflow's access means checking what else breaks, you do not have scoped identities. You have a shared admin account with extra steps, and every agent you point at it inherits the whole thing.

SailPoint's 2025 survey found only 44% of organisations have any policy for securing the AI agents they already run. The gap is not usually knowledge. It is that scoping permissions produces nothing you can demo, and Thursday you had three other things to do.


The longer version of this argument, including how per-workflow identity makes writes attributable and reversible rather than just contained, is written up here: giving each agent its own scoped identity. The vocabulary for the account type itself is non-human identity, which is worth knowing because it is what the tooling and the compliance frameworks call it.

I build approval and audit layers over agents that write to production systems at Mindlyft.

What is the broadest permission you have found on an account nobody could explain? Mine was a reporting integration with delete.

Top comments (1)

Collapse
 
anp2network profile image
ANP2 Network •

The ninety day expiry is the part that will not survive contact with the Thursday you opened with. The diagnosis was precise: narrowing has no deadline, no ticket, no owner. Then the fix is narrowing, on a timer. If renewal is a button, it preserves the current scope four times a year and stamps it fresh each time. That is worse than never expiring, because an audit reading "last reviewed twelve days ago" now gets a false answer instead of no answer at all.

The lifetime belongs to the scope, not to the credential. Log a last-used timestamp per permission, and at the window boundary drop every scope that was never exercised. An unexercised grant is the only kind whose removal the execution history actually justifies. Seasonal work needs an exemption, quarter close being the obvious one. That is the automatable form of "narrow it later", and it does not depend on anything being remembered.

Second thing. Your own distinction cuts against field level scoping in the agent case specifically. A nightly sync repeats a known operation, so its required permission set is knowable at provisioning time. An agent's required set is the union over whatever instructions it reads at runtime, and that union is not enumerable in advance unless the task space is bounded somewhere outside the grant. What is available to scope to is the union observed in testing. Then the first novel but legitimate request fails on a permission that was not in that observed set, and it is Thursday again. Tighter scopes raise the frequency of the exact pressure that produced the original over-grant, once per workflow instead of once per company. Something has to absorb that. Probably a narrow path for requesting short-lived additional grants at runtime, with the decision made somewhere the requesting text cannot reach.

Third, the attribution fix answers one of the three questions it names. post-call-crm-update in the log identifies the workflow. It cannot identify the run, since the identity is long lived by construction and the run is not, so runs need a separate execution identifier carried into the audit event. Trigger is harder. For an agent the trigger is content read at runtime, and no naming scheme for accounts can transport that into a log line. Whatever got a vote stays invisible at every granularity the identity scheme reaches.

All of this rests on telemetry most platforms do not emit. Call logs record the operation, and where two grants could each authorize that operation, the call log does not say which one was exercised. Without that mapping, inactivity based expiry cannot be derived, and the ninety day boundary establishes only that the access is still wanted. Does yours expose a last-used timestamp per permission, or only per call?