Everything for AWS Services in one place — pick a section below. 10 reviewed items across 4 content types.
Quick Answer
AssumeRole is an STS, Security Token Service, API call where a caller, a user or more commonly an AWS service like an EC2 instance or Lambda function, presents an identity and requests to become a specific IAM role, and STS returns a short-lived access key, secret key, and session token, typically valid from 15 minutes to a few hours, instead of any permanent credential. This is safer than long-lived access keys because a leaked temporary credential expires quickly on its own and is scoped to exactly the permissions of the assumed role, whereas a leaked long-lived access key remains valid indefinitely until someone notices and manually revokes it.
Detailed Answer
Imagine a hotel that, instead of giving long-term employees a single master key that opens every room forever, issues a fresh keycard every shift that only opens the specific rooms that employee needs for that shift and automatically deactivates itself at the end of the shift. If a keycard is lost or stolen, the exposure window is just the remainder of that one shift — nobody has to track down and physically disable a master key that could open every door indefinitely, for years, until someone happens to notice it's missing.
AWS IAM's AssumeRole mechanism, backed by the Security Token Service, was designed to replace exactly the older pattern of generating a long-lived access key and secret key pair, essentially a master key that works until someone manually deletes it, with this shift-based, auto-expiring keycard model. This matters because long-lived access keys are a huge source of real breaches — they get accidentally committed to git repositories, baked into container images, or leaked in logs, and unlike a temporary credential, they don't self-destruct, so a leak from months ago can still be actively exploitable if nobody caught it in the meantime.
Internally, when a service like checkout-worker running on an EC2 instance or in a Lambda function needs to access, say, an S3 bucket or a DynamoDB table, it calls sts:AssumeRole, often transparently via the AWS SDK's default credential provider chain, which for EC2 fetches credentials from the instance metadata service, or for Lambda from environment variables injected by the Lambda execution environment, specifying the target role's ARN. STS validates that the calling identity is permitted to assume that role, checked against the role's trust policy, which explicitly lists who's allowed to assume it, and if allowed, returns a temporary AccessKeyId, SecretAccessKey, and SessionToken bundle valid for a configured duration, default one hour, configurable up to the role's max session duration. All subsequent AWS API calls include that session token alongside the temporary keys, and AWS automatically rejects any request using expired temporary credentials outright.
In production, this pattern is what underlies EC2 instance profiles, Lambda execution roles, and cross-account access. A payments-api service can be granted an instance profile role scoped tightly to exactly the S3 bucket and DynamoDB table it needs, and nothing more, with credentials that rotate automatically every hour without any human or deployment process ever touching a static secret. Teams monitor CloudTrail for AssumeRole events to track exactly which identities are assuming which roles and from where, which is also a strong security signal — an AssumeRole call from an unexpected IP range or an unusual role and service combination is a classic indicator of compromised infrastructure worth investigating immediately.
The gotcha: teams sometimes still create long-lived IAM user access keys just to get something working quickly, like a local script or a CI job, and because those keys never expire on their own, they tend to persist for years, attached to overly broad permissions granted temporarily that nobody ever tightens, turning what was meant to be a quick shortcut into a long-term, high-blast-radius credential far more dangerous than the fleet of properly-scoped, auto-expiring role-based credentials everything else uses. Auditing for and eliminating long-lived access keys entirely, replacing them with role assumption, including for CI/CD via OIDC federation, is a common senior-level security initiative.
Code Example
# assume a role and get temporary credentials valid for 1 hour
aws sts assume-role \
--role-arn arn:aws:iam::123456789012:role/payments-api-s3-access \
--role-session-name payments-api-checkout-worker-session
# trust policy on the role: WHO is allowed to assume it
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "Service": "ec2.amazonaws.com" },
"Action": "sts:AssumeRole"
}]
}
# CloudTrail: audit which identities assumed which roles, and from where
aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRoleInterview Tip
A junior engineer typically says 'AssumeRole lets you get permissions for a role,' without explaining why that's more secure than an access key. For a senior/architect role, the interviewer is actually looking for you to articulate the core security property — short expiry limits blast radius automatically, no human needs to remember to rotate or revoke anything — and to discuss real detection value, like monitoring CloudTrail AssumeRole events for anomalous patterns. Extra credit for flagging that long-lived IAM user access keys created as a quick fix are a common, dangerous drift pattern that undermines an otherwise solid role-based architecture across the rest of the account.
◈ Architecture Diagram
┌──────────┐ AssumeRole ┌─────┐ temp creds ┌────────┐
│EC2/Lambda│ ───────────▶ │ STS │ ───────────▶ │ S3/DDB │
└──────────┘ └─────┘ (1hr TTL) └────────┘
✗ long-lived key = never expires, high risk
✓ temp creds = auto-expire, scoped to role💬 Comments
Quick Answer
ALB health checks and actual request-serving capacity check fundamentally different things — health checks confirm a lightweight path responds successfully at a set interval, but real requests can still fail from causes health checks never exercise, most commonly an idle-timeout mismatch between the ALB and the backend's own keep-alive timeout, causing the ALB to reuse a connection the backend already closed. Check ALB access logs' target_status_code and error_reason fields alongside the backend's keep-alive settings — a 502 with target_status_code -1 means the ALB never got a valid response at all, pointing at a connection-level issue rather than an application error.
Detailed Answer
Imagine a restaurant host who checks every 30 seconds whether a specific table is occupied or free by glancing at it, a very cheap, quick check. That host can honestly report table 12 is free while completely missing that the kitchen is actually so backed up that any customer seated there right now would wait an hour for food, or that table 12's chair just broke in a way the host's quick glance never caught. The health check and the actual dining experience are checking fundamentally different things, and one being fine says nothing reliable about the other.
AWS's Application Load Balancer health checks are deliberately lightweight and narrow by design — they hit a configured path, like /health, at a configured interval and expect a specific status code, existing purely to detect whether a target is completely down or unresponsive so the ALB can stop routing to a dead instance. They were never designed to guarantee every real request will succeed, because a full simulation of production traffic patterns on every health check interval would itself add load and complexity that defeats the purpose of a cheap, frequent liveness signal in the first place.
The actual 502 usually stems from a layer the health check doesn't touch: TCP and HTTP connection handling between the ALB and the target. ALBs keep connections to backend targets alive and reuse them for multiple requests for efficiency, governed by an idle timeout on both the ALB side, default 60 seconds, configurable, and the backend web server's own keep-alive timeout — a Node.js or nginx backend serving payments-api might have a shorter keep-alive timeout than the ALB expects. If the backend closes a connection the ALB still considers open, and then the ALB tries to reuse it for a new request, the backend responds with a connection reset, and the ALB surfaces that as a 502 to the client, a failure mode entirely invisible to a simple periodic health check hitting a fresh connection each time it runs.
In production, the ALB's access logs, delivered to S3 if enabled, include a target_status_code field and error_reason. A 502 where target_status_code shows -1 means the ALB never received a valid HTTP response from the target at all, a connection-level failure, versus a genuine 502 the application itself returned deliberately. Cross-referencing the timing of these 502s against deploy events is also critical, since a common cause is a rolling deployment where a target gets deregistered and stops accepting new connections, but in-flight requests routed to it right before deregistration get abruptly cut off if the deregistration delay, the connection draining timeout, is set too short for how long real requests to something like checkout-worker actually take to complete.
The non-obvious gotcha: the fix for the keep-alive mismatch is counterintuitive to many engineers — you must set the backend server's keep-alive or idle timeout to be LONGER than the ALB's idle timeout, not shorter, so the backend never closes a connection the ALB still considers usable. Getting this backwards, a very common misconfiguration since it feels natural to want the backend to be the more aggressive timeout, is one of the single most common causes of intermittent, hard-to-reproduce 502s that show up only under real concurrent traffic and never during simple manual health-check-style testing.
Code Example
# check target health status - passing here does NOT rule out connection-level 502s aws elbv2 describe-target-health --target-group-arn arn:aws:elasticloadbalancing:...:targetgroup/checkout-worker-tg # nginx backend: keep-alive MUST exceed the ALB idle timeout (default 60s), never be shorter keepalive_timeout 65s; # 5s longer than ALB's 60s idle timeout, prevents connection races # raise the ALB idle timeout to match, if the backend legitimately needs longer connections aws elbv2 modify-load-balancer-attributes \ --load-balancer-arn arn:aws:elasticloadbalancing:...:loadbalancer/app/checkout-worker-alb \ --attributes Key=idle_timeout.timeout_seconds,Value=90
Interview Tip
A junior engineer typically says 'the health check is passing so the instance must be fine,' treating health checks as a complete signal of request-serving capability. For a senior/architect role, the interviewer is actually looking for you to name the specific mechanism, ALB-to-backend keep-alive timeout mismatches and deregistration or connection-draining races during deploys, and to know the fix requires the backend's keep-alive timeout to exceed the ALB's, which is counterintuitive enough that most engineers get it backwards on their first try. Referencing ALB access log fields like target_status_code and error_reason for diagnosis is a strong practical signal of real production experience.
◈ Architecture Diagram
┌─────┐ keep-alive 60s ┌────────┐ keep-alive 30s ┌──────┐
│Client│ ───────────────▶│ ALB │ ───────────────▶│Target│
└─────┘ └────────┘ └──────┘
reuses conn backend already
after 45s ✗ closed it → 502💬 Comments
Quick Answer
Combine three independent signals: AWS Cost Anomaly Detection for sudden spend changes, S3 Server Access Logs or CloudTrail data events for unusual access patterns, spikes in GET/PUT volume or requests from unexpected principals, and AWS Config rules or IAM Access Analyzer that continuously check bucket policies and ACLs for public accessibility and alert immediately on drift. Public exposure is a security incident, not just a cost one, and needs far faster detection than a monthly billing review would ever catch.
Detailed Answer
Think about a family storage unit rental where you'd want three different kinds of alerts: one for your bill went up unexpectedly, something changed in usage, one for someone unrelated to the family is now able to access the unit, a lock got left open, and one for a bunch of items were moved out overnight, unusual activity. Each of those is a genuinely different kind of problem requiring a different detection method — checking the monthly bill alone would never catch an unlocked door, and checking the lock alone would never catch a slow, creeping cost increase from someone quietly hoarding more and more storage over time.
S3 buckets accumulate exactly these three independent risk categories over time, and AWS provides separate purpose-built tools for each because they're genuinely different problems. Cost Anomaly Detection and budget alerts exist for the usage-and-spend-changed-unexpectedly category, built on AWS's cost and usage data using anomaly detection rather than fixed thresholds, so it catches gradual drift, not just hard limit breaches. CloudTrail data events and S3 Server Access Logs exist for the who-is-actually-touching-this-data category. AWS Config rules plus IAM Access Analyzer exist for the is-this-bucket's-permission-configuration-actually-safe category, continuously evaluating policy and ACL state rather than only checking at creation time and never again.
Internally, a typical detection setup enables S3 Server Access Logging, or CloudTrail data events, which are more detailed but higher-volume and higher-cost, writing to a dedicated logging bucket, feeds that into an analysis pipeline, Athena queries over the access logs or a SIEM ingestion, and separately runs AWS Config's s3-bucket-public-read-prohibited and s3-bucket-public-write-prohibited managed rules continuously, which evaluate bucket policy and ACL state on every change and flag non-compliant buckets within minutes, not at the next scheduled audit weeks later. Cost Anomaly Detection runs its own ML-based baseline over historical spend per service and linked account, firing when actual spend deviates meaningfully from the learned pattern, catching a runaway lifecycle rule, one accidentally configured to keep transitioning already-archived objects repeatedly and incurring repeated retrieval charges, that a fixed dollar-threshold budget alert might miss entirely if the absolute dollar amount involved is still small.
In production, teams route AWS Config non-compliance findings, especially public-bucket rules, to a page-immediately channel since public exposure of something like a payments-api-logs bucket is a live security incident, while cost anomalies typically route to a next-business-day review channel unless the anomaly is extreme, since cost issues are important but rarely require middle-of-the-night response. Access pattern analysis via Athena over access logs is usually a weekly or on-demand investigation tool rather than continuous alerting, because usage volume alone is noisy, but it becomes essential once an anomaly from either of the other two signals points you toward a specific bucket worth investigating further.
The non-obvious gotcha: a bucket can be perfectly private, no public policy, AWS Config green across the board, while still leaking data through an overly permissive IAM role attached to an unrelated service. If user-auth-service's IAM role was granted s3:* on all buckets months ago to unblock a deploy and never tightened afterward, that role can read a completely unrelated, correctly-locked-down bucket, and neither the bucket-policy checks nor a public-access scan would ever catch this, because the exposure lives in the IAM role's permissions, not the bucket's own configuration. A full picture requires reviewing IAM policies granting S3 access fleet-wide, not just auditing bucket policies in isolation.
Code Example
# continuously check for public buckets, not just at creation time
aws configservice put-config-rule --config-rule '{
"ConfigRuleName": "s3-bucket-public-read-prohibited",
"Source": { "Owner": "AWS", "SourceIdentifier": "S3_BUCKET_PUBLIC_READ_PROHIBITED" }
}'
# set up cost anomaly detection scoped to S3 spend specifically
aws ce create-anomaly-monitor --anomaly-monitor '{
"MonitorName": "s3-spend-anomaly",
"MonitorType": "DIMENSIONAL",
"MonitorDimension": "SERVICE"
}'
# query access logs for unusual GET volume against a specific bucket
aws athena start-query-execution --query-string \
"SELECT remoteip, COUNT(*) FROM s3_access_logs WHERE bucket='payments-api-logs' GROUP BY remoteip ORDER BY 2 DESC"Interview Tip
A junior engineer typically says 'turn on S3 access logging and review it,' treating this as one single problem. For a senior/architect role, the interviewer is actually looking for you to separate cost, access-pattern, and public-exposure risk into three distinct detection mechanisms with different urgency levels, and to recognize the deeper gotcha that a private bucket can still leak through an overly broad IAM role elsewhere in the account, meaning true coverage requires auditing IAM permissions fleet-wide, not just the bucket's own policy in isolation.
◈ Architecture Diagram
┌─────────┐ ┌──────────────┐ ┌───────────────┐
│ Cost │ │ Access Logs │ │ Config/Access │
│ Anomaly │ │ (CloudTrail) │ │ Analyzer │
└─────────┘ └──────────────┘ └───────────────┘
│ spend↑ │ unusual GETs │ public=✗
↓ ↓ ↓
ticket investigate page NOW
✗ blind spot: private bucket + overbroad IAM role elsewhere💬 Comments
Quick Answer
Manage security group rules exclusively through infrastructure-as-code with mandatory peer review, layer a preventive guardrail using AWS Organizations Service Control Policies that outright deny any security group rule allowing inbound traffic from 0.0.0.0/0 on sensitive ports like a database port, and run continuous detective controls, AWS Config rules checking for 0.0.0.0/0 ingress on non-web ports, that auto-remediate or page immediately on drift. The key principle is defense in depth: code review catches most mistakes before they ship, SCPs make certain dangerous changes structurally impossible regardless of who tries them, and Config rules catch anything that slips through both anyway.
Detailed Answer
Think about a bank vault door with three independent layers of protection: a policy requiring two employees to authorize any change to the vault's access list, a process control; a physical mechanism that makes it impossible to set the vault's lock to always-open no matter who tries, even a well-meaning employee under pressure, a hard technical constraint; and a silent alarm that triggers immediately if the vault's configuration is ever found in an unsafe state, even if it happened by some unexpected path, continuous detection. No single layer is trusted alone — the whole design assumes any one layer can fail or be bypassed under real-world conditions.
AWS security groups are stateful virtual firewalls attached to resources like EC2 instances or RDS databases, and because a single misconfigured rule, allowing 0.0.0.0/0, meaning any IP address on the internet, on a database port like 5432, can expose sensitive data with essentially no additional steps required, mature platform teams don't rely on any single safeguard. Instead they deliberately layer preventive controls, making the dangerous action impossible or requiring review, with detective controls, catching it fast if it happens anyway, because manual review processes do fail under time pressure, and purely reactive monitoring alone means some window of actual exposure exists before anyone notices it.
The concrete layers: security group and VPC configuration lives entirely in Terraform or CloudFormation, never manually edited in the console for production environments, since console changes are themselves flagged by CloudTrail and Config as drift from the IaC-managed state, with pull request review required before any change merges. At the AWS Organization level, a Service Control Policy attached to the production OU can explicitly deny ec2:AuthorizeSecurityGroupIngress calls where the request includes 0.0.0.0/0 as a CIDR source combined with specific sensitive ports, making that specific dangerous action fail with an access-denied error regardless of the IAM permissions of whoever's account is making the call — this is a hard organizational boundary, not a convention anyone has to remember to follow correctly. Separately, AWS Config continuously evaluates every security group's actual current rules against custom rules checking for open sensitive ports, and can be wired to an automatic remediation Lambda that reverts an unsafe rule within minutes of it appearing, whatever the source of the change.
In production, teams monitor AWS Config compliance dashboards specifically for security-group-related rules across every account in the Organization, not just production, since a lower-environment misconfiguration can be a stepping stone for lateral movement, and treat any SCP-denied API call attempt as a signal worth investigating even though it was successfully blocked. A legitimate engineer hitting that guardrail unexpectedly usually means their actual intent, opening access for a specific partner IP range, say, needs a properly scoped rule instead, while a non-legitimate attempt is itself a security signal worth escalating right away.
The non-obvious gotcha: SCPs only govern IAM API calls within accounts that are members of the AWS Organization and only apply going forward from when they're attached — they don't retroactively fix or even flag security groups that were already misconfigured before the SCP existed. Rolling out this kind of guardrail needs to be paired with an immediate one-time sweep, using AWS Config's ability to evaluate existing resources against a new rule, to find and fix any pre-existing exposed database security groups, or the SCP creates a false sense that we're now protected while an old, dangerous rule from before the policy was silently grandfathered in and still live in production.
Code Example
# SCP: deny opening sensitive ports to the internet, regardless of caller's IAM permissions
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Deny",
"Action": "ec2:AuthorizeSecurityGroupIngress",
"Resource": "*",
"Condition": {
"StringEquals": { "ec2:ingress-rule-desc-cidr": "0.0.0.0/0" },
"NumericEquals": { "ec2:ingress-rule-port-range": "5432" }
}
}]
}
# security group managed via Terraform, never manual console edits in production
resource "aws_security_group_rule" "payments_api_db" {
type = "ingress"
from_port = 5432
to_port = 5432
protocol = "tcp"
cidr_blocks = ["10.0.4.0/24"] # internal subnet only, never 0.0.0.0/0
security_group_id = aws_security_group.payments_api_db.id
}
# one-time sweep BEFORE relying on the SCP: find pre-existing exposed rules
aws configservice start-config-rules-evaluation --config-rule-names restricted-sshInterview Tip
A junior engineer typically says 'require code review for security group changes,' relying entirely on process discipline to prevent mistakes. For a senior/architect role, the interviewer is actually looking for layered defense: IaC plus review as the first layer, Service Control Policies making the dangerous action structurally impossible as a second, and continuous Config-based detection with auto-remediation as a third catch-all, because no single layer is assumed reliable under real-world pressure and human error. Flagging that SCPs are not retroactive, and that rollout requires an explicit sweep of pre-existing resources, is a strong signal of real operational experience with these controls.
◈ Architecture Diagram
┌────────┐ ┌──────────┐ ┌─────────────┐ │ IaC │ → │ SCP │ → │ Config │ │+review │ │(structural│ │ (detective, │ │(process)│ │ deny) │ │auto-remediate)│ └────────┘ └──────────┘ └─────────────┘ layer 1 layer 2 layer 3 ✗ SCP is NOT retroactive - sweep existing resources first
💬 Comments
Context
A 25-account AWS Organization where years of expedience left long-lived IAM user keys everywhere: in CI systems, on developer laptops, baked into two AMIs, and shared between 'temporary' integrations — with a quarterly key-rotation spreadsheet as the control.
Problem
Access keys are unrevocable-in-practice (nobody knew all the consumers), unattributable (shared keys mean shared identity), and the rotation spreadsheet was theater; a security review found keys unused for two years with AdministratorAccess, and one key in a public repo's history.
Solution
Ran an eradication program in three fronts: humans moved to Identity Center (SSO) with short-lived sessions and no IAM users at all; workloads moved to roles — EC2 instance profiles, IRSA for EKS, and OIDC federation for external CI (GitHub Actions trust policies scoped to repo+branch+environment claims); machine-to-machine integrations that 'needed' keys got re-architected onto AssumeRole with external IDs or replaced with resource policies. Permission boundaries cap what any delegated role can grant, closing the privilege-escalation loop. Enforcement: SCPs deny iam:CreateAccessKey org-wide (break-glass exempted), and credential reports drive a weekly burn-down until the count hit zero.
Commands
trust policy: {Federated: token.actions.githubusercontent.com, Condition: {StringEquals: {sub: 'repo:org/app:environment:prod'}}}SCP: Deny iam:CreateAccessKey unless PrincipalArn == break-glass
audit: aws iam generate-credential-report; weekly count -> zero
Outcome
IAM user keys went from 340 to 0 in two quarters; every action in CloudTrail now attributes to a specific human session or workload role; the leaked-key class of incident became structurally impossible; and the quarterly rotation spreadsheet was deleted with prejudice. The security review's follow-up closed all credential findings.
Lessons Learned
The last 20 keys took half the program — each guarded a forgotten integration that had to be understood before it could be re-architected. OIDC trust-policy conditions (branch, environment) are where the real least-privilege lives; a role any branch can assume is just a slower access key.
💬 Comments
Context
A data platform whose S3 spend had grown into the top-three infra line items: 2PB across 200 buckets, everything in Standard, no lifecycle policies, and multipart-upload debris plus noncurrent versions accumulating invisibly since bucket creation.
Problem
Nobody owned storage-class decisions ('Standard is the default'), versioned buckets retained every noncurrent object forever, incomplete multipart uploads held phantom gigabytes, and access patterns were unmeasured — the platform was paying hot-storage prices for archives read once a year.
Solution
Measured first: S3 Storage Lens plus Storage Class Analysis on the big buckets mapped actual access patterns (68% of bytes untouched in 90+ days). Then policy by data class: Intelligent-Tiering as the default for unpredictable-access buckets (automatic, no retrieval-fee surprises for the frequent/infrequent tiers), explicit lifecycle transitions for known-pattern data (logs: Standard -> Standard-IA at 30d -> Glacier Instant at 90d -> expire at 400d per compliance), noncurrent-version expiration everywhere versioning ran (keep 3 versions/30 days), and AbortIncompleteMultipartUpload on every bucket via org-wide bucket-policy baseline. Glacier Deep Archive took the true archives after a retrieval-time sign-off with data owners.
Commands
lifecycle: {Transitions: [{Days: 30, StorageClass: STANDARD_IA}, {Days: 90, StorageClass: GLACIER_IR}], NoncurrentVersionExpiration: {NoncurrentDays: 30, NewerNoncurrentVersions: 3}, AbortIncompleteMultipartUpload: {DaysAfterInitiation: 7}}aws s3api put-bucket-intelligent-tiering-configuration --bucket data-lake ...
storage lens: org-wide dashboard -> bytes by class, noncurrent share, MPU debris
Outcome
Monthly S3 spend dropped 60% within a quarter: 30% from class transitions, 18% from noncurrent-version expiry (one bucket held 11 versions average), 12% from multipart debris and true-archive Deep Archive moves. Retrieval-fee surprises: zero, because Intelligent-Tiering covered the unpredictable buckets and explicit policies only touched measured-cold data.
Lessons Learned
Measure before transitioning — one 'obviously cold' bucket was a model-training input read in monthly bursts, and Standard-IA retrieval fees would have eaten the savings; Intelligent-Tiering handled it correctly. Multipart debris and noncurrent versions are the free money: pure waste, zero risk, one policy line each.
💬 Comments
Symptom
During a marketing-event traffic spike, a VPC-attached Lambda's error rate climbs with a mix of invocation failures and multi-second cold starts; simultaneously, an ECS service in the same subnets fails to scale out. CloudWatch shows Lambda throttles at zero — capacity exists, but something else is starving.
Error Message
Lambda: 'ENILimitReachedException / Lambda was not able to create an ENI in the VPC ... because the subnet has no free IP addresses'. ECS events: 'unable to place task ... no available IP addresses in subnet subnet-0abc'.
Root Cause
The Lambda had been attached to the same two small private subnets (/24s) as the general workload tier 'because they had NAT'. Hyperplane ENI sharing keeps steady-state ENI counts low, but the scale surge multiplied concurrent execution environments while ECS scaled in the same pools — and the subnets' ~500 usable IPs ran out. Two elastic systems shared one small, unmonitored address pool, and the first victim was whoever asked next.
Diagnosis Steps
Solution
Immediate: shifted the Lambda to two larger, dedicated /22 subnets (config change, no code) and let the event ride. Durable: subnet strategy by workload class — elastic compute (Lambda, Fargate, autoscaled ECS) gets dedicated large subnets sized for peak concurrency math, static infra stays separate; free-IP-per-subnet became a monitored metric with alerts at 30% remaining; and the VPC's CIDR plan gained secondary CIDR headroom for the next growth event.
Commands
aws ec2 describe-subnets --subnet-ids subnet-0abc --query 'Subnets[].AvailableIpAddressCount'
aws lambda update-function-configuration --function-name api-hook --vpc-config SubnetIds=subnet-big1,subnet-big2
alert: subnet_available_ips / subnet_total_ips < 0.3
Prevention
Treat subnet free-IP as a capacity dimension with the same seriousness as CPU quota: monitor it, alert on it, and do peak-concurrency arithmetic when attaching elastic services to subnets (Lambda: peak concurrent executions; Fargate/ECS: peak tasks — each consumes an IP). Never co-tenant elastic and static tiers in small shared subnets.
💬 Comments
Symptom
An external notification (a researcher's report, mercifully, not an incident) arrives: the company's RDS endpoint answers connection attempts from the internet. The database sits in a 'private' subnet — but its security group contains an inbound rule allowing 5432 from 0.0.0.0/0, and the subnet's route table, changed during an old migration, exposes a path via a misconfigured public NAT instance's subnet association.
Error Message
No system error. Config timeline: the SG rule was added three years earlier during a vendor integration debugging session ('temporary — will remove after the call'); the routing exposure arrived eighteen months later during a network migration; neither change was ever reviewed against the other.Root Cause
Two individually-survivable mistakes composed: an over-broad SG rule that outlived its author's memory (classic 'temporary' half-life: forever), and a later routing change that converted 'broad rule, unreachable anyway' into 'reachable'. No continuous control examined actual reachability — SG audits happened annually by spreadsheet, and the routing migration's review never considered which dormant SG rules it would activate.
Diagnosis Steps
Solution
Closed the rule immediately and verified via VPC Reachability Analyzer that no path remained; rotated database credentials and audited connection logs for the exposure window (no unauthorized access found — the researcher was first). Structural: AWS Config rules + Security Hub controls continuously flag 0.0.0.0/0 on database/admin ports with auto-remediation (rule removal + ticket) for tier-1 resources; Network Access Analyzer scopes run weekly proving 'no internet path to data tier' as an assertion, not an assumption; and SG changes now carry expiry tags — anything marked temporary auto-opens a removal ticket at its deadline.
Commands
aws ec2 revoke-security-group-ingress --group-id sg-xyz --protocol tcp --port 5432 --cidr 0.0.0.0/0
aws ec2 start-network-insights-analysis ... # Reachability Analyzer: internet -> db-tier must fail
config rule: restricted-common-ports + auto-remediation; SG tag: {temporary: 2026-08-01} -> reaperPrevention
Audit reachability, not rules in isolation — composition is where exposures live (rule x route x peering). Continuous automated controls (Config/Security Hub) beat annual spreadsheets by exactly the three years this rule survived. 'Temporary' needs mechanical expiry; human memory is not a control.
💬 Comments