---
title: Access requests and least privilege
slug: access-requests-and-least-privilege
summary: How to turn "please give me access" into a time-bound, least-privilege grant through code, with an approver, an expiry and evidence for audit.
order: 13
updated: 2026-11-11
status: ready
---

Use this page for any request for cloud, database, bucket, repo or tool access, and for periodic access reviews. Access requests are high volume and low complexity, which makes them a good candidate for automation, as long as a human approves every grant.

## Every request must answer five questions

| Question | Example answer |
| --- | --- |
| Who? | A named person or a service role, never a shared login |
| What resource? | `<bucket>` read, `<database>` read-only schema `reports` |
| What level? | read, write, admin (admin needs a strong reason) |
| Why? | Ticket and business reason |
| Until when? | 30 days, end of project, or permanent with quarterly review |

If any answer is missing, ask once in the ticket and wait.

## Prefer groups and roles over direct grants

- [ ] Add the person to an existing group or permission set that fits. Do not create a new policy per person.
- [ ] For AWS, grant through IAM Identity Center permission sets assigned to groups.
- [ ] For databases, grant roles (for example `reports_read`) and add users to roles.
- [ ] Production write and admin access is just-in-time where possible, with automatic expiry.

## Write the smallest policy that works

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ReadReportsBucket",
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:ListBucket"],
      "Resource": [
        "arn:aws:s3:::<bucket>",
        "arn:aws:s3:::<bucket>/reports/*"
      ]
    }
  ]
}
```

- [ ] No `"Action": "*"` and no `"Resource": "*"` for write actions.
- [ ] Use conditions where they help: `aws:PrincipalTag`, `aws:SourceVpc`, `aws:RequestedRegion`.
- [ ] Check with IAM Access Analyzer policy validation before the MR.

```bash
aws --profile <readonly-profile> accessanalyzer validate-policy \
  --policy-type IDENTITY_POLICY --policy-document file://policy.json \
  --query 'findings[].{type:findingType,issue:issueCode,msg:findingDetails}'
```

## Database access pattern

```sql
-- Run by an admin through the approved change process, not by the agent
CREATE ROLE reports_read NOLOGIN;
GRANT CONNECT ON DATABASE <database> TO reports_read;
GRANT USAGE ON SCHEMA reports TO reports_read;
GRANT SELECT ON ALL TABLES IN SCHEMA reports TO reports_read;
ALTER DEFAULT PRIVILEGES IN SCHEMA reports GRANT SELECT ON TABLES TO reports_read;
GRANT reports_read TO <user>;
```

Prefer IAM database authentication or short-lived credentials from a secret manager over static passwords.

## The flow

- [ ] Ticket has the five answers and an approver (the resource owner, not the requester's friend).
- [ ] Change is an MR to the access-as-code repo (Terraform for IAM, Identity Center assignments, groups).
- [ ] Plan shows only the expected grant.
- [ ] Approver approves the MR or the ticket; the link goes in the MR.
- [ ] Apply, then ask the requester to confirm it works.
- [ ] Record the expiry and set a reminder or automated revoke.

## Access reviews

- [ ] Quarterly: list who has admin or production write, per account and system.
- [ ] Flag unused access (IAM last-accessed data, database login history).
- [ ] Revoke via MR, with the owner's sign-off.

```bash
aws --profile <readonly-profile> iam generate-service-last-accessed-details --arn <role-arn>
aws --profile <readonly-profile> iam get-service-last-accessed-details --job-id <job-id>
```

## Pitfalls

- Granting `AdministratorAccess` "temporarily". Temporary grants without an expiry are permanent.
- Copying another user's permissions wholesale. Ask what they actually need.
- Long-lived IAM user access keys for people. Use SSO. If keys exist, follow [secret rotation](./secret-rotation-and-exposed-secrets.md) to retire them.
