From 02ded6c26b3d11a292688a870f31347899ac1acc Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Tue, 13 Oct 2026 21:12:40 +1100 Subject: [PATCH 1/2] Terminate haproxy edge TLS at Traefik (#547) Browsers coalesce HTTP/2 connections across *.unkin.net hosts sharing the traefik-external IP. A request for a haproxy-fronted host (e.g. git.unkin.net) on a grafana connection skips SNI passthrough and hits Traefik with no HTTP router, returning 404. - replace the TLS passthrough listener with an HTTPS listener terminating the three LE wildcards - replace the haproxy TLSRoute with an HTTPRoute for the same hosts - add a BackendTLSPolicy re-encrypting to haproxy:443 Reviewed-on: https://git.unkin.net/unkin/argocd-apps/pulls/547 Co-authored-by: unkin-agent Co-committed-by: unkin-agent --- apps/base/haproxy/backendtlspolicy.yaml | 16 ++++++++++++ apps/base/haproxy/gateway.yaml | 24 ++++++++++++------ apps/base/haproxy/httproute.yaml | 33 +++++++++++++++++++++++++ apps/base/haproxy/kustomization.yaml | 2 +- apps/base/haproxy/service.yaml | 2 +- apps/base/haproxy/tlsroute.yaml | 33 ------------------------- 6 files changed, 67 insertions(+), 43 deletions(-) create mode 100644 apps/base/haproxy/backendtlspolicy.yaml delete mode 100644 apps/base/haproxy/tlsroute.yaml diff --git a/apps/base/haproxy/backendtlspolicy.yaml b/apps/base/haproxy/backendtlspolicy.yaml new file mode 100644 index 0000000..0260b30 --- /dev/null +++ b/apps/base/haproxy/backendtlspolicy.yaml @@ -0,0 +1,16 @@ +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: BackendTLSPolicy +metadata: + name: haproxy + namespace: haproxy + labels: + app: haproxy +spec: + targetRefs: + - group: "" + kind: Service + name: haproxy + validation: + wellKnownCACertificates: System + hostname: git.unkin.net diff --git a/apps/base/haproxy/gateway.yaml b/apps/base/haproxy/gateway.yaml index 19591d4..aeb0fb1 100644 --- a/apps/base/haproxy/gateway.yaml +++ b/apps/base/haproxy/gateway.yaml @@ -1,10 +1,8 @@ --- # External (DMZ) front for the haproxy edge on the traefik-external LB VIP -# 198.18.199.0. The :443 listener is TLS Passthrough: haproxy owns the three -# wildcard certs and terminates behind Traefik, so there are no certificateRefs -# here. Listener hostnames are deliberately unset and the routes carry the -# explicit hostname list instead; allowedRoutes Same keeps other namespaces off -# these listeners. +# 198.18.199.0. Traefik terminates TLS so HTTP/2 connections coalesced from +# other *.unkin.net hosts still route by Host; the hop to haproxy is re-encrypted via +# BackendTLSPolicy. Routes carry the explicit hostname list. apiVersion: gateway.networking.k8s.io/v1 kind: Gateway metadata: @@ -21,11 +19,21 @@ spec: allowedRoutes: namespaces: from: Same - - name: https-passthrough + - name: https port: 443 - protocol: TLS + protocol: HTTPS tls: - mode: Passthrough + mode: Terminate + certificateRefs: + - group: "" + kind: Secret + name: wildcard-unkin-net-tls + - group: "" + kind: Secret + name: wildcard-main-unkin-net-tls + - group: "" + kind: Secret + name: wildcard-ceph-unkin-net-tls allowedRoutes: namespaces: from: Same diff --git a/apps/base/haproxy/httproute.yaml b/apps/base/haproxy/httproute.yaml index 9ca1411..2688a64 100644 --- a/apps/base/haproxy/httproute.yaml +++ b/apps/base/haproxy/httproute.yaml @@ -34,3 +34,36 @@ spec: - path: type: PathPrefix value: / +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: haproxy + namespace: haproxy + labels: + app: haproxy +spec: + hostnames: + - sonarr.main.unkin.net + - radarr.main.unkin.net + - lidarr.main.unkin.net + - readarr.main.unkin.net + - prowlarr.main.unkin.net + - nzbget.main.unkin.net + - jellyfin.main.unkin.net + - fafflix.unkin.net + - git.unkin.net + - dashboard.ceph.unkin.net + - auth.unkin.net + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: haproxy + sectionName: https + rules: + - backendRefs: + - group: "" + kind: Service + name: haproxy + port: 443 + weight: 1 diff --git a/apps/base/haproxy/kustomization.yaml b/apps/base/haproxy/kustomization.yaml index 16f50f5..35c5cc0 100644 --- a/apps/base/haproxy/kustomization.yaml +++ b/apps/base/haproxy/kustomization.yaml @@ -8,7 +8,7 @@ resources: - deployment.yaml - service.yaml - gateway.yaml - - tlsroute.yaml + - backendtlspolicy.yaml - httproute.yaml - pdb.yaml - vpa.yaml diff --git a/apps/base/haproxy/service.yaml b/apps/base/haproxy/service.yaml index d98b2d7..13cd587 100644 --- a/apps/base/haproxy/service.yaml +++ b/apps/base/haproxy/service.yaml @@ -6,7 +6,7 @@ metadata: namespace: haproxy spec: type: ClusterIP - # Reached only by the external Traefik's TLS-passthrough TLSRoute, so the + # Reached only by the external Traefik (re-encrypted HTTPRoute), so the # peer address here is a Traefik pod, not the client. sessionAffinity is # deliberately absent: keyed on ClientIP it would pin whole Traefik pods, # not clients. Backend persistence rests on the per-backend SRVNAME cookie. diff --git a/apps/base/haproxy/tlsroute.yaml b/apps/base/haproxy/tlsroute.yaml deleted file mode 100644 index 1a2c37c..0000000 --- a/apps/base/haproxy/tlsroute.yaml +++ /dev/null @@ -1,33 +0,0 @@ ---- -apiVersion: gateway.networking.k8s.io/v1 -kind: TLSRoute -metadata: - name: haproxy - namespace: haproxy - labels: - app: haproxy -spec: - hostnames: - - sonarr.main.unkin.net - - radarr.main.unkin.net - - lidarr.main.unkin.net - - readarr.main.unkin.net - - prowlarr.main.unkin.net - - nzbget.main.unkin.net - - jellyfin.main.unkin.net - - fafflix.unkin.net - - git.unkin.net - - dashboard.ceph.unkin.net - - auth.unkin.net - parentRefs: - - group: gateway.networking.k8s.io - kind: Gateway - name: haproxy - sectionName: https-passthrough - rules: - - backendRefs: - - group: "" - kind: Service - name: haproxy - port: 443 - weight: 1 From 283f9e8ac40ab9e06b5c431cc960f315fb2bed49 Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Tue, 13 Oct 2026 21:26:50 +1100 Subject: [PATCH 2/2] Give stalwart-apply a writable HOME (#549) The stalwart-apply PostSync Job fails with "Read-only file system": stalwart-cli caches the server schema under $HOME/.cache, which sits on the read-only root. The plan never applies, so Stalwart runs without its certificate, listeners or stdout tracer. - mount a /tmp emptyDir in both Job containers - set HOME=/tmp for the cli and kubectl (discovery cache) Reviewed-on: https://git.unkin.net/unkin/argocd-apps/pulls/549 Co-authored-by: unkin-agent Co-committed-by: unkin-agent --- apps/base/mail/stalwart-apply-job.yaml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/apps/base/mail/stalwart-apply-job.yaml b/apps/base/mail/stalwart-apply-job.yaml index 70059a3..79578f7 100644 --- a/apps/base/mail/stalwart-apply-job.yaml +++ b/apps/base/mail/stalwart-apply-job.yaml @@ -34,6 +34,9 @@ spec: - --file - /plan/plan.ndjson env: + # the cli caches the server schema under $HOME/.cache + - name: HOME + value: /tmp - name: STALWART_URL value: http://stalwart.mail.svc:8080 - name: STALWART_USER @@ -63,6 +66,8 @@ spec: - name: plan mountPath: /plan readOnly: true + - name: tmp + mountPath: /tmp containers: - name: restart image: registry.k8s.io/kubectl:v1.33.13 @@ -71,6 +76,9 @@ spec: - restart - statefulset/stalwart - --namespace=mail + env: + - name: HOME + value: /tmp securityContext: runAsUser: 65532 runAsGroup: 65532 @@ -86,6 +94,9 @@ spec: requests: cpu: 50m memory: 32Mi + volumeMounts: + - name: tmp + mountPath: /tmp volumes: - name: plan configMap: @@ -93,3 +104,5 @@ spec: items: - key: plan.ndjson path: plan.ndjson + - name: tmp + emptyDir: {}