<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title><![CDATA[notes // rudr]]></title>
    <description><![CDATA[Writing on cloud, infrastructure, distributed systems, and whatever broke this week.]]></description>
    <link>https://tldr.rudr.me</link>
    <image>
      <url>https://tldr.rudr.me/opengraph-image.png</url>
      <title>notes // rudr</title>
      <link>https://tldr.rudr.me</link>
    </image>
    <generator>tldr.rudr.me</generator>
    <lastBuildDate>Sat, 16 May 2026 14:30:00 GMT</lastBuildDate>
    <atom:link href="https://tldr.rudr.me/rss.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[en-US]]></language>
    <item>
      <title><![CDATA[The Host header: how nginx, ALBs, and Cloudflare all play the same game (differently)]]></title>
      <description><![CDATA[
I hit the same IP address twice. Different `Host` headers. Different responses.

```bash
$ curl -skI https://104.16.132.229/ -H "Host: cloudflare.com"
HTTP/2 301

$ curl -skI https://104.16.132.229/ -H "Host: github.com"
HTTP/2 403
```

Same IP. Same port. Same TLS handshake. Two completely different outcomes — because one header changed.

That header is `Host`. And once you understand how it works, you'll see it everywhere: in nginx configs, in ALB listener rules, in Cloudflare's routing logic, in every reverse proxy you'll ever operate. It's one of those things that's quietly running the show.

---

## How a request actually gets routed

Before DNS resolution, before TCP, before TLS — when you type `rudr.me` into a browser, the OS has to turn that name into an IP address. It asks a recursive resolver (usually your ISP's, or 8.8.8.8, or 1.1.1.1), which walks the DNS tree until it gets an A record back: a single IP.

That IP might serve one site. Or it might serve thousands.

When your browser opens a TCP connection to that IP and sends an HTTP request, it includes a `Host` header:

```
GET / HTTP/1.1
Host: rudr.me
```

This header is how the server on the other end knows which site you want. Not from the IP — the IP just got you to the machine. The `Host` header tells the machine *what you're looking for*.

This is called **virtual hosting**, and it's been in the HTTP spec since 1.1 (1997). One IP, many domains, routing by header. The entire modern web runs on it.

---

## What nginx does with it

In nginx, virtual hosting is explicit. You define `server` blocks, each with a `server_name` directive:

```nginx
server {
    listen 80;
    server_name rudr.me www.rudr.me;
    root /var/www/rudr;
}

server {
    listen 80;
    server_name api.rudr.me;
    proxy_pass http://localhost:3000;
}
```

When a request comes in, nginx reads the `Host` header and matches it against every `server_name`. First match wins. If nothing matches, nginx falls through to a default server — usually the first block defined, or one explicitly marked `default_server`.

This is permissive by default. Send `Host: anything.example.com` to an nginx server with no matching block, and it'll likely serve you *something* — the default. That's a deliberate design choice: fail open rather than fail closed.

You can make it strict by defining a catch-all that returns 444 (nginx's "drop connection" status):

```nginx
server {
    listen 80 default_server;
    server_name _;
    return 444;
}
```

But out of the box, nginx doesn't enforce that `Host` has to match anything specific.

---

## What an AWS ALB does with it

An Application Load Balancer operates at Layer 7 — it terminates HTTP/S, reads headers, and routes based on rules you define. Host-based routing is a first-class feature:

```
IF Host header is "api.rudr.me"
  THEN forward to target group: api-service

IF Host header is "rudr.me" OR "www.rudr.me"
  THEN forward to target group: frontend
```

ALBs evaluate rules in priority order. If no rule matches, the default action fires — which you configure explicitly (usually a fixed 404 or redirect).

The critical difference from nginx: an ALB's routing rules are defined in AWS, not in a config file. They're managed through the console, CLI, or Terraform. And because the ALB sits in front of your entire infrastructure, it's the first thing that sees the `Host` header — your backend never has to think about routing.

One thing ALBs do that surprises people: by default, they forward the original `Host` header to the backend unchanged. So if `api.rudr.me` hits your ALB and routes to an EC2 instance, that instance sees `Host: api.rudr.me` — not the ALB's internal hostname. You can override this with a target group attribute, but the default is pass-through.

I tested this when I had my own ALB running. Sending a `Host` header for a domain that wasn't configured in any rule just hit the default action — no response from an unexpected backend, no information leak. The ALB didn't try to be helpful. It just matched rules, and if nothing matched, it used the default. That's the difference in philosophy from nginx.

---

## What Cloudflare does with it

Cloudflare's IP space is shared across millions of sites. When you put your domain behind Cloudflare, your DNS A record points to a Cloudflare IP — the same IP potentially used by thousands of other domains. Cloudflare's edge network uses the `Host` header (and SNI during the TLS handshake) to figure out which customer's configuration applies to your request.

This is where my original curl experiment comes in:

```bash
$ curl -skI https://104.16.132.229/ -H "Host: cloudflare.com"
HTTP/2 301

$ curl -skI https://104.16.132.229/ -H "Host: github.com"
HTTP/2 403
```

`104.16.132.229` is a Cloudflare IP. When I told it `Host: cloudflare.com`, it knew that domain — and redirected me (301). When I told it `Host: github.com`, it had no configuration for that domain on that IP. Not a permissive fallback like nginx. Not a configured default action like an ALB. A hard 403.

Cloudflare is strict by design. Sending a `Host` it doesn't recognize means the request has no place to go. There's no leaking traffic to an unexpected backend, no serving a default page. The request is rejected.

This strictness is a security property. At Cloudflare's scale, being permissive would be dangerous — you'd risk routing requests to wrong customers, leaking responses, or creating ambiguity that could be exploited. So they fail closed.

---

## The pattern: same mechanism, different policy

All three systems — nginx, ALB, Cloudflare — use the `Host` header to route requests. The mechanism is identical. What differs is the policy:

| System | No matching Host? | Why |
|---|---|---|
| nginx | Serve default block | Fail open; operator's responsibility to harden |
| AWS ALB | Execute default action (usually 404/redirect) | Explicit fallback; infrastructure-defined |
| Cloudflare | 403 | Fail closed; multi-tenant security requirement |

Understanding this distinction matters when you're debugging. If a request lands somewhere unexpected, the question isn't "is the routing broken?" — it's "which policy fired, and why?"

---

## The debugging superpower

Once you know this, `curl -H "Host: ..."` becomes a tool you'll use constantly.

**Test a backend before DNS propagates:**
```bash
curl -sI http://NEW_SERVER_IP/ -H "Host: myapp.com"
```

**Verify which ALB rule fires for a given domain:**
```bash
curl -skI https://ALB_DNS_NAME/ -H "Host: api.myapp.com"
```

**Check if nginx is serving the right vhost:**
```bash
curl -sI http://localhost/ -H "Host: staging.myapp.com"
```

You're not waiting for DNS. You're not guessing what the load balancer will do. You're sending the exact signal the routing layer reads, and watching what comes back.

Senior engineers reach for this during deploys, during incidents, during DNS migrations. It's fast, it's precise, and it works against every HTTP server ever built — because the `Host` header has been required since HTTP/1.1.

---

## What's next from here

The `Host` header is the HTTP-layer version of this problem. TLS has its own version: **SNI** (Server Name Indication), which solves the same problem one layer down — how does a server know which certificate to serve before it's decrypted the request? Worth one short session if you want the full picture.

Kubernetes Ingress is the declarative version of everything nginx does here, at cluster scale. If you're heading toward DevOps/SRE work, you'll write Ingress rules constantly — and they're just Host-header routing with a YAML interface.

The header is small. The surface area it controls is not.]]></description>
      <link>https://tldr.rudr.me/notes/host-header-routing</link>
      <guid isPermaLink="true">https://tldr.rudr.me/notes/host-header-routing</guid>
      <category><![CDATA[DNS]]></category>
      <category><![CDATA[Routing]]></category>
      <category><![CDATA[Deployment]]></category>
      <dc:creator><![CDATA[Rudra Patel]]></dc:creator>
      <pubDate>Sat, 16 May 2026 14:30:00 GMT</pubDate>
    </item>
  </channel>
</rss>
