Ask a platform team why an engineer reading one log group during an incident holds ReadOnlyAccess across a whole production account, and the answer is rarely carelessness. It is arithmetic. A pre-built role has to serve every request that will ever be made against it, so it is as wide as the widest need anyone has ever had. Least privilege written once, in advance, for a population of people and tasks, is never least privilege for the task in front of you.

This post explains the alternative: write the permission per request, from what the person actually asked for, inside a ceiling the organisation set in advance. It follows two requests from the demo organisation, Northwind Labs, through the least-privilege scoper in InsightKeys, and is frank about where the approach stops.

In short

  • Pre-built roles fail by design. A role shared by many tasks must cover all of them, so every grant carries the permissions of the widest one.
  • Scope each grant from three inputs. The request’s own words, the ceiling the resource owner set for that entitlement, and what similar past grants actually used.
  • The ceiling is a governance decision; the scope is an operational one. Owners decide the most anyone may get. The scoper decides how much less this request needs, and never more.
  • Narrow grants change the risk and the route. In the demo, naming one log group or one table takes 10 points off the risk score, which can be the difference between an automatic approval and a wait.
  • It is not magic. A scope can be too narrow, and the scoper only knows what the catalogue and the history tell it. A new, small request is the remedy, and it is quick.

Why roles drift wide

Most access programmes start with a sensible-looking catalogue: a read role, an operator role and an administrator role per account or database. Each is designed in a workshop, approved by a committee and then left alone. The trouble begins the first time a real task does not fit. An engineer needs to read CloudWatch logs and also start a Logs Insights query; the read role lacks logs:StartQuery, so someone adds it, or adds logs:* to save the next person the trouble. Repeat that for two years and the read role is a collection of every action anyone has needed once.

Nobody reviews the sum, because nobody owns it. The role is shared by dozens of people and hundreds of tasks, so narrowing it would break someone. The safe move for any individual is always to add, never to remove. That is how organisations end up granting *:Describe*, *:Get* and *:List* across an account to someone who needs to read the logs of one service.

Industry
data

Cloud permission studies have repeatedly found that the large majority of permissions granted to identities are never used. Separately, breach reports year after year list stolen or misused credentials among the most common ways attackers get in, which means unused permissions are not harmless: whatever a credential can do, an attacker holding it can do too.

Sources: Microsoft, State of Cloud Permissions Risks report (2023); Verizon Data Breach Investigations Report (annual editions). Exact percentages vary by study and year.

ROLE BY ROLE One role per job family

A read role sized for the widest read task anyone has had. Granted whole, every time, to everyone who asks for “read”.

REQUEST BY REQUEST One permission per request

The same read entitlement as a ceiling, and each grant cut down to the actions and resources this task needs, for this hour.

Three inputs: the words, the ceiling, the history

Per-request scoping only works if it is safe by construction. InsightKeys gets there by separating who decides the most that can be granted from who decides how much this request gets.

The ceiling. Every resource in the catalogue has entitlements, typically Read, Write and Admin, and each entitlement has a ceiling: the widest permission that entitlement can ever become, set in the catalogue and agreed with the resource owner. For payments-db (Postgres) the Write entitlement’s ceiling is DELETE, INSERT, SELECT and UPDATE on every table. The scoper works strictly inside it. It can remove actions and name resources; it cannot add an action the ceiling does not hold.

The words. The request is a sentence: “look at the payments-api logs on prod-payments for INC-4412, 1 hour.” The intake agent has already matched it to a resource in the catalogue (it never invents one) and a level. The scoper reads the same sentence for the things a role cannot know: which service, which table, which namespace, whether the task reads or changes.

The history. Similar past grants show what people doing this kind of task actually used. If everyone who has read payments-api logs during an incident used the same six log actions, a seventh is unlikely to be needed.

The catalogue of resources with Read, Write and Admin entitlements, each showing its ceiling and the resource owner
The catalogue is where ceilings live. Each resource lists its entitlements, the widest permission each may become, and the owner who answers for it.

Two further limits sit outside the control plane altogether. The broker that does the granting runs in the customer’s network and has its own local policy file, broker.yaml, which lists actions it will never grant whatever it is told: by default iam:*, organizations:* and sts:*. It can also refuse admin altogether. So there are three layers: the owner’s ceiling, the scoper’s narrowing, and the broker’s own refusal list, owned by the customer and enforced where the credential lives.

