Every public OpsAPI host, from the demo identity service to this website, goes through the same edge: a DNS record that only points the way, an edge proxy that terminates TLS and chooses a backend, and the Kubernetes ingress that hands the request to a pod. Knowing which layer owns what saves a lot of time when something returns the wrong status.
The path, hop by hop
| Layer | Owns | Where to look when it breaks |
|---|---|---|
| Cloudflare DNS | Name → edge. A CNAME per host, DNS-only (grey cloud) | dig the host; it must return the edge, not a Cloudflare proxy IP |
| Edge PoP (wslproxy) | TLS certificate, server + rule records, headers, rate limits | Response headers; the PoP’s access and error log for the host |
| Ingress (traefik) | Host → Service | kubectl get ingress; a 404 “page not found” is the ingress |
| Pod | The application | /ready and the pod log |
Why DNS-only
The records are not proxied by Cloudflare. The edge issues its own Let’s Encrypt certificate per host, and the HTTP-01 challenge has to reach the edge on port 80. A proxied record would also put a second TLS hop and a second set of limits in front of the edge. The cost is that the edge’s addresses are public, so the edge is also where per-host WAF policies and rate limits are switched on.
identity.demo 300 IN CNAME <edge hostname>. ; DNS-only
dashboard.demo 300 IN CNAME <edge hostname>.
opsapi.org 300 IN CNAME <edge hostname>. ; apex: CNAME flatteningInside the edge: servers and rules
The edge is wslproxy, an OpenResty gateway. A server record describes a hostname: its listeners, TLS settings, extra upstream headers and which rules apply. A rule says what to do with a matching request. Rules are read from disk on every request, so changing one needs no reload.
{
"id": "host:identity.demo.opsapi.org",
"server_name": "identity.demo.opsapi.org",
"rules": "opsapi-demo-default",
"listens": [{ "listen": "80" }],
"ssl_enabled": true,
"ssl_force_https": true,
"ssl_auto_renew": true,
"custom_headers": []
}{
"id": "opsapi-demo-default",
"priority": 1,
"match": {
"rules": { "path": "/", "path_key": "starts_with" },
"response": {
"code": 305,
"redirect_uri": "<cluster ingress>:<port>",
"backends": [{ "address": "<cluster ingress>:<port>", "weight": 100 }],
"routing": { "mode": "least_conn" }
}
},
"servers": ["host:identity.demo.opsapi.org", "host:dashboard.demo.opsapi.org"]
}| Rule code | What the edge does |
|---|---|
| 305 | Proxy pass to the backend (the normal case) |
| 301 / 302 | Redirect, e.g. www → apex |
| 200 / 403 | Serve a stored HTML body, with that status |
| 306 | CAPTCHA challenge, then proxy |
One rule can serve many hosts because the ingress routes by the Host header: all the demo hosts share a single rule pointing at the cluster ingress.
Headers set at the edge apply to every request. We once had the edge force X-Namespace-Slug: demo on the identity host. It pinned every API call to one workspace, and when a second workspace was added, every request to it was refused. Tenant selection belongs in the client, not the proxy.
Edge records are GitOps
Server and rule files live in the repo under .github/wslproxy/data/. A workflow validates them (every rule a server references must exist), then imports them into the edge control plane with their ids preserved, and takes them live. The second PoP mirrors the first.
- Add
servers/prod/host:<fqdn>.jsonand point itsrulesat a rule id. - Add the host to that rule’s
serverslist, or add a new rule file. - Create the DNS-only CNAME to the edge.
- Run the workflow (dry run first), then check the certificate and a
curl -I.
opsapi.org: a static site behind the same edge
This website is a Next.js static export published to GitHub Pages from the website/ folder of the bwalia/opsapi repo. Pages has no custom domain configured. The edge owns opsapi.org and www.opsapi.org, terminates TLS, and proxies to https://bwalia.github.io/opsapi, prefixing the repo path to every request.
- No custom domain on Pages. If Pages knew about opsapi.org it would redirect requests for the github.io address back to opsapi.org, which comes back through the edge: a loop.
- No basePath. The edge already maps
/xto/opsapi/x, so/_next/…assets resolve. The flip side is that the raw github.io URL won’t render correctly; that’s fine, nobody should use it. - Trailing slashes. The export writes
/demo/index.html; links use/demo/so Pages serves the file without a redirect.
Checks that catch most problems
dig +short identity.demo.opsapi.org # the edge, not a Cloudflare proxy address
curl -sI https://identity.demo.opsapi.org/ready # 200 from the app
curl -sI https://opsapi.org/ # 200, served by the edge from Pages
curl -s https://opsapi.org/ | grep -o '<title>[^<]*'A bare 404 page not found comes from the ingress (no route for that Host). A 502 or 504 with the edge’s own page means the edge couldn’t reach the backend. A certificate for the wrong name means the host isn’t registered at the edge yet, or DNS points somewhere else.