> ## Content Index
> Fetch the complete content index at: https://blog.melardenio.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Cloudflare Tunnel 502 Bad Gateway: Removing a Dead Route, and What Access Hides From Scanners
- URL: https://blog.melardenio.com/cloudflare-tunnel-502-bad-gateway-removing-a-dead-route-and-what-access-hides-from-scanners/
- Published: 2026-10-13T14:20:13.000Z
- Updated: 2026-10-13T14:33:06.000Z
- Author: Rommell Ardenio
- Tags: Software & Homelab

I run seven public hostnames from one homelab through a single Cloudflare Tunnel. One of them had been returning a **502 Bad Gateway** for a while, and I only found out when I ran a security scan. This post covers what causes that 502, and a side effect of Cloudflare Access that surprised me during the same scan.

## One tunnel, many apps

A Cloudflare Tunnel runs one small daemon, `cloudflared`, on the server. It connects out to Cloudflare, so you don't open any ports on your router. Each public hostname is mapped to a local port in the tunnel's config. In my case the tunnel is locally managed, which means the routes live in `/etc/cloudflared/config.yml` on the server, not in the Cloudflare dashboard. If you're looking for a route in the dashboard and can't find it, check which kind of tunnel you have.

## Where the 502 came from

The hostname `demotripticket` was a demo copy of one of my apps. The tunnel still routed it to local port 3102, but nothing was listening on that port any more. Cloudflare could reach my server fine, and the server had nothing to hand it, so visitors got a 502\. A hostname in the tunnel config will keep returning 502 for as long as its target app is stopped or deleted.

The fix is two steps, in this order:

1. Delete the DNS record for the hostname in Cloudflare.
2. Remove its lines from the tunnel's `config.yml`, validate the file, and restart the tunnel.

I backed up the config first. Afterwards I checked that the tunnel was active with four connections, that the remaining six hostnames answered exactly as before, and that the demo hostname no longer resolved.

## What the scanner actually saw

I had run an OWASP ZAP baseline scan against all seven hostnames. Two of my apps sit behind **Cloudflare Access**. A request to either one returns a 302 redirect to the Access login page. So ZAP never touched those apps. It scanned the login page, and it reported things like a loose content security policy and cookie settings, which belong to Cloudflare's page and not to my code.

That is a real benefit. Access keeps the actual app invisible to anyone who isn't signed in, including scanners. It also means a scan of the public hostname tells you very little about the app behind it. To test the app itself you need to scan from inside, with a logged-in session.

## What I'd do again

- When you delete an app, delete its tunnel route and its DNS record in the same sitting.
- Take a backup of the config before editing it, and test afterwards.
- Treat a clean external scan of an Access-protected hostname as "I scanned the front door", not "the app is secure".

## How to tell which part is broken

A 502 from Cloudflare means the request reached your tunnel but the app behind it did not answer. Three checks on the server, in this order, find out why. The port 3102 below is the one from my story:

```
ss -ltnp | grep 3102             # is anything listening on the port?
curl -sI http://localhost:3102   # does it answer locally?
journalctl -u cloudflared -n 50  # what does the tunnel itself say?
```

If nothing is listening, the app is stopped or deleted, and the route is dead. If it answers locally but the site still shows 502, look at the service address in the config: `http` versus `https`, or `localhost` versus a container name.

## What a route looks like in config.yml

This is a generic example with made-up hostnames, not my real config:

```
tunnel: <TUNNEL-ID>
credentials-file: /etc/cloudflared/<TUNNEL-ID>.json

ingress:
  - hostname: app.example.com
    service: http://localhost:3000
  - hostname: old-demo.example.com
    service: http://localhost:3102   # nothing listens here: 502
  - service: http_status:404
```

The last rule is a required catch-all. When you clean up, remove the lines for the dead hostname and never the final `http_status:404` rule.

## Validate before you restart

```
cloudflared tunnel ingress validate
cloudflared tunnel ingress rule https://app.example.com
sudo systemctl restart cloudflared
```

The first command checks the file's syntax. The second shows which rule a given URL would match, which is a quick way to confirm a hostname still routes where you think it does. Add `--config /etc/cloudflared/config.yml` if cloudflared can't find the file by itself. These are the commands to use, and I'm not claiming I typed each one exactly this way. What I did record is the outcome: tunnel active with four connections, the remaining hostnames answering as before, and the removed one no longer resolving.

## Delete the app, the DNS record and the route together

The 502 only happened because the three pieces went out of sync: the app was gone, but the DNS record and the route stayed behind. Removing them in one sitting avoids that. Keep a copy of the config first, so a typo in the edit can be rolled back in seconds.

## Related reading

- [The Router Wasn't the Problem](https://blog.melardenio.com/the-router-wasnt-the-problem/)
- [Docker Build Cache on a Homelab: 152 GB Used, 17.7 GB Safely Freed](https://blog.melardenio.com/docker-build-cache-on-a-homelab-152-gb-used-17-7-gb-safely-freed/)
- [Fifteen Years of Blog Posts and the Migration That Fought Back](https://blog.melardenio.com/fifteen-years-of-blog-posts-and-the-migration-that-fought-back/)