DevOps
The Auth Template That Trusted Its Caller: AccessKeyID Injection in EKS
Bala Paranj Dev.to (EN Zone)
2 views
✓ Human-authored analysis; AI used for formatting and proofreading.
When a user's kubectl command authenticates to an Amazon EKS cluster, three components cooperate to turn an AWS identity into a Kubernetes user:
The aws-iam-authenticator client signs an STS GetCallerIdentity request and sends the URL to the cluster as a presigned bearer token.
The cluster's webhook authenticator parses the URL, verifies the signature, and extracts the AWS principal's identity.
A configured identity-mapping template turns that AWS principal into a Kubernetes user. For example, arn:aws:iam::...:role/EksAdmin becomes the Kubernetes user eks-admin.
Bug lived in step 3. The mapping template historically supported the {{AccessKeyID}} substitution, like this:
- userARN: "arn:aws:iam::*:role/*"
username: "user:{{AccessKeyID}}"
groups:
- "system:masters"
The intent was reasonable: include the AWS access key ID in the Kubernetes username for audit correlation. The AccessKeyID came from the bug. The webhook parsed it from the client-supplied URL's query parameters X-Amz-Credential rather than from the STS response that AWS itself returned.
In a presigned URL, the query parameters are signed for delivery, not authenticated as content. AWS APIs normalise duplicate parameters by some specific rule (last-wins, case-insensitive, etc.); the aws-iam-authenticator URL parser used a slightly different rule. The mismatch is an HTTP parameter pollution primitive: an attacker adds a case-variant duplicate (x-amz-credential lowercase, when AWS expects mixed-case X-Amz-Credential), and the two parsers see different values for what is supposedly the same parameter.
Client sends:
X-Amz-Credential=ATTACKER_KEY/...
x-amz-credential=VICTIM_KEY/... (case-variant duplicate)
AWS STS sees: AccessKeyId=VICTIM_KEY
aws-iam-authenticator sees: AccessKeyID=ATTACKER_KEY
STS authenticates the request as VICTIM, returns "yes, that's a valid AWS principal." The mapping template substitutes {{AccessKeyID}} with ATTACKER_KEY. The Kubernetes user becomes user:ATTACKER_KEY. But the AWS-side authentication succeeded for VICTIM. A different user lands in the cluster than AWS authorised.
This is the disclosed bug behind Kubernetes HackerOne 1580493. The fix: substitute from server-derived values (SessionName, role ARN) instead of from anything the client influences.
The Fix is Two Words
The remediation is the smallest possible diff:
- username: "user:{{AccessKeyID}}"
+ username: "user:{{SessionName}}"
SessionName comes from the STS response. The AWS side computed, signed, and returned it. The client cannot influence it through the request. The template substitution is now consuming a value with clear provenance.
Or skip templating and use ARN-based mapping:
- userARN: "arn:aws:iam::*:role/EksAdmin"
username: "eks-admin"
When the matching is on the AWS principal's ARN (also from the STS response, also signed), the mapping is one-to-one and there's no client-supplied value left to inject.
The System Invariant
Identity-mapping templates in aws-iam-authenticator must not substitute
{{AccessKeyID}}. Any value drawn from client-controlled URL parameters is a parameter-injection primitive against the cluster's identity layer.
In Stave's observation schema, the identity-mapping state is captured under the cluster's auth block:
{
"id": "acme-eks-cluster",
"type": "k8s_cluster",
"vendor": "kubernetes",
"properties": {
"auth": {
"kind": "cluster",
"webhook": {
"provider": "aws-iam-authenticator",
"identity_mapping": {
"uses_access_key_id": true,
"templates": [
"user:{{AccessKeyID}}"
]
}
}
}
}
}
uses_access_key_id is the engine's verdict. It is true when any template string contains the {{AccessKeyID}} placeholder. The templates array carries the underlying evidence. The vendor field is kubernetes (not aws) because the asset is a Kubernetes-domain resource even though the cluster runs on AWS. Stave's vendor heuristic uses scope tags to filter applicable controls, and mis-tagging this as aws would silently exclude the control from running.
The Stave Control
id: CTL.K8S.AUTH.ACCESSKEYMAP.001
name: K8s Clusters Must Not Map Identity via AccessKeyID
severity: high
unsafe_predicate:
all:
- field: properties.auth.kind
op: eq
value: cluster
- field: properties.auth.webhook.identity_mapping.uses_access_key_id
op: eq
value: true
Two leaf clauses, both required. Severity high where the cluster's identity layer is one URL parameter away from impersonation, but the control fires on the configuration shape, not on a confirmed exploitation.
Why Z3 Doesn't Help
This is a presence check at the collector layer. The collector inspects the template strings for {{AccessKeyID}} and emits a boolean. CEL evaluates the boolean.
A reachability question "given the URL parser's normalisation behaviour and the STS response format, is there an HTTP encoding that yields a different AccessKeyID on each side?" would be Z3-shaped, but that's URL-parser semantics, not configuration semantics.
Reproducing The Detection
The repository ships a self-contained example at
stave/examples/eks-aws-auth-template-injection/:
go run ./examples/eks-aws-auth-template-injection before
Captured stdout:
=== before ({{AccessKeyID}} template) ===
status: NON_COMPLIANT total_assets=1 violations=1
CTL.K8S.AUTH.ACCESSKEYMAP.001 fired on 1 asset(s):
- acme-eks-cluster severity=high exposure_score=76.64
assertion: fires=true (expected) ✓
After the template is fixed:
=== after ({{SessionName}} + role ARN) ===
status: COMPLIANT total_assets=1 violations=0
CTL.K8S.AUTH.ACCESSKEYMAP.001: no findings
assertion: fires=false (expected) ✓
Why This Bug Slipped Past Reviews
The mapping template syntax itself was sanctioned in the upstream documentation. Operators who wrote {{AccessKeyID}} were following the example. The documentation didn't make the parser-mismatch risk visible. The substitution looked semantically similar to {{SessionName}}.
The deeper issue is the layer the value crosses. {{SessionName}} and the role ARN are values STS returns to the authenticator after signature verification. {{AccessKeyID}} is a value the authenticator reads back from the request URL which means it's already been consumed by AWS for signature verification, but the verification is about the request's integrity, not about the field's value being authentic. Two different verifications on two different layers.
This is a recurring pattern in cloud-provider auth flows: the client provides several values, the provider authenticates the request, then a downstream component reads one of those values back and treats it as authenticated by association. The downstream component's trust assumption is unjustified.
The Remediation
The minimal fix: replace {{AccessKeyID}} with {{SessionName}} in every identity-mapping template. The deeper fix: stop using template substitution for user identity at all. Use ARN-based exact matching:
mapRoles: |
- rolearn: arn:aws:iam::111122223333:role/EksAdmin
username: eks-admin
groups:
- system:masters
- rolearn: arn:aws:iam::111122223333:role/EksDeveloper
username: eks-developer
groups:
- acme:developers
ARN-based mapping has the property that there is no client-controlled substring in the username. The substitution surface vanishes; no parser-mismatch exists to exploit.
For clusters that legitimately need session-scoped usernames (rare, mostly for CI integrations where the IAM role is shared across many concurrent jobs), {{SessionName}} provides identifiers that come from the STS response and survive the parameter-pollution attack.
The Prevention Lesson
Three layers, in priority order:
Helm chart enforcement. The aws-auth ConfigMap is typically managed via a Helm chart or Terraform module. The chart's variable schema rejects any value matching {{AccessKeyID}} at template-render time. The chart fails to render rather than producing an unsafe ConfigMap.
Admission policy. For clusters where the ConfigMap is edited directly, OPA Gatekeeper or Kyverno watches the aws-auth ConfigMap and denies any update whose mapRoles / mapUsers content contains {{AccessKeyID}}. The deny fires at admission time before the misconfiguration reaches the cluster.
stave apply in CI against the post-deploy observation snapshot. The example shipped with this article is the template. PRs that introduce a cluster with uses_access_key_id: true produce exit code 3.
Checklist
Identity-mapping templates in aws-iam-authenticator do not contain {{AccessKeyID}} (use {{SessionName}} or ARN-based exact matching)
Helm chart / Terraform module validates the template content at render time
OPA Gatekeeper or Kyverno denies aws-auth ConfigMap updates that introduce {{AccessKeyID}}
stave apply runs in CI against post-deploy observations; PRs with uses_access_key_id: true fail
Cluster authentication audits explicitly verify the substitution sources for identity-mapping templates
The substitution looked safe because the syntax was documented. The bug was that the documented syntax trusted client-controllable input. The lesson is upstream of EKS: any auth template whose substitutions cross a trust boundary needs an explicit declaration of which values came from where.
The example at eks-aws-auth-template-injection is a self-contained Go program that loads two fixture snapshots, runs pkg/stave.Apply, asserts that CTL.K8S.AUTH.ACCESSKEYMAP.001 fires on the template-injection fixture and is silent on the remediated one, and exits zero when both assertions hold. Stave detects this pattern and 31 other H1-grounded scenarios from local AWS / EKS configuration snapshots, with no cloud credentials.
Read original: https://dev.to/bala_paranj_059d338e44e7e/the-auth-template-that-trusted-its-caller-accesskeyid-injection-in-eks-5h6o
← Previous
Borsa Verisiyle Çalışırken Sürekli Dönüp Dolaşıp Kullandığım 6 Python Kütüphanesi
Next →
Building a Schema With an AI Agent Without Naming a Single Column
Related
Comments0
No comments yet — be the first