Establish clean homelab infrastructure baseline
lint / yaml (push) Has been cancelled
lint / ansible (push) Has been cancelled
lint / terraform (push) Has been cancelled

Reorganize the brownfield repository, remove retired and generated artifacts, harden ignore rules, and record the GitOps/IaC redesign.
This commit is contained in:
2026-09-09 16:47:20 +00:00
commit 88a02ababa
418 changed files with 50579 additions and 0 deletions
+118
View File
@@ -0,0 +1,118 @@
# Envoy Gateway — LAN ingress + Authelia forward-auth
The cluster's HTTP entry point for services on `ad.ddupan.top`, and the enforcement
point for Authelia authentication. Envoy's LoadBalancer holds **192.168.10.127** (k3s
ServiceLB); routing is by `Host` header.
- Chart: `oci://docker.io/envoyproxy/gateway-helm` **v1.5.6**, ns `envoy-gateway-system`
- `gateway.yaml` — `GatewayClass eg` + `Gateway eg` (`:80` plaintext, `:443` wildcard TLS)
- Gateway API **v1.3.0**
- Replaced Contour on 2026-07-25 (see [Why not Contour](#why-not-contour))
## Adding a service
1. an `HTTPRoute` with `parentRefs` → `eg` / `envoy-gateway-system`, `sectionName: https`
2. an A record in `samba_ad_extra_a_records` → `192.168.10.127` (`../../infrastructure/samba-ad`), then
`ansible-playbook provision-dc.yml --tags dns`
3. **optionally** a `SecurityPolicy` for Authelia forward-auth — see `../../apps/netbox`
No certificate work: the `:443` listener already serves the `*.ad.ddupan.top` wildcard
from `../cert-manager`. Worked example: `../../apps/netbox`.
## Why not Contour
Contour worked as an ingress, but supports **only the gRPC** Envoy ext_authz protocol —
*"Only the Envoy GRPC authorization protocol will be supported"* (Contour 1.33 docs).
Authelia implements the **HTTP** ExtAuthz filter. The two cannot meet, so Authelia
forward-auth was impossible.
That mattered because **NetBox has no SSO group→role mapping** — its group/superuser
mapping is LDAP-only, and the OIDC pipeline can only assign one static group. Without
forward-auth the options were "promote every user by hand" or "drop SSO and use LDAP,
losing 2FA". Envoy Gateway's `SecurityPolicy.extAuth.http` removes the dilemma.
Envoy Gateway was chosen over Traefik because everything already built — Gateway,
HTTPRoute, wildcard cert, DNS — is Gateway API, so only the GatewayClass and controller
changed. Traefik would have meant reverting to `IngressRoute` + `Middleware`.
## Forward-auth: how it fits together
```
browser ──▶ Envoy ──(ext_authz HTTP)──▶ Authelia ──▶ 200 + Remote-* headers
│ └──▶ 401 ⇒ 302 to auth.ddupan.top
└──▶ upstream app (headers attached)
```
Pieces, each in the directory that owns it:
| where | what |
|---|---|
| `../../apps/netbox/securitypolicy.yaml` | `SecurityPolicy` targeting the app's HTTPRoute |
| `../../apps/authelia/referencegrant-extauth.yaml` | lets a SecurityPolicy in another namespace reference the Authelia Service |
| `../../apps/authelia/values.yaml` | `server.endpoints.authz.ext-authz` + an `access_control` rule |
| `../../apps/netbox/networkpolicy.yaml` | stops anything bypassing Envoy to reach the app directly |
### Three details that cost time
- **`headersToBackend` lives under `extAuth.http`, not `extAuth`.** One level up the API
rejects it: `unknown field "spec.extAuth.headersToBackend"`.
- **`backendRefs.port` is the SERVICE port, not the container port.** The Authelia chart
publishes `80 → targetPort http (9091)`; using `9091` fails with
`TCP Port 9091 not found on service authelia/authelia`.
- **Authelia's session cookie is scoped to `ddupan.top`**, which already covers
`*.ad.ddupan.top` — so SSO works across both without touching the cookie config.
### ⚠ Trust boundary
Apps behind forward-auth trust `Remote-*` headers. Two things make that safe and **both**
must remain true:
1. `headersToBackend` **overrides** any client-supplied value ("coexisting headers will be
overridden"), so a spoofed `Remote-User` cannot survive the hop through Envoy.
2. A `NetworkPolicy` per app restricts pod ingress to `envoy-gateway-system`, so nothing
in-cluster can bypass Envoy. k3s enforces NetworkPolicy (kube-router), so this is real.
Verified by test: a pod in `default` sending `Remote-User: admin` to the app Service gets
**connection refused**, while the same request from `envoy-gateway-system` is served.
Also set `failOpen: false` — if Authelia is down, refuse traffic rather than admit
unauthenticated requests to an app whose auth model is "trust the header".
## Install
```bash
# The Gateway API CRDs may already be owned by another tool's field manager (Contour's
# quickstart used client-side apply), which makes Helm fail with
# "conflict with kubectl-client-side-apply: .spec.versions".
# Transfer ownership WITHOUT deleting the CRDs (deleting them would delete every
# Gateway and HTTPRoute):
helm pull oci://docker.io/envoyproxy/gateway-helm --version v1.5.6 --untar
kubectl apply --server-side --force-conflicts --field-manager=helm \
-f gateway-helm/crds/gatewayapi-crds.yaml
helm upgrade --install envoy-gateway oci://docker.io/envoyproxy/gateway-helm \
--version v1.5.6 -n envoy-gateway-system --create-namespace
kubectl apply -f envoy-gateway/gateway.yaml
```
Only one LoadBalancer can hold `:80/:443` — k3s ServiceLB uses hostPorts, so a second
one's `svclb-*` DaemonSet sits at `0/1` and the Gateway stays `PROGRAMMED: False` until
the previous controller's Service is gone.
## Verify
```bash
kubectl -n envoy-gateway-system get gateway eg # PROGRAMMED=True, ADDRESS 192.168.10.127
kubectl -n envoy-gateway-system get pods # envoy-... 2/2
kubectl -n <ns> get securitypolicy # Accepted=True
curl -s -o /dev/null -w '%{http_code}\n' http://192.168.10.127/ # 404 = serving, no route matched
```
`ss -lntp` shows **nothing** on `:80/:443` even when healthy — ServiceLB forwards via
iptables/NodePort rather than binding. Test with `curl`, not `ss`.
## Outstanding
Contour source manifests were removed from the clean baseline. Live leftovers may
still exist and must be inventoried before deletion; do not infer live state from
the archive cleanup. The legacy Git history retains the retired manifests.
+116
View File
@@ -0,0 +1,116 @@
# LAN ingress for k3s services on ad.ddupan.top.
#
# Replaced Contour 2026-07-25. Contour was dropped for one concrete reason: it
# supports ONLY the gRPC Envoy ext_authz protocol ("Only the Envoy GRPC authorization
# protocol will be supported" — Contour 1.33 docs), while Authelia implements the
# HTTP ExtAuthz filter. That made Authelia forward-auth impossible, which in turn made
# AD-group -> role mapping impossible for apps like NetBox whose SSO support has no
# group mapping. Envoy Gateway's SecurityPolicy supports extAuth.http, so it works.
#
# Everything else carried over unchanged: the Gateway API objects, the wildcard cert
# from ../cert-manager, and the DNS A records in ../../infrastructure/samba-ad.
---
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: eg
spec:
controllerName: gateway.envoyproxy.io/gatewayclass-controller
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: eg
namespace: envoy-gateway-system
spec:
gatewayClassName: eg
listeners:
# Plaintext :80 — ACME http-01 (the bao-acme ClusterIssuer solves through here)
# and HTTP->HTTPS redirects. Never serve anything sensitive on it.
- name: http
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: All
kinds:
- group: gateway.networking.k8s.io
kind: HTTPRoute
# HTTPS :443. One wildcard covers every LAN service, so adding a service is an
# HTTPRoute + a DNS A record — no cert work and no edit here.
#
# ⚠ The cert Secret MUST live in this Gateway's namespace (a listener may only
# reference a Secret in its own namespace without a ReferenceGrant), which is why
# cert-manager/certificate-wildcard-ad.yaml issues into envoy-gateway-system.
#
# ⚠ Never point this at a Secret the gateway controller generates for its own
# internal use. Doing exactly that with Contour's `contourcert-*` xDS secret killed
# its dataplane for 141 days — see the legacy Git history.
- name: https
protocol: HTTPS
port: 443
hostname: "*.ad.ddupan.top"
tls:
mode: Terminate
certificateRefs:
- kind: Secret
name: wildcard-ad-ddupan-top-tls
allowedRoutes:
namespaces:
from: All
kinds:
- group: gateway.networking.k8s.io
kind: HTTPRoute
# HTTPS :443 for auth.ddupan.top specifically.
#
# WHY a second listener rather than another hostname on the one above: a
# listener carries exactly one hostname, and `*.ad.ddupan.top` cannot match
# `auth.ddupan.top` — different zone (Cloudflare is authoritative for
# ddupan.top, the DC only for ad.ddupan.top) and one label shallower. Envoy
# selects between them by SNI, so both coexist on :443 cleanly.
#
# This exists so in-cluster clients reach Authelia over the LAN instead of
# hairpinning through Cloudflare proxy IPs that are unroutable from this
# network. Full reasoning in ../cert-manager/certificate-auth-ddupan.yaml;
# the route is ../../apps/authelia/httproute.yaml.
- name: https-auth
protocol: HTTPS
port: 443
hostname: "auth.ddupan.top"
tls:
mode: Terminate
certificateRefs:
- kind: Secret
name: auth-ddupan-top-tls
allowedRoutes:
namespaces:
from: All
kinds:
- group: gateway.networking.k8s.io
kind: HTTPRoute
# HTTPS :443 for git.ddupan.top. Third listener for the same reason as the
# second: one hostname per listener, and the *.ad.ddupan.top wildcard cannot
# match a name in the ddupan.top zone. Envoy picks between all three by SNI.
#
# Exists so `git push` stays on the LAN instead of going out to Cloudflare and
# back down the tunnel into this same cluster. The repo is what you need during
# an incident, so it must not depend on the WAN. Cert:
# ../cert-manager/certificate-git-ddupan.yaml; route: ../../apps/gitea/httproute.yaml.
- name: https-git
protocol: HTTPS
port: 443
hostname: "git.ddupan.top"
tls:
mode: Terminate
certificateRefs:
- kind: Secret
name: git-ddupan-top-tls
allowedRoutes:
namespaces:
from: All
kinds:
- group: gateway.networking.k8s.io
kind: HTTPRoute