flowchart LR W["Request sentence"] --> S["Least-privilege scoper"] H["Similar past grants"] --> S C["Entitlement ceiling
set by the owner"] --> S S --> P["Scoped permission
actions and named resources"] P --> R["Risk score
narrow scope -10"] R --> B["Broker local policy
never-grant list"] B --> G["Grant in AWS,
PostgreSQL or Kubernetes"]

The ceiling bounds the scoper; the broker’s own policy bounds everything the control plane sends.

Worked example: four log actions instead of read on everything

Priya, an SRE at the demo organisation, is on call for payments when incident INC-4412, “Settlement batch delayed for EU merchants”, opens. Priya types one sentence: “look at the payments-api logs on prod-payments for INC-4412, 1 hour.” The request becomes AR-0001.

The Read entitlement on the prod-payments AWS account has a ceiling of *:Describe*, *:Get*, *:List* and cloudwatch:GetMetricData. Granted whole, that would let Priya describe and read almost anything in an account the catalogue marks as critical and holding payment data and personal data.

The scoper narrowed it to six CloudWatch Logs actions — logs:DescribeLogGroups, logs:DescribeLogStreams, logs:FilterLogEvents, logs:GetLogEvents, logs:GetQueryResults and logs:StartQuery — on one resource pattern: arn:aws:logs:*:411122223333:log-group:/ecs/payments-api*. That is the log groups of one service, in one account, and nothing else.

AR-0001 detail page: a timeline showing the scoper narrowing the permission, automatic approval, the grant becoming active and session watch asking, beside a risk breakdown totalling 30
AR-0001. The timeline records the narrowing as an agent action, and the risk breakdown shows the −10 that a named-resource scope earns.

The narrowing shows up in the risk score too. AR-0001 scored 30, Medium: production +30, critical resource +20, sensitive data +8, one hour or less −3, first time Priya has had this access +5, narrow scope on named resources −10, on call for the resource’s team −10, and an open incident referenced −10. Flow #10, “Read access: automatic when the risk is low”, allows automatic approval at 30 or below. Without the narrowing, the same request would have scored 40 and waited for a person.

That is the operational case for per-request scoping. It is not only safer; it is faster for the engineer, because a narrow grant is a cheap grant to approve.

What happened next shows the security case. During the grant, Priya’s session made the two log calls the request asked for, then a call to secretsmanager:GetSecretValue on prod/payments/stripe-key. AWS denied it, because the scoped policy did not include it. Under the unscoped ceiling, *:Get* would have allowed it. Session watch flagged the call as sensitive and asked security to approve a revoke. Whatever the explanation turns out to be, the payment provider’s key was never read.

Worked example: one table instead of every table

Databases show the same pattern. Sam, an engineer, asks: “write access to fix one invoice status in the payments database for 30 minutes, OPS-219.” That is AR-0004. The Write entitlement on payments-db (Postgres) has a ceiling of DELETE, INSERT, SELECT and UPDATE on *.*. The request mentions invoices and a status fix. The scoper narrowed it to INSERT, SELECT and UPDATE on billing.invoices. DELETE went; every other table went.

AR-0004 as seen by the admin: a risk breakdown totalling 62, step-up verified, the permission INSERT, SELECT and UPDATE on billing.invoices, awaiting the owner's approval
AR-0004. The approver sees the narrowed permission, not the ceiling: INSERT, SELECT and UPDATE on one table, for 30 minutes.

When the broker creates the grant, it creates a PostgreSQL login role for this grant alone, with VALID UNTIL set to the expiry and table-level privileges on billing.invoices. At revoke, the broker terminates that role’s sessions and removes it. The same mechanism produced AR-0008, a read request during INC-4412, which the scoper narrowed from SELECT on *.* to SELECT on billing.invoices.

RequestCeiling (what a pre-built role would grant)Scoped grantEffect
AR-0001 · logs on prod-payments*:Describe*, *:Get*, *:List*, cloudwatch:GetMetricDataSix logs: actions on /ecs/payments-api*Secret read attempt denied by AWS; auto-approved at 30
AR-0004 · fix an invoiceDELETE, INSERT, SELECT, UPDATE on *.*INSERT, SELECT, UPDATE on billing.invoicesNo delete; one table; owner approves
AR-0005 · roll back a deployAdministratorAccess (*)ecs:Describe*, ecs:List*, ecs:StopTask, ecs:UpdateService on service/auth*Admin request still goes to two people, but for a fraction of the power
AR-0007 · crash-looping podsRead on the namespaceget, list, watch on eks-prod/paymentsRead verbs only; auto-approved at 14
AR-0008 · read invoicesSELECT on *.*SELECT on billing.invoicesA real Postgres login for one table, gone at expiry

What changes for the business

The reason to care is not elegance. Per-request scoping changes three things a security leader is accountable for.

Exposure. If a credential is stolen during a grant, the damage is bounded by what that grant allows. A session that can read one service’s logs for an hour is a very different incident from a session that can read every secret and object in a production account. Exposure is the product of breadth and time; JIT shrinks the time, scoping shrinks the breadth.

Approval quality. Approvers asked to approve “Write on payments-db” are asked to trust. Approvers asked to approve “INSERT, SELECT and UPDATE on billing.invoices for 30 minutes, ticket OPS-219” can actually judge. A concrete permission is the precondition for an approval meaning anything.

Evidence. An auditor sampling access changes wants to see that what was granted matched the business need. The record for every request shows the ceiling, the scoped permission, who or what narrowed it, and what the person did with it.

For the CISO

Least privilege stops being a policy statement and becomes a property of every individual grant, visible on each request.

The broker’s never-grant list means a mistake in the control plane cannot hand out IAM or Organizations permissions.

For the platform lead

Owners set ceilings once; nobody maintains a growing zoo of special-case roles.

Engineers get a narrower grant faster, because narrow scope lowers the risk score and more requests clear automatic approval.

For the board

The question “what could someone have done with that access?” has a short, specific answer for every grant.

That answer is what limits the cost of a stolen credential.

Rolling it out

Per-request scoping is only as good as the ceilings and the catalogue it works from. The order of work matters.

Introducing per-request scopingTypical owners; adapt to your organisation
1
Run discovery and build the cataloguePlatform

Connect the broker and discover AWS accounts and permission sets, PostgreSQL roles and Kubernetes bindings. Use what it finds to build the catalogue: each resource, its entitlements and a named owner.

2
Set ceilings per entitlementResource owners

Write down the widest Read, Write and Admin each resource should ever allow. Existing role definitions are a reasonable first draft; trim them.

3
Set the broker’s local policySecurity

Review the never-grant list in broker.yaml, the longest grant it will create, and whether admin may be granted at all.

4
Start the scoper at a low autonomy levelSecurity

The scoper defaults to L3, act and tell. Organisations that want to watch first can set it to L2, ask first, and review its narrowing before it applies.

5
Watch for too-narrow grantsPlatform

Look at session events for denied calls that were in fact needed, and at follow-up requests raised minutes after a grant. Adjust ceilings or catalogue wording.

6
Retire the wide rolesPlatform + owners

Once requests flow through the catalogue, convert standing access to the old roles into on-request access.

Where it stops

What per-request scoping does not do

The scoper narrows from the request’s words and from history; it does not read the application’s code, so it can cut a permission the task turns out to need. When that happens the person sees an access-denied error and asks again with a narrower or different request — quick, but not free. Granularity follows the integration: action and resource level for AWS, table level for PostgreSQL (not row or column level), namespace and verbs for Kubernetes. A ceiling the owner set too wide is still too wide; the scoper narrows within it, it does not question it. Drivers for Google Cloud, Azure, MySQL, MongoDB and Snowflake are on the roadmap, not available today.

There is also a point of judgement. Some teams will prefer a slightly wider grant that never fails over a narrow one that occasionally does, particularly in break-glass situations. Flow #5 in the demo organisation, break-glass for whoever is on call, exists for exactly that: it trades breadth for speed under step-up, tells security, caps the grant at one hour and puts it in front of a reviewer afterwards. Per-request scoping is the default; it does not have to be the only path.

A role is a guess about every future task. A scoped grant is an answer to this one.

Let owners set the most anyone may have, let each request take only what its own words and history justify, and let the broker refuse what nobody should be granted at all.

Keep reading