feat(plex): one advertised hostname for local+remote, its own LE cert, and a LAN definition so LAN clients stop reading as remote #48

Merged
jakman85 merged 1 commits from feat/plex-vip-hostname-and-lan-networks into main 2026-10-04 21:47:32 +00:00
Owner

Follow-up to #47. Two symptoms you reported after publishing was restored.

1. LAN clients were shown as Remote

Root cause, measured: LanNetworksBandwidth was ABSENT, so Plex had no definition of a local
network and classified everything as WAN — 468 requests logged Allowed Network (WAN), ZERO
(LAN)
, including Seerr dialling from inside the cluster.

allowedNetworks (the ALLOWED_NETWORKS env) only waives auth — it does not make a client
local. Two different settings, and only one of them has an env hook. Measured across the image's
whole init surface: it writes exactly six prefs and supports no PLEX_PREFERENCE* of any kind, and
zero files mention LanNetworks. So a hook is the only GitOps path.

2. One advertised hostname instead of a fragile generated URL

#47 advertised https://<dashed-ip>.<serverhash>.plex.direct:32400 — works, but hard-codes the VIP
and the server's cert hash into a cluster-var, two values nothing kept in sync.

Your design instead: one hostname, plex-vip.${BASE_DOMAIN}, split-horizon — PiHole answers it
with ${PLEX_L4_VIP} on the LAN, Cloudflare answers it publicly with the WAN address NATed to the
same VIP. PLEX_ADVERTISE_URL is deleted, and the cert-hash fragility goes with it.

Changes

