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.
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)
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]>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Follow-up to #47. Two symptoms you reported after publishing was restored.
1. LAN clients were shown as Remote
Root cause, measured:
LanNetworksBandwidthwas ABSENT, so Plex had no definition of a localnetwork and classified everything as WAN — 468 requests logged
Allowed Network (WAN), ZERO(LAN), including Seerr dialling from inside the cluster.allowedNetworks(theALLOWED_NETWORKSenv) only waives auth — it does not make a clientlocal. 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, andzero 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 VIPand 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 itwith
${PLEX_L4_VIP}on the LAN, Cloudflare answers it publicly with the WAN address NATed to thesame VIP.
PLEX_ADVERTISE_URLis deleted, and the cert-hash fragility goes with it.Changes
catalog/plex.yamlADVERTISE_IP→ the hostname;lifecycle.postStartre-assertingLanNetworksBandwidth+customCertificate*every start; TLS env (optional: true)apps/wave-0/plex-cert.yaml(new)Certificatefor exactly that name fromletsencrypt-prodschemas/…schema.json→ 1.11.0optionalonsecretKeyRef/configMapKeyRefcatalog/pihole.yamlplex-vipsplit-horizon answerclusters/**PLEX_ADVERTISE_URLremoved;PLEX_L4_VIP's second consumer recordedWhy the schema change is load-bearing, not tidiness: the Certificate must be issued into the
plexnamespace, and that namespace is created by the renderedapps/wave-0/plex.yamlitself — soit can't be issued in an earlier tier and lands in the same reconcile as the workload. With hard
refs Plex would sit in
CreateContainerConfigErroruntil a cert issued, andapps-wave-0iswait: truewithapps-wave-1dependsOn it — a slow DNS01 challenge would have stalled the wholeapp 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 to192.168.2.20(the Gateway VIP) via the zone wildcard, and:32400there returnedhttp=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=0andsecureConnectionsdefaults to Preferred, soclients 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 theremote 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-policyall pass; rc 3 is the two known environmental skips (gitea-settings— nolocal token;
conformanceL2/L3 —CLUSTER_AVAILABLEunset). Not green — reported as rc 3.l0passing proves the schema edit admitsoptionaland still validates every catalog file;render-goldenproves catalog and goldens agree. Only plex and pihole goldens moved.⚠️ Unverified — read before merging
plex-vipstill resolves to the GatewayVIP,
LanNetworksBandwidthis still absent, the hook has never executed.defaults to AES-256/SHA-256, which some consumers reject. The hook guards against a missing
bundle, not a present-but-unreadable one.
If it fails, Plex still runs but the advertised
https://URL is unservable — i.e. this couldleave the advertised URL broken in a new way. The first reconcile needs watching.
externalTrafficPolicy: Cluster,once
192.168.0.0/16is LAN, genuine remote clients arriving via the VIP are also classifiedlocal, so remote bandwidth limits won't bind. The 2026-10-03 metering trade-off, now stronger.
plex-viprecord and the WANtcp/32400 forward.
(measured). A volume is the better shape; that's the clean follow-up.
Needs approval
Touches
clusters/**, soci/approval-policy.shwill 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
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]>