This page is generated from skills/eks-ingress-migration/references/ingress-discovery.md. Edit the source, not this page.
Ingress Discovery
Rating model: Express every finding as Impact 0–5 using the Impact Indicator rubric.
Presence is decided by estate state; severity by priority order. Classify the estate first, then rate only what exists:
- Absent controller / empty estate / orphaned dead config = Non-event (0) — nothing to migrate (an unbuilt or abandoned road); it never inflates a finding. Carve-out — control-plane exposure survives zero routes: a reachable, running controller on a known-CVE/EOL version is a security finding even with zero Ingress objects. Example: ingress-nginx's validating admission webhook (CVE-2025-1974, CVSS 9.8, fixed v1.11.5 / v1.12.1) is exploitable from the pod network with no Ingress configured and the webhook is on by default. "Zero routes" bounds data-plane exposure, never control-plane exposure.
- Present-but-broken controller (CrashLoopBackOff / unreachable) splits by whether it has bound routes — bound routes = Ingress objects whose
spec.ingressClassName(or the deprecatedkubernetes.io/ingress.classannotation) matches this controller's class, i.e. routes this controller is responsible for serving:
- With bound routes → suspected active outage. For an in-pod data plane (ingress-nginx) the pod is the data plane — all replicas down means those routes are down now. Flag it urgently and separately, outside the migration score (migration difficulty is the wrong lens for a live outage). Do not fold it into a silent tech-debt 1.
- With zero bound routes → tech debt (1) + a mandatory cleanup note (deployed then abandoned; the owner must fix or remove it).
- Healthy controller serving live traffic anchors the business and security dimensions at full weight. A healthy migration-target controller (e.g. AWS LB Controller) with nothing bound to migrate is 0 effort — do not deduct.
Verify before you downgrade (conservative default). "Serves no live traffic" must be evidenced (read-only), not inferred from pod status: for ingress-nginx confirm all replicas are down (one crashlooping replica while others serve is still live); for the AWS Load Balancer Controller the data plane is the ALB/NLB, which keeps serving registered targets while the controller pod is down or even uninstalled — check load-balancer/target-group state (
aws elbv2 describe-load-balancers,aws elbv2 describe-target-health --target-group-arn <arn>forhealthytargets, and CloudWatchRequestCount/ActiveConnectionCounton the LB), not the pod. If you cannot verify zero traffic, treat the estate as live. Where read-only evidence points to a state, say so in the report.Priority order (sets severity and breaks ties — it does not gate presence): (1) business logic / revenue — the live traffic at stake · (2) security / reputation · (3) effort. Effort is NOT a severity driver — how hard a fix is depends on who implements it (trivial for an expert, hard for a novice), so never raise or lower Impact by remediation effort; if effort is mentioned, label it an operator note. Security findings anchor on exposure / blast radius, business findings on live traffic; priority order breaks ties, it does not zero out a real security exposure just because the business traffic behind it is small.
Three independent dimensions stack (they do not override each other): a single controller/route can carry (a) a migration-difficulty deduction (config complexity), (b) a tech-debt deduction (present-but-broken with zero bound routes = +1; a broken controller with bound routes is instead a suspected active outage flagged outside the score — not a stacked deduction), and (c) a security deduction (CVE/EOL). Priority order ranks the dimensions within one finding; stacking means each of the three gets its own row in the Score Breakdown and they add up.
Band mapping is a starting point — 🟢 0 / 🟡 1–2 / 🟠 3–4 / 🔴 5 — but the Impact Indicator criteria set the final score (e.g. an easy-to-deploy prerequisite stays 🟡 low even if it blocks a path). All checks are read-only (
kubectl get/describe,aws … describe/list).
Purpose
Discover all ingress controllers, IngressClass resources, and Ingress objects in the cluster.
Checks to Execute
1.1 — Ingress Controllers Installed
What to check:
- Deployments/DaemonSets running ingress controllers
- Common controllers: nginx-ingress, AWS LB Controller, Traefik, HAProxy, Istio, Contour, Kong
How to check:
- List Deployments across all namespaces → filter for ingress-related names
- List DaemonSets across all namespaces → filter for ingress-related names
- Check namespaces:
ingress-nginx,kube-system,aws-load-balancer-controller - List pods with labels:
app.kubernetes.io/name=ingress-nginx,app.kubernetes.io/name=aws-load-balancer-controller
Impact (per Impact Indicator — mind presence vs. absence, and verify before downgrading):
- 🟢 0 (Non-event): No controller found / absent / empty estate — nothing to migrate (unbuilt road). Contributes 0; never rate this as a finding. If Ingress objects exist without any controller, see the orphaned-config handling in
report-generation.mdStep 1 (still 0, but emit the Migration Crew Alert note). - ⚠️ Active outage (NOT a migration-score item): controller present but broken (
CrashLoopBackOff/ImagePullBackOff/ unreachable) AND Ingress objects are bound to it. For an in-pod data plane (ingress-nginx) those routes are down now — surface it as an urgent flag, separately and outside the 0–100 score (a live-outage question, not a migration-difficulty one). Verify first: for ingress-nginx confirm all replicas are down (a multi-replica controller can serve while one pod crashloops); for the AWS LB Controller check the ALB/target-group state (the ALB keeps serving while the pod is down). If you cannot verify zero traffic, treat it as live. - 🟡 1 (Tech debt): Controller present but broken with zero bound routes — deployed then abandoned. Deduct 1 and emit a mandatory cleanup note: it carries no live traffic but can mis-align/mis-configure other systems, so the cluster owner must fix or remove it. This is not a migration-difficulty rating; if it also has complex config, that complexity is scored separately under its own categories.
- 🟢 0 / 🟡 1–2 (Low): Single healthy modern controller. A healthy migration-target controller (AWS LB Controller v2.x) with nothing bound to migrate is 0 effort — do not deduct; a healthy controller you are migrating from (e.g. nginx) that serves live routes is 🟡 1–2.
- 🟠 3–4 (Medium): Multiple controllers, or a legacy controller (nginx-ingress, ALB Ingress Controller v1). A broken controller still counts as a present controller for the "multiple controllers" assessment; its brokenness is scored by the rows above, not double-counted here.
- Security carve-out: a reachable known-CVE/EOL controller is a security finding even with zero routes (control-plane exposure) — rate it under §1.4 (Controller Currency, EOL & CVE Exposure), not here.
- ⬜ Unknown: Cannot determine controller health — state what to check and why.
1.2 — IngressClass Resources
What to check:
- IngressClass resources defined in the cluster
- Default IngressClass annotation (
ingressclass.kubernetes.io/is-default-class: "true") - Whether Ingress resources reference a specific IngressClass
How to check:
- List IngressClass resources (networking.k8s.io/v1)
- Check for default class annotation
- Cross-reference with Ingress resources'
spec.ingressClassName
Impact (per Impact Indicator — mind presence vs. absence; the behaviors below are version-specific — state them correctly):
- 🟢 0 (Non-event): No controller installed — IngressClass findings are moot (nothing reconciles them, whether or not Ingress objects exist); route through the
report-generation.md§1.0 short-circuit. Also 0 for an empty estate (no IngressClass, no controller, no Ingress). - 🟡 1–2 (Low): IngressClass defined, a default set, and Ingress resources reference it explicitly via
ingressClassName. - 🟠 3–4 (Medium): IngressClass exists but Ingress resources use the deprecated
kubernetes.io/ingress.classannotation instead ofingressClassName; or Ingress resources exist with no class and no default IngressClass set. Spec-compliant controllers are permitted to ignore Ingresses without a class (IngressClass API: "implementations may choose to ignore Ingresses without a class specified"); ingress-nginx serves them only if started with--watch-ingress-without-class=true(defaultfalse) or if an IngressClass is marked default (ingressclass.kubernetes.io/is-default-class: "true"). The real risk is silent non-reconciliation — routes quietly not served — not an "ambiguous/implicit default". - 🔴 5 (High): Multiple IngressClasses marked default. On all EKS-supported Kubernetes versions (v1.25+) the
DefaultIngressClassadmission plugin silently assigns the newest default (bycreationTimestamp, alphabetically-lowest name as tiebreak) to a classless Ingress — admission succeeds, it does not reject. The real risk is therefore silent misrouting / cutover ambiguity: a classless Ingress quietly binds to a class nobody intended, so traffic can land on the wrong controller during a cutover. This is the source-of-truth behavior, verified in theDefaultIngressClassadmission-plugin code across tags v1.25 → v1.36 and its merge PR kubernetes/kubernetes#110974. (Historical note — Kubernetes ≤ v1.24 only: the plugin rejected creation of a classless Ingress while multiple defaults existed; that behavior was removed in v1.25.0 by the PR above. The kubernetes.io Default IngressClass concept page still describes the old rejection behavior — it is stale for the versions this skill targets and must not be read as a live EKS outcome.) Either way the remediation is identical: keep exactly one default IngressClass. - ⬜ Unknown: Cannot determine IngressClass usage.
1.3 — Ingress Resource Inventory
What to check:
- Total Ingress resources across all namespaces
- Which namespaces have Ingress resources
- Ingress resources without an IngressClass (will use default)
How to check:
- List all Ingress resources (networking.k8s.io/v1) across all namespaces
- Count per namespace
- Check each for
spec.ingressClassNameorkubernetes.io/ingress.classannotation
Impact (per Impact Indicator):
- 🟡 1–2 (Low): All Ingress resources have explicit IngressClass, manageable count (<50)
- 🟠 3–4 (Medium): Some Ingress resources missing IngressClass, or high count (50-200)
- 🔴 5 (High): >200 Ingress resources, or many without IngressClass assignment
- ⬜ Unknown: Cannot list Ingress resources
1.4 — Controller Currency, EOL & CVE Exposure
What to check (read-only):
- The container image tag/version of each ingress controller.
- Whether that version is end-of-life / unsupported or carries known CVEs.
- For ingress-nginx specifically: whether snippet annotations are enabled (injection surface).
How to check (read-only):
kubectl get deploy <controller> -n <ns> -o jsonpath='{.spec.template.spec.containers[0].image}'— extract the version tag for every controller found in 1.1.- Compare each version against the project's supported/EOL matrix.
- For ingress-nginx, read the controller ConfigMap:
kubectl get cm <controller> -n <ns> -o jsonpath='{.data.allow-snippet-annotations} {.data.annotations-risk-level}'. - Admission-webhook exposure (required for the control-plane CVE band below): confirm whether the validating admission webhook is actually present and reachable — do NOT assume from the version alone. Run
kubectl get validatingwebhookconfigurationsand look for the ingress-nginx entry (default nameingress-nginx-admission); cross-check the controller Deployment args and theingress-nginx-controller-admissionService. These object names are built from the chart's fullname, not the bare release name: it is the release name when that already contains the chart name, otherwise<release>-ingress-nginx. So the VWC isingress-nginx-admissionfor a release namedingress-nginx, but<release>-ingress-nginx-admissionotherwise (and the Service likewise…-controller-admission) — match by the controller's owner references / labels, not just the literal default names. The webhook ships enabled by default in Helm installs. Decide exposure as follows and record the exact state (webhook: exposed | not-exposed | unverified) in the finding — do not collapse it to a bare present/absent flag:- Exposed — the controller Deployment is started with a non-empty
--validating-webhook=<address>arg (e.g.--validating-webhook=:8443, the Helm default), so the webhook server is actually listening on the controller pod (pod-IP:8443). This is the CVE-2025-1974 attack surface: the exploit reaches the pod's webhook server directly over the pod network, so on a< v1.11.5 / < v1.12.1controller the control-plane path is live regardless of route count (🔴 5 band). A presentingress-nginx-admissionVWC and a backedingress-nginx-controller-admissionService corroborate that the webhook is also wired into the API server, but it is the listening server that makes it exploitable. - Not exposed — the necessary condition is that the webhook server is not listening: the controller Deployment has no (or an empty)
--validating-webhook=<address>arg. That flag takes a service-address string, not a boolean — there is no--validating-webhook=falseform; when the value is absent/empty the server is simply never started. An absent VWC and/or an absent admission Service are corroborating signals (a Helmcontroller.admissionWebhooks.enabled=falseinstall drops the VWC, the Service and the arg together) but do NOT, on their own, close the path: the VWC and admission Service are API-server-side plumbing, whereas the exploit targets the pod's listening webhook server at pod-IP:8443 directly. Only a confirmed server-not-listening (arg absent/empty) drops the 🔴 5 band; then note it and fall back to the data-plane assessment. - Unverified (fail closed) — if you cannot confirm the server is not listening — the controller Deployment args are
Forbidden/unreadable, or only a partial read succeeded (e.g. you could read the VWC or the Service but not the args) — you have not verified the necessary condition, so default to exposed for a< v1.11.5 / < v1.12.1controller (the webhook is on by default; a missing read must not silently drop the 🔴 5 band). This takes precedence over any VWC/Service-absent observation: an absent VWC with unreadable controller args is still Unverified → exposed, because the absence of the API-server plumbing does not prove the pod's webhook server is down. Score it as exposed and flag "webhook state unverified — assumed exposed; re-check the controller args with cluster-admin."
- Exposed — the controller Deployment is started with a non-empty
Deterministic version facts (cite in the finding):
- ingress-nginx
< v1.9.0is affected by CVE-2023-5043 / CVE-2023-5044 (configuration-snippet / permanent-redirect annotation injection → arbitrary command execution / privilege escalation). Treat any controller< v1.9.0as a security finding. - Since v1.9.0,
allow-snippet-annotationsdefaults tofalseandannotations-risk-leveltoHigh. If a cluster setsallow-snippet-annotations: "true", it re-opens the injection surface — flag it. - AWS Load Balancer Controller: v2.7.2+ for the ALB Ingress path; ≥ v2.13.3 (L4) / ≥ v2.14 (L7) for Gateway API.
Impact (per Impact Indicator — anchor on EXPOSURE / blast-radius for security, and on live traffic for business; never on patch effort):
A CVE/EOL finding's severity comes from what it exposes, not how hard the upgrade is. Two exposure surfaces exist and are independent:
- Data-plane exposure scales with the live traffic the controller serves (business-critical routes > internal > none).
- Control-plane exposure is the controller's own attack surface (e.g. ingress-nginx's validating admission webhook) and exists whenever the controller process is running and reachable — regardless of how many routes it serves. "Zero routes" does NOT imply "not exploitable."
- 🟢 0 (Non-event): The controller is absent, or fully down — all replicas
CrashLoopBackOff/unreachable, so neither the data plane nor the admission webhook is serving. Record as an informational note, deduct 0. (A broken-with-zero-routes controller still earns its separate §1.1 tech-debt point — that is not a CVE deduction; a broken-with-bound-routes controller is an active outage, handled in §1.1.) - 🟡 1–2 (Low): EOL/CVE controller serving only non-critical / internal / low-traffic routes, with no known control-plane RCE; snippet hardening intact.
- 🟠 3–4 (Medium): A controller is behind/approaching EOL, or
allow-snippet-annotations=trueis set on a current controller (injection surface re-opened), serving routes of moderate business importance. - 🔴 5 (High): Either (a) an EOL/unsupported controller with known CVEs (e.g. ingress-nginx
< v1.9.0) actively serving business-critical / revenue / public-facing live traffic; or (b) a running controller exposing a known control-plane RCE regardless of route count — e.g. ingress-nginx< v1.11.5 / < v1.12.1with the validating admission webhook exposed (or unverified — see the tri-state in "How to check" step 4; unverified defaults to exposed) (CVE-2025-1974, CVSS 9.8): exploitable from the pod network with zero Ingress objects, leading to cluster-wide Secret disclosure / takeover. A healthy, zero-route vulnerable controller is a critical finding here — not a non-event. - ⬜ Unknown: Cannot read controller image/version — state what to check.
Every controller version found MUST appear in the report (Current Configuration + Ingress Discovery), with EOL/CVE status called out — do not roll multiple controllers into one line.
Remediation sequencing (SAFETY — do not get this wrong): setting
allow-snippet-annotations: falseis a breaking change for any Ingress currently using snippet annotations — the controller drops those routes and can cause immediate downtime. If snippet-using ingresses exist (cross-check §3.1), you MUST NOT recommend disabling it as an "urgent / Day-1 / immediate" action. Sequence it after those routes are migrated or redesigned. The recommendation wording must read "re-disable snippet annotations after migrating the snippet routes", never "urgent: set false now". The same applies to retiring an EOL controller that still serves live routes — migrate first, retire last.
1.5 — EKS Auto Mode Detection
What to check (read-only):
- Whether the cluster runs EKS Auto Mode (changes how load balancing is provided).
How to check (read-only):
aws eks describe-cluster --name <cluster> --query 'cluster.computeConfig'— Auto Mode is enabled whencomputeConfig.enabled = true(with managednodePools).- Recognize Auto Mode's managed load-balancing IngressClass:
spec.controller: eks.amazonaws.com/alb(parametersapiGroup: eks.amazonaws.com,kind: IngressClassParams); NLB vialoadBalancerClass: eks.amazonaws.com/nlb. This is distinct from the self-managed LBC (ingress.k8s.aws/alb).
Why it matters: on Auto Mode the ALB Ingress path needs no self-managed LBC install (it's built in); a eks.amazonaws.com/alb IngressClass is a managed controller, not a missing one. Gateway API L7 still requires the LBC ≥ v2.14 unless/until Auto Mode exposes it natively.
Impact (per Impact Indicator): informational — record Auto Mode status in Current Configuration; it does not by itself carry a migration impact, but it changes the Migration Options guidance.