Threat model
The cluster runs multiple workloads — this site, an MCP server, and other experiments. The Kubernetes API is the most sensitive surface: anyone reaching it with valid credentials gets full control of every workload. Everything below is about narrowing who can reach that API and what they can do once there.
Control Plane ACL
LKE supports an IP allowlist on the API server (the “Control Plane ACL”). It’s enabled and populated with the IP ranges that need long-lived access (admin, internal services).
CI deploys are the awkward case — GitHub-hosted runners egress from Azure IP ranges that change every run. There’s no static IP to whitelist permanently.
The workflow handles this by:
- Reading the current ACL via the Linode API.
- Saving it verbatim.
- Writing a new ACL that adds
0.0.0.0/0and::/0for the duration of the deploy. - Running the deploy.
- Restoring the original ACL — always, even on failure
(
if: always()).
The open window is roughly 1–3 minutes per deploy. During that window, the cluster API is reachable from anywhere — but reaching it still requires the kubeconfig credentials, which live only in GitHub’s encrypted secret store.
Alternatives considered:
- Whitelist only the runner’s egress IP. Tried it, didn’t
work reliably — ACL propagation across control plane replicas
lags long enough that
kubectloften EOFs the first connections. Eventually consistent but not workable for short CI jobs. - In-cluster self-hosted runner. Most secure option: the runner lives in the cluster and uses a ServiceAccount, ACL stays totally closed. More moving parts to maintain — on the roadmap.
- Tailscale/WireGuard. Workable middle ground. Adds a dependency.
Supply chain
A few specific decisions to reduce supply-chain risk on the CI runner, which holds Linode and kubeconfig credentials:
- The Hugo theme is vendored into this repo. It used to be pulled as a Go module from a third-party GitHub account on every build. If that account got compromised, malicious theme code would execute on the runner with full credential access. Now the theme code is checked in directly, at a specific known state.
- No remote module fetches at build time.
go.modwas removed.
The actions are now forced onto the Node 24 runtime ahead of the
Node 20 cutoff. Pinning them to commit SHAs and swapping
peaceiris/actions-hugo@v2 for a direct binary install would tighten
this further. On the list.
Traefik
The ingress controller runs with the dashboard and API
disabled. Earlier versions of this manifest passed
--api.insecure=true, which exposed Traefik’s dashboard on
:8080 of every Traefik pod with no auth. Not externally
exposed (the NodeBalancer only forwards 80/443), but it gave
any pod in the cluster full read access to routing and cert
metadata. Removed.
If the dashboard is ever needed again, the right way is an
authenticated IngressRoute with a BasicAuth or ForwardAuth
middleware — not the insecure flag.
What’s not protected by any of this
- Anything baked into the public image — it’s a static site, so no code or secrets ship inside the container.
- DNS —
woodard.techresolves to the NodeBalancer IP, which is intentionally public.