<!--
.. title: Part 20: The front door, built out of responsibilities
.. slug: late-to-kube-part-20-the-front-door-built-out-of-responsibilities
.. date: 2026-10-18 21:05:35 UTC
-->

## TL;DR

- Ingress is a chain of responsibilities, not one box: a BGP-capable router so I never worry about which node to point at, a firewall doing what firewalls do best (outside the cluster, on purpose), and CloudFront carrying everything HTTPS from the internet's edge.
- BGP-capable router so I never worry about which node to point at; [Cilium](https://cilium.io) announces service IPs, the announcement is the contract.
- Firewall stays outside [Kubernetes](https://kubernetes.io): admission control is *its* job, in the place it's built for.
- CloudFront carries everything HTTPS from the internet's edge; its documented IP ranges mean "only CloudFront may talk to the firewall's ingress port" is a generatable, versionable rule.
- [Traefik](https://traefik.io) is the ingress controller inside - chosen on a track record, not a benchmark - with [cert-manager](https://cert-manager.io) + ACME for certificates.

## The question every ingress answer gets wrong

Every ingress question eventually becomes "which box do I point at?" Load balancer IPs, node ports, ingress controllers with their own published addresses - and every one of those answers hides a question nobody asked: who keeps that address true when the cluster changes?

The usual answer is "you do, manually," or "a cloud controller does, somewhere." I wanted neither. My answer is that the question should dissolve entirely: nothing should have to *know* which node to point at, because the network should already know. That's what BGP buys, and it's why the announcement from last post belongs to this story rather than the fabric one.

## The announcement, remembered

To recap the shape: Cilium announces the cluster's service addresses to my router over BGP. The router learns them the way it learns any route, and the cluster becomes just another prefix-holder on my network. From that moment, "which node?" stops being a question anyone answers - the routing table answers it, continuously, and updates itself when nodes come and go. A node dies, the routes adjust, traffic follows the survivors. Nobody re-points anything.

This is the part I keep having to defend, because it sounds exotic: a BGP-capable router in a homelab. It shouldn't sound exotic. It's one more dynamic-routing neighbor, and my firewall handles it for breakfast. What it removes is an entire category of operational debt - the hardcoded edge address, the DNS record that lags reality, the "wait, which node was that again?" archaeology. The announcement is the contract, and contracts don't need maintenance, only honoring.

## The firewall's job, done outside

Here's a design decision I feel strongly about: the firewall stays outside Kubernetes. Not "connected to it" - *outside* it, as in not running on it, not managed by it, not a workload in it.

The reason is the same principle that's run through this whole arc: every component should do the one job it's built for, in the place it's built for. A firewall's job is admission control at the border - deciding what traffic is allowed to enter, from where, to what. That job belongs to the component whose whole existence is that border, the one that's been doing it since before the cluster existed and will keep doing it after the cluster is rebuilt from a git repo and a PXE stick. If I put the firewall *inside* the cluster, its fate is coupled to the thing it's supposed to be protecting - and a border guard who dies with the fortress isn't a border guard, it's a decorator.

So: packets arrive at the firewall (which learned where the cluster lives via BGP, above), and the firewall does what it does best - the rules. What's allowed in, from whom, to which port. Not the cluster's job. Not the ingress controller's job. The firewall's job, done in the place the firewall is.

## Traefik, and the certificates

The last link in the chain is the ingress controller, and here I'll confess to having a favourite: Traefik. This is the one component I chose on familiarity first and ideology second - I've used it extensively before, it works really well, and that track record means more to me than a benchmark. [NGINX](https://nginx.org) is a fine web server wearing an ingress costume; Traefik was designed as an edge router from day one, and it shows in how its configuration model maps to what an ingress actually does: routes, entry points, middlewares, services. I wanted an ingress controller I'd stop thinking about, and Traefik is the one I already had stopped thinking about.

Certificates are where it gets interesting, because the ACME challenge for DNS-01 validation needs Route53 credentials, and this estate doesn't do long-lived static keys - post 12 established that hill. The solution is credproxy: a home-backed IRSA-simulation, my own service that mints credentials the way ECS and EKS do, so cert-manager can ask Route53 to answer challenges without anyone holding a permanent key. That mechanism is a chapter of its own, and a fun one - Cilium plays a big role in making it work properly - so I'll leave it teased here rather than crammed into a paragraph.

## CloudFront, the front door's front door

And now the piece that surprises people: all things HTTPS come in via CloudFront, which I know sounds like the most cloud-native thing in what is supposedly a self-hosted story. Stay with me.

CloudFront does three things for this setup, and none of them require me to hand anything over:

1. **Edge security.** TLS termination, DDoS absorption, the world's largest chokepoint standing between the internet and my house. CloudFront's IP ranges are public and well documented - which means my firewall rule set can be precise: the *only* source allowed to talk to the firewall on the ingress port is CloudFront. Everything else is dropped at the border, unlogged, uncounted. The internet never talks to my house directly; it talks to Amazon, and Amazon talks to me.
2. **IaC integration.** CloudFront fits into the same IaC automation as everything else in the estate - distributions, behaviors, certificates, all of it in code, reviewed, applied, versioned like every other change I make. The edge is declarative too. My IaC story is a two-tool settlement, and it's deliberate: CloudFormation where I want infrastructure deployed and then *forgotten* - it's a service, the stack is maintained by AWS, and I never think about state again - and Pulumi where I want real code, because infrastructure written in an actual language gets the same treatment as any other code: functions, types, review, refactor. Yes, Pulumi has state management too - the difference is it's a detail of the tool, not the language I'm forced to think in. What I actually can't stand is HCL: a bespoke DSL pretending to be configuration while failing at both jobs. HashiCorp, once again, sits this one out. OpenTofu is a fine fork, but the moment anyone tells me HCL is a solved problem I remember my last skirmish with a `for_each` expression, and my stomach settles the debate.
3. **One documented source.** Because the IP ranges are published, "only CloudFront may connect" isn't a hope, it's a rule I can generate, version, and diff. When Amazon publishes new ranges, my automation picks them up. The front door's guest list writes itself.

The result is a chain, and each link owns exactly one thing: **CloudFront** owns the edge - TLS, DDoS, and the guest list. **The firewall** owns admission - what comes in, from whom, to where. **BGP** owns the addressing - where the cluster's services actually live, updated in real time. **Cilium** owns the fabric - getting a packet from the edge of the cluster to the right pod. **Traefik** owns the routing - path, host, service, inside the cluster where HTTP actually becomes a request to a workload.

Every component does the one job it's built for. No box knows more than it needs to. And when a piece fails, the blast radius is one responsibility, not the whole door.

```mermaid
flowchart LR
    NET["The internet"] --> CF["CloudFront<br/>TLS 1 - DDoS absorption - guest list"]
    CF -->|"only CloudFront may talk<br/>to the firewall's ingress port"| FW["Firewall<br/>admission"]
    FW --> ROUT["Router<br/>routes learned via BGP"]
    ROUT --> CIL["Cilium<br/>the fabric"]
    CIL --> TR["Traefik<br/>TLS 2 - routing"]
    TR --> POD["Workload"]
```

One detail worth pausing on: the certificate story reaches backwards through this whole chain. Traefik terminates TLS *again* inside the cluster (cert-manager + ACME via credproxy keeps those certs valid), while CloudFront terminates it at the edge - so the connection is encrypted end to end, but it's terminated twice, once at Amazon's border and once at mine, each side with its own certificates and its own renewal story. That's not accidental duplication; it's the chain-of-responsibility model applied to TLS itself.