← Back to Blog

IAM Least-Privilege: What I Learned the Hard Way

Why setting up a scoped AWS IAM deploy user instead of using root credentials matters — and a step-by-step guide to doing it right for S3 + CloudFront CI/CD pipelines.

What Is IAM Least-Privilege?

The principle of least privilege means giving any user, service, or system only the minimum permissions required to do its job — nothing more. In AWS, this is enforced through IAM (Identity and Access Management) policies that explicitly allow or deny specific actions on specific resources.

It sounds simple. In practice, most people skip it — especially when getting started. I was one of them.

The Mistake I Almost Made

When I first set up my CI/CD pipeline to deploy this portfolio to AWS S3 + CloudFront, the path of least resistance was to use my personal IAM user's access keys — the one with AdministratorAccess. It would have worked instantly.

Using admin credentials in a CI/CD pipeline means that if those credentials are ever exposed — in a log, a public repo, or a breach — an attacker has full control of your entire AWS account. Every service. Every region. Every resource.

I caught myself before doing it. Here's what I did instead.

Step 1 — Create a Dedicated Deploy User

The first step is creating a separate IAM user whose only purpose is to run deployments. This user should have no AWS Console access — only programmatic access (access key + secret key).

  1. Go to IAM → Users → Create user
  2. Name it something descriptive: portfolio-deploy
  3. Select Programmatic access only — no Console login
  4. Skip adding any policies for now — we'll do that next
  5. Generate and save the access key ID and secret key

Step 2 — Write a Scoped IAM Policy

Instead of attaching a managed policy like AmazonS3FullAccess, write a custom policy that grants only what the deploy pipeline actually needs:

{ "Version": "2012-10-17", "Statement": [ { // Allow syncing files to the S3 bucket "Sid": "S3DeployAccess", "Effect": "Allow", "Action": [ "s3:PutObject", "s3:GetObject", "s3:DeleteObject", "s3:ListBucket" ], "Resource": [ "arn:aws:s3:::your-bucket-name", "arn:aws:s3:::your-bucket-name/*" ] }, { // Allow CloudFront cache invalidation only "Sid": "CloudFrontInvalidation", "Effect": "Allow", "Action": "cloudfront:CreateInvalidation", "Resource": "arn:aws:cloudfront::YOUR_ACCOUNT_ID:distribution/YOUR_DIST_ID" } ] }

Attach this policy directly to the portfolio-deploy user. That's it. Nothing else.

What This Policy Allows vs. What It Blocks

Action Allowed? Why
Upload files to S3 bucket✅ YesNeeded for deployment
Delete stale files from S3✅ YesNeeded for --delete flag
Invalidate CloudFront cache✅ YesNeeded post-deploy
Access other S3 buckets❌ NoNot in Resource ARN
Delete the S3 bucket itself❌ NoNot in Action list
Access EC2, RDS, Lambda, etc.❌ NoNot in policy at all
Create or delete IAM users❌ NoNot in policy at all
Access AWS Console❌ NoProgrammatic access only

Step 3 — Store Credentials in GitHub Secrets

Never put AWS credentials in your code, workflow files, or environment variables that get committed. GitHub Secrets are the right place — they're encrypted at rest and are masked in all logs.

Go to your repo → Settings → Secrets and variables → Actions → New repository secret:

AWS_ACCESS_KEY_ID → access key for portfolio-deploy user AWS_SECRET_ACCESS_KEY → secret key for portfolio-deploy user AWS_REGION → e.g. us-east-1 S3_BUCKET → your-bucket-name CLOUDFRONT_DISTRIBUTION_ID → your distribution ID

Step 4 — Reference Secrets in Your Workflow

Your GitHub Actions workflow never sees the actual values — it references them by name:

- name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v2 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: ${{ secrets.AWS_REGION }}
Even if someone forks your repo or your workflow logs are exposed, the secret values never appear. GitHub replaces them with *** in all output.

What Happens If the Credentials Leak?

This is the key question — and the whole point of least-privilege.

With admin credentials leaked:

An attacker can spin up EC2 instances for crypto mining, exfiltrate data from every S3 bucket, delete your entire infrastructure, create new IAM users with full access, and rack up a bill worth thousands of dollars — all before you notice.

With least-privilege deploy credentials leaked:

An attacker can upload files to one specific S3 bucket and invalidate one CloudFront distribution. That's it. You rotate the key, the damage is contained, and you move on.

Blast radius is everything in security. Least-privilege doesn't prevent breaches — it limits what an attacker can do when one happens.

How to Rotate a Compromised Key

If you ever suspect your credentials are exposed:

  1. Go to IAM → Users → portfolio-deploy → Security credentials
  2. Click Deactivate on the compromised key immediately
  3. Create a new access key
  4. Update the new values in GitHub Secrets
  5. Delete the old deactivated key
  6. Review CloudTrail logs to audit what the compromised key was used for

Lessons Learned

Further Reading

← All Posts