the published surface, on a fresh root

This repository is now the public half of a split: six documents saying what to
decide, and one gap. How the cluster is built moved to jeffry/homelab-impl, which
is private because its README is an inventory of chart versions and image tags.

History starts here deliberately, and not as tidiness. The previous history
contained that inventory, and this repository is public — a deletion commit would
have removed it from the tree and left it in the log. A fresh root can only carry
what is in it.

Two pointers rewritten rather than deleted: private-access.md and .loom/README.md
both directed a reader to TAILSCALE.md and README.md at the root, which are now
private. They now say a fuller reference exists, that it is private, and how to
ask — because a public page naming a private thing as its answer is the failure
this project has now hit four times.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-07 15:19:14 -04:00
co-authored by Claude Opus 5
commit cc66e227a3
9 changed files with 566 additions and 0 deletions
+89
View File
@@ -0,0 +1,89 @@
# Put something on the internet, the way production would
**This cluster is staging for the production cluster**, and that is the reason it
exists rather than a use it happens to have.
> **Production's route manifests apply here unmodified.** *The only difference
> between the two Gateway definitions is the TLD — `.dev` here, `.com` there.
> Secret names are identical.*
**So the point of this page is what to leave out**, not what to add.
---
## Carry no hostname of your own
**Routes pin to a listener by `sectionName` and inherit its hostname.** *That is
what makes them portable — a route that carries its own `hostnames:` stops
inheriting and stops working in the other environment.*
**Four listeners**, matching production one for one: `web` (plain HTTP), and
`websecure`, `theoria` and `notes` over HTTPS. **Pin to the one whose hostname you
want.**
**One route is the exception and keeps its hostname:** the HTTP-to-HTTPS
redirect, because the `web` listener declares no hostname at all — *dropping it
there would widen the redirect from one host to every host.*
## The Gateway is not in the controller's namespace, on purpose
**The Gateway lives in `traefik`; the controller runs in `traefik-system`.**
*Only the **Gateway's** namespace affects how `parentRefs` resolve, and
production names `traefik`* — **so production's refs resolve here verbatim.**
Moving the controller would have meant an uninstall against a live service for
something no application can observe.
## Plain HTTP does not answer for everything
**Only one hostname redirects from HTTP.** *The others have no route on the `web`
listener and return 404 on plain HTTP.* **That mirrors production exactly**, and
it is a consequence of the exception above rather than an oversight.
## A typo attaches to nothing, silently
**A route with a wrong `sectionName` binds to no listener and reports nothing.**
```sh
kubectl get gateway traefik-gateway -n traefik \
-o jsonpath='{range .status.listeners[*]}{.name}: attached={.attachedRoutes}{"\n"}{end}'
```
> **`attachedRoutes` is the number that actually bound.** *Check it, not whether
> the route exists.*
## TLS is issued here and only terminated here
**cert-manager owns every certificate**, via DNS-01 against Cloudflare. **Do not
configure the proxy's own ACME resolver.**
*A certificate issues without the hostname resolving publicly, because the
challenge is DNS-based. **Iterate against the staging issuer** — production rate
limits are easy to hit during a rebuild.*
*A failed issuance backs off starting at **one hour**, and deleting the
`CertificateRequest` does not reset it — it looks hung and is not. **Delete and
re-apply the `Certificate`.***
## The older path still works
**One service is on a plain `Ingress`** rather than the Gateway, and stays that
way until it is migrated. *Production uses no `Ingress` at all; when that last one
moves, the environments match exactly.* **The divergence has an explicit end
state, so it cannot quietly become permanent.**
---
## Checking this is still true
**Verified 2026-09-03.**
```sh
kubectl get gatewayclass # ACCEPTED=True
kubectl get gateway -n traefik # PROGRAMMED=True, with an address
curl -sS -o /dev/null -w "%{http_code}\n" https://class.hypertheory-labs.dev/
# A hostname with no listener MUST 404. A 200 means a route was widened.
curl -skS -o /dev/null -w "%{http_code}\n" \
--resolve bogus.hypertheory-labs.dev:443:192.168.5.10 https://bogus.hypertheory-labs.dev/
```