Résumé
ONYEDIKACHI BERNARDCLOUD · DEVSECOPS · AI
RésuméGet in touch
CASE 02IDENTITY & ACCESS

The Overpowered Role

A web server's IAM role could reach every S3 bucket in the account. I traced it, proved the risk, cut it down to what the app needs and verified the fix.

Status: REMEDIATED · VERIFIED

DOMAIN
Identity & access
PLATFORM
AWS EC2, IAM, S3
SEVERITY
High
OUTCOME
Least privilege

TOOLS

AWS EC2AWS IAMAWS CLIAWS Systems Manager (SSM)EC2 Instance Metadata ServiceIMDSv2Amazon S3AWS CloudShellIAM JSON policies
RESULT
BEFOREAny action, any bucket
AFTER3 actions, 1 bucket
01

Objective

I set out to check whether an EC2 workload's role was granted more than the application needs, whether its instance metadata settings exposed those credentials more than necessary, and to fix both without breaking the app.

02

Scenario

A web application runs on an EC2 instance and uses an IAM role through an instance profile. It needs limited access to one S3 bucket that holds its configuration, static assets and logs. The role I found had far broader S3 permissions than that. I wanted to answer six questions: which role the instance uses, what that role allows, whether it exceeds what the app needs, whether the metadata service exposes its credentials, whether I could demonstrate the excess access, and whether I could cut it down without breaking the app.

03

What I found

WebAppOverPrivS3Policy.jsonWILDCARD
{
  "Effect": "Allow",
  "Action": ["s3:*"],
  "Resource": "*"
}
FINDING 01HIGH

Over-privileged S3 permissions

The role's custom policy allowed every S3 action on every resource.

The app needed one bucket. The role could read, change or delete any bucket in the account, including finance reports. I proved it was usable, not theoretical: from an SSM session on the instance I listed the unrelated finance bucket and read an object from it.

FINDING 02MEDIUM

IMDSv1 still allowed

The instance reported HttpTokens: optional with a hop limit of 2, and the metadata endpoint returned the role's temporary credentials to a plain request. If an application bug ever let an attacker reach that endpoint, they would walk away with credentials carrying all of that S3 access. The two weaknesses made each other worse.

04

What I did

  1. Traced the identity chain

    I followed the running web server to its instance profile and the IAM role behind it, so every permission tied back to a workload.

  2. Read the role's policies

    I found one custom policy granting every S3 action on every resource, plus the standard SSM managed policy. No inline policies.

  3. Confirmed the reach

    From an approved SSM session I listed the app bucket and the finance bucket, and read an object from the finance bucket.

  4. Rewrote it to least privilege

    I mapped what the app really uses (read config and assets, write logs, list its own bucket) and replaced the wildcard with a scoped policy limited to those prefixes.

  5. Hardened instance metadata

    I required IMDSv2 tokens and dropped the hop limit from 2 to 1.

  6. Verified the fix

    Finance bucket access was denied, app access still worked, and the verification function returned PASS on all three checks.

EVIDENCE

BASH
# Identity chain
aws ec2 describe-instances --instance-ids $INSTANCE_ID \
  --query "Reservations[0].Instances[0].IamInstanceProfile.Arn" --output text

# Metadata exposure (before)
curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/

# Enforce IMDSv2
aws ec2 modify-instance-metadata-options --instance-id $INSTANCE_ID \
  --http-tokens required --http-endpoint enabled --http-put-response-hop-limit 1

Scoped policy (actions and resources only; account ID masked):

JSON
{
  "Statement": [
    { "Sid": "ReadConfigAndAssets", "Effect": "Allow", "Action": ["s3:GetObject"],
      "Resource": ["arn:aws:s3:::webapp-data-<ACCOUNT_ID>/config/*", "arn:aws:s3:::webapp-data-<ACCOUNT_ID>/assets/*"] },
    { "Sid": "WriteLogs", "Effect": "Allow", "Action": ["s3:PutObject"],
      "Resource": ["arn:aws:s3:::webapp-data-<ACCOUNT_ID>/logs/*"] },
    { "Sid": "ListAppBucket", "Effect": "Allow", "Action": ["s3:ListBucket"],
      "Resource": ["arn:aws:s3:::webapp-data-<ACCOUNT_ID>"],
      "Condition": { "StringLike": { "s3:prefix": ["config/*", "assets/*", "logs/*"] } } }
  ]
}
05

Validation

After the change, listing the finance bucket from the instance returned AccessDenied for s3:ListBucket. Listing the app bucket's config/ prefix still worked and returned app.conf and db.conf. The verification function reported:

CHECKRESULT
Over-privileged policy removedPASS
Scoped policy attachedPASS
IMDSv2 enforcedPASS
06

Before and after

CONTROLBEFOREAFTER
S3 actionss3:*Get, Put, List only
Resource scopeEvery bucketApp bucket prefixes
Finance bucketAllowedDenied
App bucketAllowedAllowed
IMDS tokensOptionalRequired
IMDS hop limit21
VerificationNot runPASS
07

What I recommend

  1. 01Give every EC2 role only the actions it needs, and avoid wildcards unless there is a documented reason.
  2. 02Require IMDSv2 on every workload that supports it.
  3. 03Review IAM policies on a schedule for unused or excessive permissions.
  4. 04Use resource-level restrictions instead of "Resource": "*" wherever the service allows it.
  5. 05Separate permissions per workload so one compromise does not expose unrelated data.
  6. 06Validate policies with the IAM policy simulator, and watch IAM and S3 activity through CloudTrail.

“An EC2 role is part of the workload's attack surface. Securing it means protecting how credentials are obtained and limiting what they can do.”

WHAT I LEARNED

SKILLS IT PROVES

  • Cloud Security
  • AWS IAM
  • Least Privilege
  • Access Control
  • EC2 Security Hardening
  • Vulnerability Assessment
  • S3 Access Control
  • Misconfiguration Detection and Remediation
  • Security Validation and Testing
View my labs on GitHub