Skip to main content
Technology & EngineeringVibe Coding Security323 lines

Least-Privilege Permissions

Quick Summary32 lines
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 lines
Paste into your CLAUDE.md or agent config

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.

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 DoesThe RiskThe Fix
roles/owner on a service accountFull project controlUse specific roles like roles/datastore.user
GRANT ALL ON *.*DB admin access from appGrant per-table, per-operation
--privileged Docker flagContainer escape to hostDrop all capabilities, add only what's needed
API keys with no scope restrictionsKey compromise = full accessRestrict by IP, referer, and API
chmod 777World-writable fileschmod 644 for files, 755 for dirs
Root user in containersBreakout gives host rootUSER nonroot in Dockerfile

Principle: Start With Zero, Add What Breaks

The fastest way to get correct permissions:

  1. Deploy with zero permissions
  2. Watch it fail
  3. Add exactly the permission it needs
  4. 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

Get CLI access →