✓ Human-authored analysis; AI used for formatting and proofreading.
The AWS Load Balancer Controller manages security groups for ALBs on EKS. It identifies which security groups it owns using three tags:
elbv2.k8s.aws/cluster
ingress.k8s.aws/stack
ingress.k8s.aws/resource
When it reconciles an Ingress resource, it queries EC2 for security groups matching those tags. If it finds one, it assumes ownership and applies the Ingress annotations as inbound rules.
Tags are not a security boundary. Anyone who can apply tags to a security group can claim ownership of it in the controller's view.
A researcher demonstrated this in HackerOne #1238482: a developer with standard K8s namespace access and ec2:CreateTags permission can tag any security group. One protecting a database, an admin bastion, an internal service with the controller's expected tags, create a matching Ingress, and watch the controller open that security group to 0.0.0.0/0.
The Attack in Four Steps
Prerequisites: K8s namespace access (standard developer access on a shared EKS cluster), ec2:CreateTags permission (commonly granted for resource tagging workflows), knowledge of the victim SG ID (discoverable via read-only ec2:DescribeSecurityGroups), and the cluster name (visible in kubeconfig or instance metadata).
Step 1: Tag the victim security group with the controller's expected tags:
aws ec2 create-tags \
--resources sg-victim123 \
--tags \
Key=elbv2.k8s.aws/cluster,Value=prod-cluster \
Key=ingress.k8s.aws/stack,Value=attacker-ns/attacker-ingress \
Key=ingress.k8s.aws/resource,Value=ManagedLBSecurityGroup
Step 2: Create an Ingress resource in the attacker's namespace:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: attacker-ingress
namespace: attacker-ns
annotations:
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP":22}]'
alb.ingress.kubernetes.io/inbound-cidrs: 0.0.0.0/0
spec:
rules: []
Step 3: The controller reconciles. It queries EC2 for security groups with those tags. It finds the victim SG. It applies the Ingress annotations.
Step 4: The victim security group now has an inbound rule: port 22 from 0.0.0.0/0. SSH to every instance using that SG is open to the internet.
The controller did what it was designed to do. The tag was the only signal it used to determine ownership. The tag was attacker-controlled.
What Stave Can and Cannot Detect
The software vulnerability: tag-based resource identification without additional ownership verification is in the Load Balancer Controller code. That is out of scope for infrastructure configuration analysis. Stave does not inspect controller logic.
The attack outcome where a security group with an unrestricted inbound rule is an infrastructure configuration state. That is evaluated by Stave.
Before attack:
CTL.VPC.SG.UNRESTRICTED.001 → COMPLIANT
(victim SG has no 0.0.0.0/0 rules)
After attack:
CTL.VPC.SG.UNRESTRICTED.001 → NON_COMPLIANT
(victim SG has 0.0.0.0/0 on port 22)
The control does not know whether the unrestricted rule was created manually, by the controller legitimately processing a real Ingress, or via this attack. It evaluates the state. The state is wrong. The finding fires.
The invariant:
No security group may allow unrestricted inbound access (0.0.0.0/0) on sensitive ports.
The attack bypasses every upstream control such as RBAC, IAM policy review, change management and directly produces a state that violates this invariant. Continuous evaluation against the invariant catches the violation at the next snapshot, regardless of the attack path.
The Compound Chain
The full attack requires two conditions beyond the SG misconfiguration: the K8s namespace access to create Ingress resources, and the ec2:CreateTags permission to tag security groups. Both are addressable with infrastructure controls.
In Stave's compound chain model:
chain: eks_sg_tag_confusion_path
members:
- CTL.K8S.RBAC.WILDCARD.001 (Ingress creation unconstrained)
- CTL.IAM.EC2.CREATETAGS.001 (ec2:CreateTags granted broadly)
- CTL.VPC.SG.UNRESTRICTED.001 (outcome: unrestricted inbound rule)
postconditions: [network_access, lateral_movement]
CTL.VPC.SG.UNRESTRICTED.001 alone fires on the outcome. The chain fires when all three conditions are simultaneously true. The attack path is open end to end. Fixing any one member breaks the chain.
The Preventive Controls
Three infrastructure controls address different points in the attack path:
Restrict the ALB controller's IAM role at the resource level:
{
"Effect": "Allow",
"Action": [
"ec2:AuthorizeSecurityGroupIngress",
"ec2:RevokeSecurityGroupIngress"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"aws:ResourceTag/elbv2.k8s.aws/cluster": "${CLUSTER_NAME}"
}
}
}
This limits the controller's SG modification permission to security groups already tagged with the cluster name. A condition the attacker's victim SG would not satisfy before the tag manipulation. The attacker can still add the tag, so this is defense-in-depth, not a complete fix.
Restrict ec2:CreateTags via SCP on controller-owned tag keys:
{
"Effect": "Deny",
"Action": "ec2:CreateTags",
"Resource": "arn:aws:ec2:*:*:security-group/*",
"Condition": {
"ForAnyValue:StringLike": {
"aws:TagKeys": [
"elbv2.k8s.aws/*",
"ingress.k8s.aws/*"
]
}
},
"NotPrincipalArn": [
"arn:aws:iam::ACCOUNT_ID:role/alb-controller-role"
]
}
This prevents anyone except the controller's own IAM role from applying controller-owned tags to security groups, closing the tag manipulation vector at the IAM layer.
Restrict Ingress creation to trusted namespaces via K8s RBAC:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: ingress-creator
rules:
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses"]
verbs: ["create", "update", "patch"]
# Restrict to specific namespaces via RoleBinding,
# not ClusterRoleBinding
If developers cannot create Ingress resources with alb.ingress.kubernetes.io/* annotations outside approved namespaces, they cannot trigger the controller even with a correctly tagged SG.
Why This Report Has No E2E Test Yet
This finding has no E2E test in the current suite. The reason: the observation contract for this attack requires modeling the K8s Ingress creation and the controller's reconciliation as connected events, not just the SG state. The SG state alone has_unrestricted_ingress: true is already covered by existing CTL.VPC.SG.UNRESTRICTED.001 E2E tests.
The missing piece is a test fixture that models the full compound chain: RBAC permissiveness + broad ec2:CreateTags + resulting SG misconfiguration as a causally connected sequence. That requires extending the EKS observation contract to include Ingress resource permissions work that is on the roadmap.
What the existing control coverage proves: the attack outcome (an unrestricted SG) is detected within one assessment cycle of the attack occurring. The compound chain detection is additive.
Checklist
For any EKS cluster running the AWS Load Balancer Controller:
- ALB controller IAM role scoped with resource-level conditions on SG modification
-
ec2:CreateTagsrestricted via SCP for controller-owned tag key prefixes (elbv2.k8s.aws/*,ingress.k8s.aws/*) - Ingress creation restricted to approved namespaces via K8s RBAC
-
CTL.VPC.SG.UNRESTRICTED.001running on every SG in the cluster's VPC -
CTL.K8S.RBAC.WILDCARD.001running to detect over-permissive RBAC - Security group changes generate findings at the next snapshot. Review any new unrestricted rules regardless of source
The controller opened the port correctly. The tag said it should. Nobody checked who put the tag there.
How this relates to existing compliance mods
A framework benchmark in turbot/steampipe-mod-aws-compliance will catch the resulting unrestricted security group every time. There are dedicated controls for "no SG with 0.0.0.0/0 ingress on database ports" across CIS, AWS Foundational Security Best Practices, and others. What it can't catch by construction is the cause path: a K8s namespace + ingress annotation + service tag on a controller-managed SG. The framework benchmark sees the SG; it doesn't see the K8s RBAC + IAM tagging policy + controller behaviour that produced the SG. Running the framework benchmark catches the what fast; Stave's cross-asset chain controls (K8s RBAC + IAM tag permissions + controller-managed SG) catch the cause so the same SG doesn't regenerate after remediation. Two layers, same incident, both running in Powerpipe. Comparison: aws-compliance-mod.
HackerOne #1238482 AWS Load Balancer Controller tag confusion enabling security group modification. The software vulnerability is in the controller; the infrastructure outcome is detected by Stave via CTL.VPC.SG.UNRESTRICTED.001. E2E test for the compound chain is pending EKS observation contract extension.
Top comments (0)