Least-Privilege Permissions
AI coding tools default to "make it work" — and the fastest way to make it work is to grant full access everywhere. Admin IAM roles, root database users, wildcard API scopes, containers running as root. The code runs, the demo works, and you've just deployed an attack surface the size of a parking lot.
## Key Points
- name: app
- name: app
- name: tmp
1. Deploy with zero permissions
2. Watch it fail
3. Add exactly the permission it needs
4. Repeat until it works
- **AWS**: [iamlive](https://github.com/iann0036/iamlive) — captures API calls and generates least-privilege policies
- **GCP**: IAM Recommender — suggests role reductions based on actual usage
- **Azure**: Azure AD Access Reviews — identifies unused permissions
## Quick Example
```json
{
"roleName": "Contributor",
"scope": "/subscriptions/{subscription-id}"
}
```
```json
{
"roleName": "Storage Blob Data Reader",
"scope": "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Storage/storageAccounts/{account}/blobServices/default/containers/{container}"
}
```skilldb get vibe-coding-security-skills/least-privilege-permissionsFull skill: 323 linesLeast-Privilege Permissions
AI coding tools default to "make it work" — and the fastest way to make it work is to grant full access everywhere. Admin IAM roles, root database users, wildcard API scopes, containers running as root. The code runs, the demo works, and you've just deployed an attack surface the size of a parking lot.
This skill teaches you to identify and replace overprivileged patterns across every layer of your stack.
The "Admin Everything" Anti-Pattern
When you ask an AI to set up cloud infrastructure, connect a database, or configure an API client, it consistently generates the most permissive configuration possible. Not out of malice — out of optimization for "works on first try."
The Pattern in IAM
What AI generates (AWS):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "*",
"Resource": "*"
}
]
}
What AI generates (GCP):
# "Just use the Owner role, it'll work"
resource "google_project_iam_member" "service_account" {
project = var.project_id
role = "roles/owner"
member = "serviceAccount:${google_service_account.app.email}"
}
What AI generates (Azure):
{
"roleName": "Contributor",
"scope": "/subscriptions/{subscription-id}"
}
Every one of these is a disaster. A compromised service with *:* permissions can delete your entire AWS account. An Owner-role service account in GCP can modify IAM itself. A subscription-level Contributor in Azure can spin up crypto miners.
Layer-by-Layer Least Privilege
1. Cloud IAM Roles
AWS — Correct minimal policy for an app that reads from S3 and writes to DynamoDB:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadFromInputBucket",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::my-app-input-bucket",
"arn:aws:s3:::my-app-input-bucket/*"
]
},
{
"Sid": "WriteToDynamoDB",
"Effect": "Allow",
"Action": [
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:GetItem",
"dynamodb:Query"
],
"Resource": "arn:aws:dynamodb:us-east-1:123456789:table/my-app-table"
}
]
}
GCP — Correct minimal roles for a Cloud Run service:
resource "google_project_iam_member" "firestore_user" {
project = var.project_id
role = "roles/datastore.user"
member = "serviceAccount:${google_service_account.app.email}"
}
resource "google_project_iam_member" "storage_reader" {
project = var.project_id
role = "roles/storage.objectViewer"
member = "serviceAccount:${google_service_account.app.email}"
}
# If you need to write to a specific bucket, use IAM conditions
resource "google_storage_bucket_iam_member" "upload_writer" {
bucket = google_storage_bucket.uploads.name
role = "roles/storage.objectCreator"
member = "serviceAccount:${google_service_account.app.email}"
}
Azure — Correct scoped role assignment:
{
"roleName": "Storage Blob Data Reader",
"scope": "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Storage/storageAccounts/{account}/blobServices/default/containers/{container}"
}
2. Database Users
What AI generates:
-- "Just connect as root, it works"
CREATE USER 'app'@'%' IDENTIFIED BY 'password123';
GRANT ALL PRIVILEGES ON *.* TO 'app'@'%';
What production needs:
-- Application user: can only read/write to app tables
CREATE USER 'app_service'@'10.0.%.%' IDENTIFIED BY '<generated-password>';
GRANT SELECT, INSERT, UPDATE ON myapp.users TO 'app_service'@'10.0.%.%';
GRANT SELECT, INSERT, UPDATE, DELETE ON myapp.orders TO 'app_service'@'10.0.%.%';
GRANT SELECT ON myapp.products TO 'app_service'@'10.0.%.%';
-- No GRANT OPTION, no CREATE/DROP, no access to other databases
-- Migration user: separate credentials, only used during deploys
CREATE USER 'app_migrator'@'10.0.%.%' IDENTIFIED BY '<different-password>';
GRANT ALL PRIVILEGES ON myapp.* TO 'app_migrator'@'10.0.%.%';
-- Still scoped to one database, not *.*
PostgreSQL with row-level security:
-- Create a role for the application
CREATE ROLE app_service LOGIN PASSWORD '<generated>';
GRANT USAGE ON SCHEMA public TO app_service;
GRANT SELECT, INSERT, UPDATE ON public.orders TO app_service;
-- Enable row-level security for multi-tenant isolation
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON orders
USING (tenant_id = current_setting('app.tenant_id')::uuid);
3. API Scopes and OAuth
What AI generates:
const oauth2Client = new google.auth.OAuth2(clientId, clientSecret, redirectUri);
const scopes = [
'https://www.googleapis.com/auth/drive', // Full Drive access
'https://www.googleapis.com/auth/gmail.modify', // Read and write Gmail
'https://www.googleapis.com/auth/calendar', // Full calendar access
];
What you actually need (if you only read calendar events):
const scopes = [
'https://www.googleapis.com/auth/calendar.events.readonly',
];
Request the minimum scope. Users see what you're asking for. Broad scopes erode trust and create liability.
4. File System Access
What AI generates in Docker:
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
# Running as root with full filesystem access
CMD ["node", "server.js"]
What production needs:
FROM node:20-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
FROM node:20-slim
RUN groupadd -r appuser && useradd -r -g appuser -d /app -s /sbin/nologin appuser
WORKDIR /app
COPY --from=build --chown=appuser:appuser /app .
# Read-only filesystem where possible
RUN chmod -R 555 /app
# Writable only where needed
RUN mkdir -p /app/tmp && chown appuser:appuser /app/tmp
USER appuser
CMD ["node", "server.js"]
5. Container and Kubernetes Permissions
What AI generates:
apiVersion: v1
kind: Pod
spec:
containers:
- name: app
image: myapp:latest
# No security context at all — runs as root by default
What production needs:
apiVersion: v1
kind: Pod
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
containers:
- name: app
image: myapp:latest
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
The Permission Audit Process
Run this audit on every AI-generated codebase before deploying:
Step 1: Find All Credentials and Roles
# Search for IAM, role, and permission patterns
grep -rn "iam\|role\|permission\|grant\|policy\|admin\|root\|superuser" \
--include="*.tf" --include="*.yaml" --include="*.yml" \
--include="*.json" --include="*.sql" --include="*.ts" \
--include="*.js" --include="*.py"
Step 2: Check for Wildcards
# Find wildcard permissions
grep -rn '"*"\|Action.*\*\|Resource.*\*\|scope.*subscription' \
--include="*.tf" --include="*.json" --include="*.yaml"
Step 3: Verify Database Connections
# Find database connection strings — are they using root?
grep -rn "root\|admin\|postgres://" \
--include="*.env*" --include="*.ts" --include="*.js" \
--include="*.py" --include="*.yaml"
Step 4: Review OAuth Scopes
# Find OAuth scope definitions
grep -rn "scope\|googleapis.com/auth\|microsoft.graph" \
--include="*.ts" --include="*.js" --include="*.py"
Common AI-Generated Permission Mistakes
| What AI Does | The Risk | The Fix |
|---|---|---|
roles/owner on a service account | Full project control | Use specific roles like roles/datastore.user |
GRANT ALL ON *.* | DB admin access from app | Grant per-table, per-operation |
--privileged Docker flag | Container escape to host | Drop all capabilities, add only what's needed |
| API keys with no scope restrictions | Key compromise = full access | Restrict by IP, referer, and API |
chmod 777 | World-writable files | chmod 644 for files, 755 for dirs |
| Root user in containers | Breakout gives host root | USER nonroot in Dockerfile |
Principle: Start With Zero, Add What Breaks
The fastest way to get correct permissions:
- Deploy with zero permissions
- Watch it fail
- Add exactly the permission it needs
- Repeat until it works
This is slower than granting admin access. It is also the only approach that produces secure systems. Every permission you grant is an attack surface. Treat each one as a cost, not a convenience.
Automation: Policy Generators
Use tools that generate minimal policies from observed behavior:
- AWS: iamlive — captures API calls and generates least-privilege policies
- GCP: IAM Recommender — suggests role reductions based on actual usage
- Azure: Azure AD Access Reviews — identifies unused permissions
# iamlive: generate policy from actual usage
iamlive --mode proxy --output-file policy.json
# Run your app through the proxy, then use the generated policy
These tools turn the "start with zero" approach from painful to practical. Run your app, capture what it actually does, generate the policy from reality instead of guesswork.
Install this skill directly: skilldb add vibe-coding-security-skills