File What
catalog/plex.yaml ADVERTISE_IP → the hostname; lifecycle.postStart re-asserting LanNetworksBandwidth + customCertificate* every start; TLS env (optional: true)
apps/wave-0/plex-cert.yaml (new) cert-manager Certificate for exactly that name from letsencrypt-prod
schemas/…schema.json → 1.11.0 optional on secretKeyRef/configMapKeyRef
catalog/pihole.yaml the plex-vip split-horizon answer
clusters/** PLEX_ADVERTISE_URL removed; PLEX_L4_VIP's second consumer recorded

Why the schema change is load-bearing, not tidiness: the Certificate must be issued into the
plex namespace, and that namespace is created by the rendered apps/wave-0/plex.yaml itself — so
it can't be issued in an earlier tier and lands in the same reconcile as the workload. With hard
refs Plex would sit in CreateContainerConfigError until a cert issued, and apps-wave-0 is
wait: true with apps-wave-1 dependsOn it — a slow DNS01 challenge would have stalled the whole
app tier
. With optional: true, Plex starts regardless, the hook logs that TLS wasn't installed,
and the next start converges.

The PiHole line is required, not cosmetic: measured, plex-vip.${BASE_DOMAIN} resolved to
192.168.2.20 (the Gateway VIP) via the zone wildcard, and :32400 there returned http=000.

A correction to my own earlier reasoning

#47's handoff claimed a bare-IP URL "can never validate" and treated a SAN match as a requirement.
You were right that it isn't: httpsRequired=0 and secureConnections defaults to Preferred, so
clients can fall back to cleartext. The measurements stand (a validating client did fail; real
clients did send certificate unknown) — but the correct framing is that a matching cert keeps the
remote leg encrypted, not that it's required for Plex to work. Hence this branch obtains a real
cert for the advertised name rather than choosing the name to suit Plex's built-in one.

Gate

rc 3, with spec-immutability, decisions-log, lint, l0, render-golden, l1,
approval-policy all pass; rc 3 is the two known environmental skips (gitea-settings — no
local token; conformance L2/L3 — CLUSTER_AVAILABLE unset). Not green — reported as rc 3.
l0 passing proves the schema edit admits optional and still validates every catalog file;
render-golden proves catalog and goldens agree. Only plex and pihole goldens moved.

⚠️ Unverified — read before merging

  • Nothing here has run. No Certificate exists in prod, plex-vip still resolves to the Gateway
    VIP, LanNetworksBandwidth is still absent, the hook has never executed.
  • Whether Plex accepts an OpenSSL-3 PKCS#12 bundle is untested — the largest risk. OpenSSL 3
    defaults to AES-256/SHA-256, which some consumers reject. The hook guards against a missing
    bundle, not a present-but-unreadable one.
  • Whether the cert will issue for this name is inferred from the wildcard's success, not shown.
    If it fails, Plex still runs but the advertised https:// URL is unservable — i.e. this could
    leave the advertised URL broken in a new way. The first reconcile needs watching.
  • SNAT makes the LAN definition coarser than it looks: with externalTrafficPolicy: Cluster,
    once 192.168.0.0/16 is LAN, genuine remote clients arriving via the VIP are also classified
    local, so remote bandwidth limits won't bind. The 2026-10-03 metering trade-off, now stronger.
  • Still off-repo and still needed for remote: the Cloudflare plex-vip record and the WAN
    tcp/32400 forward.
  • The private key sits in the container's env — the schema/renderer support no secret volume
    (measured). A volume is the better shape; that's the clean follow-up.

Needs approval

Touches clusters/**, so ci/approval-policy.sh will block pending a non-author approving review
(or /approve-override <reason> as a PR review of type "Comment"). Deliberately not self-posted
— CLAUDE.md §1.

🤖 Generated with Claude Code

Follow-up to #47. Two symptoms you reported after publishing was restored. ## 1. LAN clients were shown as Remote Root cause, measured: **`LanNetworksBandwidth` was ABSENT**, so Plex had no definition of a local network and classified *everything* as WAN — **468 requests logged `Allowed Network (WAN)`, ZERO `(LAN)`**, including Seerr dialling from inside the cluster. `allowedNetworks` (the `ALLOWED_NETWORKS` env) only waives **auth** — it does not make a client local. Two different settings, and only one of them has an env hook. Measured across the image's whole init surface: it writes exactly six prefs and supports no `PLEX_PREFERENCE*` of any kind, and zero files mention `LanNetworks`. So a hook is the only GitOps path. ## 2. One advertised hostname instead of a fragile generated URL #47 advertised `https://<dashed-ip>.<serverhash>.plex.direct:32400` — works, but hard-codes the VIP *and* the server's cert hash into a cluster-var, two values nothing kept in sync. Your design instead: **one hostname, `plex-vip.${BASE_DOMAIN}`**, split-horizon — PiHole answers it with `${PLEX_L4_VIP}` on the LAN, Cloudflare answers it publicly with the WAN address NATed to the same VIP. `PLEX_ADVERTISE_URL` is **deleted**, and the cert-hash fragility goes with it. ## Changes | File | What | |---|---| | `catalog/plex.yaml` | `ADVERTISE_IP` → the hostname; `lifecycle.postStart` re-asserting `LanNetworksBandwidth` + `customCertificate*` every start; TLS env (`optional: true`) | | `apps/wave-0/plex-cert.yaml` *(new)* | cert-manager `Certificate` for exactly that name from `letsencrypt-prod` | | `schemas/…schema.json` → **1.11.0** | `optional` on `secretKeyRef`/`configMapKeyRef` | | `catalog/pihole.yaml` | the `plex-vip` split-horizon answer | | `clusters/**` | `PLEX_ADVERTISE_URL` removed; `PLEX_L4_VIP`'s second consumer recorded | **Why the schema change is load-bearing, not tidiness:** the Certificate must be issued *into* the `plex` namespace, and that namespace is created by the rendered `apps/wave-0/plex.yaml` itself — so it can't be issued in an earlier tier and lands in the **same reconcile** as the workload. With hard refs Plex would sit in `CreateContainerConfigError` until a cert issued, and `apps-wave-0` is `wait: true` with `apps-wave-1` dependsOn it — a slow DNS01 challenge would have **stalled the whole app tier**. With `optional: true`, Plex starts regardless, the hook logs that TLS wasn't installed, and the next start converges. **The PiHole line is required, not cosmetic:** measured, `plex-vip.${BASE_DOMAIN}` resolved to `192.168.2.20` (the Gateway VIP) via the zone wildcard, and `:32400` there returned `http=000`. ## A correction to my own earlier reasoning #47's handoff claimed a bare-IP URL "can never validate" and treated a SAN match as a *requirement*. You were right that it isn't: `httpsRequired=0` and `secureConnections` defaults to Preferred, so clients can fall back to cleartext. The measurements stand (a validating client did fail; real clients did send `certificate unknown`) — but the correct framing is that a matching cert keeps the remote leg **encrypted**, not that it's required for Plex to work. Hence this branch obtains a real cert for the advertised name rather than choosing the name to suit Plex's built-in one. ## Gate **rc 3**, with `spec-immutability`, `decisions-log`, `lint`, `l0`, `render-golden`, `l1`, `approval-policy` all **pass**; rc 3 is the two known environmental skips (`gitea-settings` — no local token; `conformance` L2/L3 — `CLUSTER_AVAILABLE` unset). **Not green — reported as rc 3.** `l0` passing proves the schema edit admits `optional` and still validates every catalog file; `render-golden` proves catalog and goldens agree. Only plex and pihole goldens moved. ## ⚠️ Unverified — read before merging - **Nothing here has run.** No Certificate exists in prod, `plex-vip` still resolves to the Gateway VIP, `LanNetworksBandwidth` is still absent, the hook has never executed. - **Whether Plex accepts an OpenSSL-3 PKCS#12 bundle is untested — the largest risk.** OpenSSL 3 defaults to AES-256/SHA-256, which some consumers reject. The hook guards against a *missing* bundle, **not** a present-but-unreadable one. - **Whether the cert will issue** for this name is inferred from the wildcard's success, not shown. If it fails, Plex still runs but the advertised `https://` URL is unservable — i.e. this could leave the advertised URL broken in a new way. The first reconcile needs watching. - **SNAT makes the LAN definition coarser than it looks:** with `externalTrafficPolicy: Cluster`, once `192.168.0.0/16` is LAN, genuine *remote* clients arriving via the VIP are also classified local, so remote bandwidth limits won't bind. The 2026-10-03 metering trade-off, now stronger. - **Still off-repo and still needed for remote:** the Cloudflare `plex-vip` record and the WAN tcp/32400 forward. - **The private key sits in the container's env** — the schema/renderer support no secret volume (measured). A volume is the better shape; that's the clean follow-up. ## Needs approval Touches `clusters/**`, so `ci/approval-policy.sh` will block pending a non-author approving review (or `/approve-override <reason>` as a PR **review** of type "Comment"). Deliberately not self-posted — CLAUDE.md §1. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
gitea-admin added 1 commit 2026-10-04 08:40:22 +00:00
feat(plex): one advertised hostname for local+remote, its own LE cert, and a LAN definition
gate / gate (pull_request) Successful in 1m49s
gate / conformance (pull_request) Has been skipped
0aa1a0a78a
Two symptoms after the publishing fix landed:

1. LAN clients were reported as Remote. Measured root cause:
   LanNetworksBandwidth was ABSENT, so Plex had no notion of a local
   network and classified everything WAN -- 468 requests logged
   "Allowed Network (WAN)", ZERO "(LAN)", including Seerr from inside the
   cluster. allowedNetworks only waives AUTH; it does not make a client
   local. Two settings, and only one has an env hook.
2. The advertised URL hard-coded both the VIP and the server's cert hash
   into a cluster-var -- two values nothing kept in sync.

Operator design: ONE hostname, plex-vip.${BASE_DOMAIN}, split-horizon --
PiHole answers it with ${PLEX_L4_VIP} on the LAN, Cloudflare answers it
publicly with the WAN address NATed to the same VIP.

- catalog/plex.yaml: ADVERTISE_IP -> https://plex-vip.${BASE_DOMAIN}:32400,
  plus a level-triggered postStart hook (pihole/qbittorrent idiom) that
  re-asserts LanNetworksBandwidth and the three customCertificate* prefs
  every start, converting PEM -> PKCS#12 so cert renewals need no extra part.
- apps/wave-0/plex-cert.yaml: cert-manager Certificate for exactly that
  name from letsencrypt-prod, so the advertised https URL is servable.
- schema 1.11.0: `optional` on secretKeyRef/configMapKeyRef. Load-bearing --
  the cert is issued INTO the plex namespace, which the rendered plex.yaml
  itself creates, so it lands in the same reconcile; hard refs would have
  held Plex in CreateContainerConfigError and, since apps-wave-0 is
  wait:true, stalled the whole app tier on a slow DNS01 challenge.
- catalog/pihole.yaml: the plex-vip split-horizon answer. Required --
  measured, the name resolved to the Gateway VIP where nothing listens
  on 32400 (http=000).
- PLEX_ADVERTISE_URL deleted; the hostname removes the cert-hash fragility.

Corrects my own earlier claim that a SAN match is REQUIRED: it is not
(httpsRequired=0, secureConnections defaults to Preferred, so clients can
fall back to cleartext). A matching cert keeps the remote leg ENCRYPTED.
Hence: obtain a real cert for the advertised name rather than pick the name
to suit Plex's built-in one.

UNVERIFIED, and stated plainly in the handoff: nothing here has run. Cert
issuance for this name is inferred from the wildcard's success, and whether
Plex accepts an OpenSSL-3 PKCS#12 bundle is untested -- the largest risk.

Touches clusters/** -> sensitive paths -> needs a non-author approval or the
operator's /approve-override; deliberately not self-posted (CLAUDE.md 1).

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
jakman85 approved these changes 2026-10-04 21:40:44 +00:00
jakman85 merged commit c163deaef9 into main 2026-10-04 21:47:32 +00:00
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Home-Network/home-network-k3s-impl#48