skip to content

A cluster runs two ingress controllers, one internal and one internet-facing. How does a given Kubernetes Ingress object end up served by exactly one of them, and what goes wrong if that selection is left ambiguous?

level: seniorimportance: should knowfreq 38%

answer

  1. Ingress → IngressClass name → spec.controller identifier
  2. default class annotation applied at CREATE, then frozen in the object
  3. two defaults → class-less Ingress creation rejected
  4. legacy kubernetes.io/ingress.class still wins in some controllers
  5. dangerous default = the public controller

basics

~20 s

Each controller installs an IngressClass whose spec.controller holds its own identifier. An Ingress selects one via spec.ingressClassName. If it is omitted, only a class annotated as default applies; with no default nothing serves the object, and with two defaults or a mismatch you get silent non-service or accidental public exposure.

solid answer

~50 s

Selection is a two-level lookup. Each controller ships an `IngressClass` object whose `spec.controller` is a fixed identifier like `k8s.io/ingress-nginx`. Every controller process serves only Ingresses pointing at a class whose `spec.controller` equals its own identifier — usually combined with a `--ingress-class` flag so two NGINX deployments can coexist with different class names. An Ingress opts in with `spec.ingressClassName: internal`. Omitting it falls back to whichever IngressClass carries the `ingressclass.kubernetes.io/is-default-class: "true"` annotation; the default is applied at **creation time** and written into the object, so later changing which class is default does not retarget existing Ingresses. Failure modes: no class and no default means nothing claims the object and `ADDRESS` stays empty; two defaults means the API server rejects creation of Ingresses without a class; and, worst, a default pointing at the internet-facing controller silently publishes an app meant to be internal. The legacy `kubernetes.io/ingress.class` annotation is deprecated but still honoured by some controllers and overrides the field — a nasty source of confusion.

code

yaml · 15 lines
yaml
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
  name: internal
  annotations:
    ingressclass.kubernetes.io/is-default-class: "true"
spec:
  controller: k8s.io/ingress-nginx
---
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
  name: public
spec:
  controller: k8s.io/ingress-nginx

go deeper

for a junior

Know that spec.ingressClassName picks the controller and that an IngressClass object connects that name to a controller.

for a middle

Explain the two-level lookup through spec.controller, the default-class annotation, and what happens when the class is missing or misspelled.

for a senior

Cover the create-time defaulting subtlety, the legacy annotation override, the accidental-public-exposure failure, and admission policy as the enforcement mechanism.

for a principal

Treat class selection as an exposure control: no ambiguous default, fail-closed defaults, policy-enforced allowed classes per namespace, and an auditable inventory of what is publicly reachable.

## The two-level lookup There are three names in play and conflating them causes most confusion. 1. **The controller identifier** — a fixed, domain-prefixed string compiled into the controller, e.g. `k8s.io/ingress-nginx`. It identifies the *implementation*. 2. **The IngressClass object** — a cluster-scoped resource with `spec.controller` set to one of those identifiers, plus an optional `parameters` reference to implementation-specific config. Its *name* (`internal`, `public`) is what users type. 3. **`spec.ingressClassName` on the Ingress** — names an IngressClass. So: Ingress → IngressClass name → controller identifier → the running controller that owns it. This indirection is what lets two deployments of the *same* implementation coexist — both have `spec.controller: k8s.io/ingress-nginx`, but each is started with a flag telling it which IngressClass name to watch, so `internal` and `public` classes route to different deployments. ## Defaulting An IngressClass may carry `ingressclass.kubernetes.io/is-default-class: "true"`. When an Ingress is created without `ingressClassName`, an admission controller stamps the default class name into the object. The subtlety that trips people: **defaulting happens at creation, and the value is persisted**. If you later mark a different class default, previously created Ingresses keep the old name — they were mutated once, not re-evaluated. Conversely, an Ingress created while no default existed has an empty `ingressClassName` forever and will not start working when you add a default later. It must be edited. If **two** classes are marked default, the API server cannot choose and rejects creation of class-less Ingresses with an error. That is annoying but safe; the dangerous configuration is a single default pointing at the public controller in a cluster where most workloads are internal, because forgetting one field then publishes a service to the internet with no error anywhere. ## The legacy annotation Before `ingressClassName` existed, selection used the `kubernetes.io/ingress.class` annotation. It is deprecated, but several controllers still honour it, and where both are present the annotation typically wins. Real clusters accumulate manifests carrying both, or Helm charts that set the annotation while the operator sets the field. Any audit of "who serves what" must grep for the annotation as well as the field, and the cleanup is to remove annotations and standardise on the field. ## Failure modes and how they present - **No class, no default.** `kubectl get ingress` shows an empty `ADDRESS`, `describe` shows no events, controller logs never mention the object. Silent. - **Class name typo.** Identical symptoms — the name does not resolve to any IngressClass. Since the field is a free string, nothing validates it at creation unless you add policy. - **Two defaults.** Creation of class-less Ingresses fails outright with a clear error. - **Wrong default — the serious one.** An internal app gets picked up by the internet-facing controller. Nothing errors; DNS may not even point at it, so it can lurk until someone finds the load balancer IP. This is the case worth naming in an interview. - **Both controllers claiming one object.** Possible when one controller is configured to watch class-less Ingresses (`--watch-ingress-without-class`) while another has the default. Two data planes serve the same host, and behaviour depends on which address DNS resolves to — giving intermittent, inconsistent results. ## Operating practice Make selection explicit and enforced. Set `ingressClassName` on every Ingress and lint for its absence in CI. Either run with **no** default class — so an omission fails visibly rather than defaulting somewhere — or, if you want a default, make it the *internal* class so mistakes fail closed rather than publishing. Add an admission policy (ValidatingAdmissionPolicy, Kyverno, Gatekeeper) that rejects Ingresses whose class is empty or not in an allowed list, and optionally restricts which namespaces may use the public class. That last rule is the practical answer to "how do you stop a team exposing something publicly by accident", since Ingress itself has no namespace-scoped permission model for classes. For visibility, `kubectl get ingress -A -o custom-columns` over `spec.ingressClassName` plus a check on the legacy annotation gives an inventory in one command; `kubectl get ingressclass -o wide` shows which controller each class maps to and which is default. ## Version note `IngressClass` and `spec.ingressClassName` are part of `networking.k8s.io/v1`, GA since Kubernetes 1.19, replacing the `kubernetes.io/ingress.class` annotation, which remains deprecated-but-honoured in several controllers.

  • You mark a different IngressClass as default. Why do existing Ingresses keep using the old one?
    Defaulting is a one-time mutation performed by admission when the object is created: the default class name is written into `spec.ingressClassName` and stored. Nothing re-evaluates it afterwards, so changing the default only affects objects created from then on. Retargeting existing objects requires editing them, which is exactly why explicit class names in source control are safer than relying on a default.
  • How would you prevent an application team from accidentally exposing a service through the internet-facing class?
    Enforce it in admission rather than convention: a ValidatingAdmissionPolicy or Kyverno/Gatekeeper rule that rejects Ingresses with an empty class and restricts the public class to an allowed set of namespaces or requires a specific label. Pair that with either no default class at all, or a default that points at the internal controller so a forgotten field fails closed instead of publishing.

saying these in an interview costs you the question

  • Thinking the IngressClass name and the controller identifier are the same string
  • Believing changing the default class retroactively retargets existing Ingress objects
  • Assuming an Ingress with no class is simply ignored by every controller — some watch class-less objects
  • Forgetting the legacy kubernetes.io/ingress.class annotation can override the field
  • Assuming two controllers cannot both serve the same host — they can, giving inconsistent behaviour

context