Zero to Hero: Cybersecurity Architecture Masterclass, Module 3
← Module 2: Proactive Design · Module 3: Cloud-Native Hardening · Module 4: Resilience & Survival →
12 min read
TL;DR
- Cloud native infrastructure hardening starts from a different assumption than on-prem hardening: there is no network perimeter, only an identity perimeter — every AWS API call is the boundary
- IMDSv1 (the EC2 metadata service without a token) is the single highest-leverage cloud-native hardening fix available — it turned an SSRF bug into the Capital One breach
- IAM policy design is architecture, not IT administration: least privilege, permission boundaries, and SCPs compose into the actual perimeter
- Infrastructure-as-code scanning (
checkov,tfsec) catches identity-perimeter mistakes in a pull request instead of in an incident aws iam simulate-principal-policyanswers “can this role actually do that?” definitively, without waiting to find out in production- Recommendation: treat IMDSv2 enforcement and IAM least-privilege review as pipeline gates, not periodic audits — the same “build constraint, not process step” principle from the OS Hardening series
The Big Picture: The Perimeter Moved to the API Call
ON-PREM MODEL CLOUD-NATIVE MODEL
────────────── ──────────────────
Firewall at network edge No fixed network edge
│ │
Trusted internal subnet Every API call carries its
│ own identity + policy
Server assumed safe if │
inside the firewall IAM evaluates: who is this,
what can they do, right now
│
Perimeter = the IAM policy
attached to the caller
Cloud-native infrastructure hardening means accepting that the network no longer defines what’s trusted — the AWS identity perimeter, enforced entirely through IAM policy evaluation on every single API call, is the only perimeter that actually exists. Module 1 called this the shift from network-centric to identity-centric trust; this module makes it concrete with the two failures that actually break it in production: a leaky metadata service and an over-permissioned role.
The Breach That Made IMDSv2 Mandatory
In 2019, a misconfigured WAF in front of a bank’s application allowed a Server-Side Request Forgery (SSRF) — an attacker convinced the application server to make an HTTP request to http://169.254.169.254, the EC2 instance metadata endpoint. IMDSv1 answered with no authentication required at all: temporary IAM credentials for the role attached to that instance, handed to anyone who could make the server issue that one request.
Those credentials had read access to S3. The attacker used them to exfiltrate over 100 million customer records. This is the Capital One breach — covered in full in the Purple Team series — and it is the single clearest illustration in cloud history of why “the perimeter is the identity, not the network” isn’t a slogan — it’s a description of exactly where that breach actually happened. The WAF misconfiguration was the entry point. The metadata service handing out credentials with zero verification was the architectural failure that turned an SSRF bug into a 100-million-record breach.
IMDSv2 closes this specific gap by requiring a session token, fetched via a PUT request, before any metadata GET request is honored — and that PUT request cannot be replayed through a typical SSRF, because SSRF vulnerabilities almost always only allow GET-style requests to be forged. This single setting is the highest-leverage cloud-native hardening control available, and it should be enforced at the account level, not left as an opt-in per instance:
# Check whether IMDSv2 is enforced (HttpTokens: required) on an instance
$ aws ec2 describe-instances --instance-ids i-0abc123 \
--query 'Reservations[].Instances[].MetadataOptions'
{
"HttpTokens": "required",
"HttpPutResponseHopLimit": 1,
"HttpEndpoint": "enabled"
}
# "required" = IMDSv2 only. "optional" = IMDSv1 still works — the gap.
# Enforce it account-wide for all new instances
$ aws ec2 modify-instance-metadata-defaults \
--http-tokens required --http-put-response-hop-limit 1
IAM Policy Design Is Architecture
If the metadata service is one way the identity perimeter leaks, an over-permissioned IAM policy is the other — and it’s far more common, because it doesn’t require a bug at all. It only requires a policy written with "Resource": "*" because scoping it felt like it would slow down a deploy.
Least privilege means a role can do exactly what its function requires and nothing else — not “read-only across the account,” but “read this specific S3 prefix, write to this specific queue.”
Permission boundaries cap what a role can ever be granted, even by someone with iam:CreatePolicy access — a safety rail against exactly the kind of iam:PassRole privilege escalation covered in the Cloud IAM series, not just against the policy as originally written.
Service Control Policies (SCPs) apply at the AWS Organization level, capping what any role in an account can do regardless of how permissive that account’s own IAM policies are — the outermost layer of the identity perimeter, and the one that survives a single account being compromised.
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["s3:GetObject"],
"Resource": "arn:aws:s3:::billing-invoices/tenant-4471/*"
}]
}
That policy can only ever read one tenant’s invoice prefix. Compare it to "Resource": "arn:aws:s3:::billing-invoices/*" — functionally identical for the one use case the developer was testing, and catastrophically different the day this role’s credentials leak.
Quick Check: Can This Role Actually Do That?
Don’t wait to find out in production. aws iam simulate-principal-policy evaluates a specific action against a role’s actual attached and inline policies — including SCPs and permission boundaries — and gives you a definitive allow/deny before anything runs:
$ aws iam simulate-principal-policy \
--policy-source-arn arn:aws:iam::123456789012:role/billing-api-role \
--action-names s3:GetObject \
--resource-arns arn:aws:s3:::billing-invoices/tenant-9982/*
{
"EvaluationResults": [{
"EvalActionName": "s3:GetObject",
"EvalResourceName": "arn:aws:s3:::billing-invoices/tenant-9982/*",
"EvalDecision": "explicitDeny", # ← the answer you needed before deploying
"MatchedStatements": [...]
}]
}
explicitDeny here means some policy statement — the role’s own policy, a permission boundary, or an SCP — explicitly blocks the action, and that takes precedence over any Allow anywhere else in the policy chain (Module 1’s deny-by-default evaluation model, in practice). Run this simulation as part of code review for any new IAM policy, not after the role is already attached to a running service.
Catching This Before It Ships: Cloud-Native Hardening via IaC Scanning
Manually reviewing every Terraform IAM policy in every pull request doesn’t scale past a handful of engineers. checkov and tfsec scan infrastructure-as-code for exactly the patterns above — wildcard resources, IMDSv1 left enabled, public S3 buckets — as a CI step, before terraform apply ever runs:
$ checkov -d ./terraform --check CKV_AWS_79,CKV_AWS_8
Check: CKV_AWS_79: "Ensure Instance Metadata Service Version 1 is not enabled"
FAILED for resource: aws_instance.billing_api
File: main.tf:14-22
Check: CKV_AWS_8: "Ensure IAM policies do not allow full administrative privileges"
FAILED for resource: aws_iam_role_policy.billing_api_policy
File: iam.tf:8-15
Resource: "*"
A failed checkov check blocking a pull request is the identity-perimeter equivalent of Stratum’s pipeline gate refusing to snapshot an unhardened image — the unsafe configuration never reaches an account where it can be exploited, because the check runs before merge, not after an audit finds it months later.
Production Gotchas
IMDSv2 enforcement can break old SDKs and tools silently. Some older AWS SDK versions and third-party agents assume IMDSv1 and simply fail to fetch credentials once HttpTokens: required is set — test in staging before enforcing account-wide.
iam simulate-principal-policy doesn’t account for resource-based policies on the target. It evaluates the principal’s policies correctly, but if the target (an S3 bucket, a KMS key) has its own resource policy denying access, you need simulate-custom-policy with both policies supplied to get the full picture.
SCPs fail closed in a way that’s easy to misdiagnose. An SCP deny produces the same AccessDenied error as a missing IAM permission — check the SCP layer explicitly before assuming the role’s own policy is the problem, or you’ll spend an hour widening a policy that was never the actual blocker.
checkov/tfsec false positives erode trust in the gate fast. Suppress specific, documented exceptions inline (#checkov:skip=CKV_AWS_79:reason) rather than disabling the check account-wide the first time it blocks something legitimate.
Framework Alignment
| Framework | Control / ID | Architectural Mapping |
|---|---|---|
| NIST CSF 2.0 | PR.AA-05 | Access permissions are managed, incorporating least privilege and separation of duties. |
| NIST SP 800-207 | Zero Trust | The identity perimeter, enforced per-API-call, is the direct implementation of continuous verification. |
| ISO 27001:2022 | 8.2 | Privileged access rights are restricted and managed. |
| SOC 2 | CC6.3 | The entity authorizes, modifies, or removes access based on roles and responsibilities. |
Key Takeaways
- The identity perimeter, not the network, is what cloud-native hardening actually secures — every IAM policy evaluation is a perimeter check
- IMDSv2 enforcement is the single highest-leverage fix available and should be an account-wide default, not an opt-in
- Least privilege, permission boundaries, and SCPs are three layers of the same perimeter — design all three deliberately, don’t rely on one
aws iam simulate-principal-policygives a definitive answer before deployment instead of an incident after- IaC scanning turns identity-perimeter mistakes into blocked pull requests instead of production findings
What’s Next
Module 3 hardened the identity perimeter against external and lateral threats. Module 4 asks what happens after a perimeter fails anyway — specifically, how immutable, WORM-locked data architecture makes ransomware and mass-deletion attacks survivable even when an attacker has already gotten past every control this module covers.
Next: Module 4: Resilience & Survival — Immutable Data Architecture and Surviving Ransomware via WORM
Get the full masterclass in your inbox → linuxcent.com/subscribe