An Ingress controller is the first thing most outside traffic touches on your cluster, yet it is frequently deployed with defaults and then treated as a solved problem. Platform teams spend weeks locking down pods, service accounts, and admission policies, then hand the internet a single Layer 7 endpoint that terminates TLS, multiplexes every service, and often has zero limits on who or what can arrive. The result is that the most attackable surface in the cluster is the one most likely to be unexamined. This guide is for engineering leaders and platform engineers who want a defensible external exposure path, and it covers the decisions that matter before your first workload goes live behind an ingress.
Start by choosing the controller, not buying one
The Kubernetes Ingress resource is a spec, not an implementation. You still have to pick a controller, and that choice silently determines your security posture. The built-in ingress-nginx controller is popular because it runs in-cluster, exposes familiar config, and emits Prometheus metrics by default. Managed controllers on GKE and EKS offload TLS termination and load balancing to the cloud provider, which removes a set of concerns around node security groups but moves the control plane to its own console. Whichever you choose, force yourself to answer three questions in writing: where TLS terminates, what backend protocol reaches the service, and whether the controller logs are retained somewhere you can actually query during an incident.
A practical default for most teams is a non-default, in-cluster controller with a dedicated namespace and a small set of approved annotations, rather than letting every service owner add whatever middleware they found online. Treat the controller as a platform component with an owner, a version policy, and a reviewable config, exactly as you would any other control plane it sits next to. It is worth reading the Kubernetes Ingress documentation and the ingress-controller reference before standardizing, because the two pages settle most arguments quickly.
Terminate TLS with a policy, not a per-service scramble
The most common insecure state on an ingress path is mixed HTTP and HTTPS. Some services get a certificate, others are exposed on port 80 only, and the team discovers it when a compliance review or a pentest flags plaintext customer traffic. The fix is to make HTTPS the only legal state, and to let the platform enforce it instead of relying on each developer to remember. A cluster-wide HTTP to HTTPS redirect, configured once on the ingress controller, turns plaintext into a redirect rather than a service being exposed.
Certificate lifecycle is where most teams actually get breached or cut off: a cert expires, a reviewer flags it, and someone rushes the fix on Friday. Automate issuance and renewal with cert-manager so that a certificate is a declarative outcome your operator backs up, not a manual reminder. Store the CA and the signing key as separate Kubernetes resources, keep the issuer in a namespace only operators can write to, and never commit a private key to git. If you already have a GitOps setup, put the certificate resources in the same pipeline and let drift detection flag a missing renewal before expiry does.
An HTTPS-only ingress as the enforced default
The minimum viable policy is an ingress annotation that forces a redirect when a request arrives on port 80. What governs whether that annotation is actually applied is admission: use an admission webhook or a validating policy that rejects Ingress objects which omit TLS or which define a backend using cleartext. That single guardrail prevents the regressions that slip in on a late-night change long after the incident-response plan was written.
Put network policies in front of the exposed services
An ingress controller forwards to whatever Service you point at it, and a Service with no surrounding policy is reachable from anywhere in the cluster that can reach the controller. That is a meaningful expansion of blast radius: a small compromise in one namespace can pivot to any backend the controller proxies. The correction is to pair every external-facing Service with a NetworkPolicy that allows ingress only from the controller's namespace and egress only to the backends it actually needs to reach.
A minimal deny-by-default policy that only lets the ingress controller reach the backend looks like this:
apiVersion: networking.k8s.io/v1\nkind: NetworkPolicy\nmetadata:\n name: allow-ingress-only\nspec:\n podSelector:\n matchLabels:\n app: payments-api\n policyTypes:\n - Ingress\n ingress:\n - from:\n - namespaceSelector:\n matchLabels:\n kubernetes.io/metadata.name: ingress-nginx\n ports:\n - protocol: TCP\n port: 8080
The pattern above matters less than the principle: namespace-lock the controller, namespace-lock the backend, and let everything else fall through to a deny. If the network plugin your cluster runs does not enforce NetworkPolicy, treat that as a blocking gap before you expose anything, because every subsequent control in this list assumes east-west isolation is real.
Rate limiting and size caps before the application
TLS and certificates handle confidentiality, but they do nothing for abuse. A controller that accepts unbounded requests forwards them straight to your pods, and a burst of autoscaling pods under load is exactly how a small, cheap attack becomes an expensive outage. The ingress layer is the right place for coarse protective limits because it is the one point every request passes through. Configure request rate limits, connection concurrency limits, and a maximum request body size on the controller so that individual backends inherit a floor of protection without each team having to build their own.
Equally important is the default for unknown paths. A controller that returns a catch-all 200 for anything is a probe-friendly target; a controller that 404s unknown paths and logs them gives you early signal. Set short timeouts so a slow upstream cannot pin connections, and cap the number of connections per client so one consumer cannot monopolize the controller. These are simple, well-documented controller settings, and they convert a broad — and often fatal — class of low-effort abuse into ordinary log noise.
Watch for drift in the security state
The sharpest risk is not the configuration you deploy today; it is the config that drifts three months from now. Someone adds a new ingress for a side project, drops the TLS block out of expedience, or widens a NetworkPolicy to make a test pass and the change sticks. Treat the ingress path as something an automated check verifies continuously, not as a one-time hardening exercise.
A quick verification checklist
- Every Ingress resource enforces TLS and no backend is reachable on cleartext HTTP.
- Certificates are issued and renewed by cert-manager, with the signing key never stored in git.
- Each externally exposed backend has a NetworkPolicy that only the controller namespace can reach.
- Rate limits, connection caps, max body size, and short timeouts are set at the controller.
- Unknown paths return 404 and the controller logs are retained and searchable during an incident.
- A policy or webhook rejects Ingress objects that omit TLS, so regressions fail before they deploy.
These checks map cleanly onto the practices in a layered security review. If you already run automated admission and GitOps, wiring these into the same pipeline is a small change that pays back in the form of fewer late-night incident pages and a cleaner posture for any external audit that needs to see you enforce rather than merely recommend.
If you want a second pair of eyes on how traffic reaches your workloads before the next release, Secpros can review your GKE, ingress, TLS, and NetworkPolicy setup and return a short prioritized security plan: which controller settings to fix first, which backends are over-exposed, and how to make the checks permanent.