Moves the WordPress behind renner.uno off the Hetzner VM wordpress-hochzeit (Ubuntu 22.04, PHP 8.1, MySQL 8.0) and into the cluster as www.renner.uno.
Why now
The site is not the frozen wedding archive it looks like. It carries an unpublished, fully composed birth announcement from 2026-05-14 (columns, image, pullquote) and a second account — s.birzer, Editor. So wp-admin has to keep working for a human author; a static export or a Git-only workflow would have frozen the site right before the thing someone was about to publish.
Shape
mariadb-operator, new in infrastructure/. WordPress speaks MySQL natively; pg4wp lists WordPress 6.5.3 as its newest tested version while this site runs 7.0.4.
Split routing on one host./ goes straight to WordPress so the blog stays publicly readable; /wp-admin and /wp-login.php route through a wordpress-gate outpost. The paperless-style inverse — gate the host, skip_path_regex the public paths — needs a negative lookahead, and the outpost's RE2 has none.
Core lives in the pinned image, only wp-content is persisted. Upgrading is a tag bump.
Backups split as elsewhere: operator takes a logical dump to Hetzner Object Storage at 02:45, Velero takes the wp-content PVC at 08:15 and skips the database volume.
Verified, not assumed
Gating all of /wp-admin is safe because wpgiftregistry is dropped: it was the only emitter of ajaxurl on this site, and of 121 admin-ajax hits in the live log window, none returned 200.
The MySQL-8-only collation utf8mb4_0900_ai_ci appears on exactly 7 tables, all from the dead RSVP plugin — 6 empty, 1 holding 5 lookup rows. They are excluded from the migration dump, so the incompatibility disappears rather than being rewritten.
Cluster enforces PodSecurity baseline, so the official Apache image needs no namespace label.
Still open
The hostname www.renner.uno must be allowed as SNI on the NetBird reverse proxy. That configuration lives in NetBird management, not in this repo.
kubectl get svc -n wordpress — bestätigen, dass der Service wordpress-mariadb heißt. WORDPRESS_DB_HOST ist darauf fest verdrahtet; bei Abweichung crashloopt der Pod (laut, nicht still).
kubectl get pvc -n wordpress --show-labels — die Velero-Auswahl greift auf app.kubernetes.io/name=wordpress,component=content.
https://www.renner.uno/wp-admin/ aufrufen und das Zertifikat prüfen: der zweite Ingress bringt keinen eigenen tls:-Block mit, sondern verlässt sich auf den gemeinsamen SNI-Zertifikatsspeicher. Erstes Vorkommen dieses Musters in diesem Repo.
Datenübernahme: db-migration.sql und wp-content einspielen, dann wp search-replace https://renner.uno https://www.renner.uno und wp plugin delete wpgiftregistry.
Bekannte Kopplung:hetzner-s3-creds liest weiterhin seed/paperless/hetzner-creds. Eine Rotation dieser Zugangsdaten trifft damit auch das WordPress-Backup.
Moves the WordPress behind renner.uno off the Hetzner VM `wordpress-hochzeit` (Ubuntu 22.04, PHP 8.1, MySQL 8.0) and into the cluster as `www.renner.uno`.
## Why now
The site is not the frozen wedding archive it looks like. It carries an unpublished, fully composed birth announcement from 2026-05-14 (columns, image, pullquote) and a second account — `s.birzer`, Editor. So wp-admin has to keep working for a human author; a static export or a Git-only workflow would have frozen the site right before the thing someone was about to publish.
## Shape
- **mariadb-operator**, new in `infrastructure/`. WordPress speaks MySQL natively; pg4wp lists WordPress 6.5.3 as its newest tested version while this site runs 7.0.4.
- **Split routing on one host.** `/` goes straight to WordPress so the blog stays publicly readable; `/wp-admin` and `/wp-login.php` route through a `wordpress-gate` outpost. The paperless-style inverse — gate the host, `skip_path_regex` the public paths — needs a negative lookahead, and the outpost's RE2 has none.
- **Core lives in the pinned image**, only `wp-content` is persisted. Upgrading is a tag bump.
- **Backups split** as elsewhere: operator takes a logical dump to Hetzner Object Storage at 02:45, Velero takes the wp-content PVC at 08:15 and skips the database volume.
## Verified, not assumed
- Gating all of `/wp-admin` is safe because `wpgiftregistry` is dropped: it was the only emitter of `ajaxurl` on this site, and of 121 admin-ajax hits in the live log window, none returned 200.
- The MySQL-8-only collation `utf8mb4_0900_ai_ci` appears on exactly 7 tables, all from the dead RSVP plugin — 6 empty, 1 holding 5 lookup rows. They are excluded from the migration dump, so the incompatibility disappears rather than being rewritten.
- Cluster enforces PodSecurity `baseline`, so the official Apache image needs no namespace label.
## Still open
The hostname `www.renner.uno` must be allowed as SNI on the NetBird reverse proxy. That configuration lives in NetBird management, not in this repo.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_014giKN9N6e5J2DF8QZmVh98
## Nach dem Merge zu tun
1. `kubectl get svc -n wordpress` — bestätigen, dass der Service `wordpress-mariadb` heißt. `WORDPRESS_DB_HOST` ist darauf fest verdrahtet; bei Abweichung crashloopt der Pod (laut, nicht still).
2. `kubectl get pvc -n wordpress --show-labels` — die Velero-Auswahl greift auf `app.kubernetes.io/name=wordpress,component=content`.
3. `https://www.renner.uno/wp-admin/` aufrufen und das Zertifikat prüfen: der zweite Ingress bringt keinen eigenen `tls:`-Block mit, sondern verlässt sich auf den gemeinsamen SNI-Zertifikatsspeicher. Erstes Vorkommen dieses Musters in diesem Repo.
4. Datenübernahme: `db-migration.sql` und `wp-content` einspielen, dann `wp search-replace https://renner.uno https://www.renner.uno` und `wp plugin delete wpgiftregistry`.
**Bekannte Kopplung:** `hetzner-s3-creds` liest weiterhin `seed/paperless/hetzner-creds`. Eine Rotation dieser Zugangsdaten trifft damit auch das WordPress-Backup.
🤖Automated review — model CLI default (complexity: complex), commit b784ce3d38c346716ead412ff12283aff01f5663.
Advisory review by the review-bot pipeline. It never approves; a human merges. Push new commits to get a fresh review.
Summary
This PR migrates www.renner.uno off a Hetzner VM into the cluster: a new mariadb-operator (WordPress needs real MySQL, not pg4wp), a WordPress Deployment/PVC/Ingress split so the public blog stays open while /wp-admin and /wp-login.php sit behind a new Authentik wordpress-gate outpost, backups (operator logical dump + Velero PVC), and the supporting network policies/OpenTofu bucket. The change is well-reasoned and consistently follows this repo's existing conventions (paperless/zone-configurator gate pattern, CNPG-style per-consumer DB placement, Velero labelSelector exclusion for the DB PVC). I found one real defect: a security-relevant comment references a network-policy file that was never added to this PR. No secrets are leaked in the diff.
🔴 Blocking
kubernetes/infrastructure/network-policies/wordpress-ns.yaml:781 (and the near-identical comment in kubernetes/apps/authentik/blueprints-configmap.yaml:9-21): both claim the public/admin split is enforced by "the narrower rule in kubernetes/apps/wordpress/network-policy.yaml" — quote: "The narrower rule in kubernetes/apps/wordpress/network-policy.yaml is what keeps that split honest." That file does not exist anywhere in this PR or the checkout (kubernetes/apps/wordpress/kustomization.yaml lists only externalsecret.yaml, mariadb.yaml, pvc.yaml, deployment.yaml, gate-service.yaml, ingress.yaml; the full 21-file diff has no such path). Compare to the actual precedent this PR is modeling itself on, kubernetes/apps/paperless/network-policy.yaml, which really does exist and scopes ingress to only the gate outpost via a workload-level endpointSelector.
On inspection this isn't just a stale comment pointing at a forgotten file — a paperless-style workload-scoped policy wouldn't even do anything extra here, because (unlike paperless) traefik must already reach the wordpress pod directly and unrestricted for the public / route (see ingress.yaml's public Ingress → wordpress:80). So the "split" between public and gated paths is enforced entirely by the two Ingress objects' path rules (/ vs /wp-admin, /wp-login.php) plus the Authentik outpost — not by any network policy — and the comment's claim that a CNP "keeps that split honest" is simply false as written. It's misleading: a future reader auditing this namespace's defense-in-depth would go looking for a file that isn't there and wrongly conclude a network-layer control exists that doesn't.
Fix by either (a) writing the intended file if there's a real workload-scoped rule to add, or (b) removing/correcting the claim in both comments to accurately describe that the split is Ingress/Authentik-enforced, not network-policy-enforced.
🟡 Suggestions
kubernetes/apps/wordpress/ingress.yaml: this is the first place in the repo that puts two separate Ingress objects on the same host (www.renner.uno), relying on only the first (wordpress) declaring the tls: block/cert-manager.io/cluster-issuer annotation, with the second (wordpress-admin) deliberately omitting both and depending on Traefik's shared TLS-cert store to pick up the certificate by SNI for routers from the second Ingress too. This is generally correct Traefik behavior (TLS certs discovered via spec.tls are added to a store keyed by SNI/host, independent of which Ingress resource supplied the router), but there's no other example of this exact split-Ingress/shared-cert pattern elsewhere in this repo to lean on, so it's worth confirming in the actual cluster after this deploys — hit https://www.renner.uno/wp-admin/ and check you get a valid cert (not a default/self-signed fallback) rather than assuming it from the comment alone.
kubernetes/infrastructure/post-crds/velero-schedules.yaml:975-977: the exclusion labelSelector: matchExpressions: key: app.kubernetes.io/instance, operator: NotIn, values: [wordpress-mariadb] assumes mariadb-operator labels the data PVC it creates for the wordpress-mariadb MariaDB CR with app.kubernetes.io/instance: wordpress-mariadb. The existing CNPG equivalent (trmnl-daily, same file, a few lines up) instead keys off cnpg.io/cluster: DoesNotExist, a label CNPG is known to set. If mariadb-operator's actual instance label differs (or is absent), this selector silently fails to exclude the DB PVC and Velero will also snapshot the live database volume — not data-lossy, just redundant with the operator's own logical Backup, and worth a one-time check (kubectl get pvc -n wordpress --show-labels) after first deploy.
kubernetes/apps/wordpress/deployment.yaml:203-204: WORDPRESS_DB_HOST: wordpress-mariadb.wordpress.svc.cluster.local:3306 assumes the mariadb-operator Service name equals the MariaDB CR name (wordpress-mariadb) exactly. Worth a quick post-deploy sanity check (kubectl get svc -n wordpress) since a naming mismatch here would just fail the pod's DB connection loudly at startup rather than silently, but it's an easy thing to catch in the PR description's own "verified, not assumed" spirit.
🟢 Nits
None beyond the above.
Tests
There's no unit-test layer for GitOps YAML in this repo; the relevant gate is .woodpecker/ci-manifests-test.yml's kustomize-build step, which builds every kustomization in the tree (including the new kubernetes/apps/wordpress/, kubernetes/infrastructure/mariadb-operator/, and the network-policies/authentik additions) and would catch a resource entry pointing at a missing file or a bad patch target. I manually cross-checked every kustomization.yaml touched or added by this PR against the files it lists and all resolve to real files, and manually verified the dependsOn targets in kubernetes/clusters/homelab/wordpress.yaml (infrastructure, infrastructure-crds, infrastructure-ceph-cluster) exist and match the same pattern every other app-of-apps Kustomization uses. kustomize-build will not catch the dangling comment reference above since it's a comment, not a broken resource — that one needed a manual read. I did not have kubectl/kustomize available in this environment to actually run the build myself.
Recommendation
Request changes — 1 blocking issue.
🤖 **Automated review** — model `CLI default` (complexity: complex), commit `b784ce3d38c346716ead412ff12283aff01f5663`.
> Advisory review by the `review-bot` pipeline. It never approves; a human merges. Push new commits to get a fresh review.
---
## Summary
This PR migrates www.renner.uno off a Hetzner VM into the cluster: a new `mariadb-operator` (WordPress needs real MySQL, not pg4wp), a WordPress Deployment/PVC/Ingress split so the public blog stays open while `/wp-admin` and `/wp-login.php` sit behind a new Authentik `wordpress-gate` outpost, backups (operator logical dump + Velero PVC), and the supporting network policies/OpenTofu bucket. The change is well-reasoned and consistently follows this repo's existing conventions (paperless/zone-configurator gate pattern, CNPG-style per-consumer DB placement, Velero labelSelector exclusion for the DB PVC). I found one real defect: a security-relevant comment references a network-policy file that was never added to this PR. No secrets are leaked in the diff.
## 🔴 Blocking
- **`kubernetes/infrastructure/network-policies/wordpress-ns.yaml:781`** (and the near-identical comment in `kubernetes/apps/authentik/blueprints-configmap.yaml:9-21`): both claim the public/admin split is enforced by "the narrower rule in `kubernetes/apps/wordpress/network-policy.yaml`" — quote: *"The narrower rule in kubernetes/apps/wordpress/network-policy.yaml is what keeps that split honest."* That file does not exist anywhere in this PR or the checkout (`kubernetes/apps/wordpress/kustomization.yaml` lists only `externalsecret.yaml, mariadb.yaml, pvc.yaml, deployment.yaml, gate-service.yaml, ingress.yaml`; the full 21-file diff has no such path). Compare to the actual precedent this PR is modeling itself on, `kubernetes/apps/paperless/network-policy.yaml`, which really does exist and scopes ingress to only the gate outpost via a workload-level `endpointSelector`.
On inspection this isn't just a stale comment pointing at a forgotten file — a paperless-style workload-scoped policy wouldn't even do anything extra here, because (unlike paperless) `traefik` must already reach the `wordpress` pod directly and unrestricted for the public `/` route (see `ingress.yaml`'s public Ingress → `wordpress:80`). So the "split" between public and gated paths is enforced entirely by the two Ingress objects' path rules (`/` vs `/wp-admin`, `/wp-login.php`) plus the Authentik outpost — not by any network policy — and the comment's claim that a CNP "keeps that split honest" is simply false as written. It's misleading: a future reader auditing this namespace's defense-in-depth would go looking for a file that isn't there and wrongly conclude a network-layer control exists that doesn't.
Fix by either (a) writing the intended file if there's a real workload-scoped rule to add, or (b) removing/correcting the claim in both comments to accurately describe that the split is Ingress/Authentik-enforced, not network-policy-enforced.
## 🟡 Suggestions
- **`kubernetes/apps/wordpress/ingress.yaml`**: this is the first place in the repo that puts two separate `Ingress` objects on the *same host* (`www.renner.uno`), relying on only the first (`wordpress`) declaring the `tls:` block/`cert-manager.io/cluster-issuer` annotation, with the second (`wordpress-admin`) deliberately omitting both and depending on Traefik's shared TLS-cert store to pick up the certificate by SNI for routers from the second Ingress too. This is generally correct Traefik behavior (TLS certs discovered via `spec.tls` are added to a store keyed by SNI/host, independent of which Ingress resource supplied the router), but there's no other example of this exact split-Ingress/shared-cert pattern elsewhere in this repo to lean on, so it's worth confirming in the actual cluster after this deploys — hit `https://www.renner.uno/wp-admin/` and check you get a valid cert (not a default/self-signed fallback) rather than assuming it from the comment alone.
- **`kubernetes/infrastructure/post-crds/velero-schedules.yaml:975-977`**: the exclusion `labelSelector: matchExpressions: key: app.kubernetes.io/instance, operator: NotIn, values: [wordpress-mariadb]` assumes mariadb-operator labels the data PVC it creates for the `wordpress-mariadb` MariaDB CR with `app.kubernetes.io/instance: wordpress-mariadb`. The existing CNPG equivalent (`trmnl-daily`, same file, a few lines up) instead keys off `cnpg.io/cluster: DoesNotExist`, a label CNPG is known to set. If mariadb-operator's actual instance label differs (or is absent), this selector silently fails to exclude the DB PVC and Velero will also snapshot the live database volume — not data-lossy, just redundant with the operator's own logical `Backup`, and worth a one-time check (`kubectl get pvc -n wordpress --show-labels`) after first deploy.
- **`kubernetes/apps/wordpress/deployment.yaml:203-204`**: `WORDPRESS_DB_HOST: wordpress-mariadb.wordpress.svc.cluster.local:3306` assumes the mariadb-operator Service name equals the MariaDB CR name (`wordpress-mariadb`) exactly. Worth a quick post-deploy sanity check (`kubectl get svc -n wordpress`) since a naming mismatch here would just fail the pod's DB connection loudly at startup rather than silently, but it's an easy thing to catch in the PR description's own "verified, not assumed" spirit.
## 🟢 Nits
None beyond the above.
## Tests
There's no unit-test layer for GitOps YAML in this repo; the relevant gate is `.woodpecker/ci-manifests-test.yml`'s `kustomize-build` step, which builds every kustomization in the tree (including the new `kubernetes/apps/wordpress/`, `kubernetes/infrastructure/mariadb-operator/`, and the network-policies/authentik additions) and would catch a resource entry pointing at a missing file or a bad patch target. I manually cross-checked every `kustomization.yaml` touched or added by this PR against the files it lists and all resolve to real files, and manually verified the `dependsOn` targets in `kubernetes/clusters/homelab/wordpress.yaml` (`infrastructure`, `infrastructure-crds`, `infrastructure-ceph-cluster`) exist and match the same pattern every other app-of-apps Kustomization uses. `kustomize-build` will not catch the dangling comment reference above since it's a comment, not a broken resource — that one needed a manual read. I did not have `kubectl`/`kustomize` available in this environment to actually run the build myself.
## Recommendation
Request changes — 1 blocking issue.
<!-- pr-review:sha=b784ce3d38c346716ead412ff12283aff01f5663 -->
Addresses the automated review on #492.
Blocking: two comments claimed the public/admin split was kept honest by
kubernetes/apps/wordpress/network-policy.yaml. That file does not exist — the
comment was written against the paperless template and never updated once the
namespace policy proved sufficient. The deeper point in the review is the real
one: a paperless-style workload-scoped policy would be a no-op here, because
traefik must reach the pod directly for every public page. The split is
enforced by the Ingress path rules and Authentik, not at the network layer,
and the comment now says so — an auditor looking for a network control would
otherwise have gone hunting for a file that was never there.
Velero: the schedule excluded the database PVC by app.kubernetes.io/instance,
a label the mariadb-operator was only assumed to set. A wrong assumption there
fails silently and sweeps the live database volume into the snapshot. Inverted
to select the content PVC by labels this repo sets on it, the same direction
openbao-daily already uses.
The Service-name assumption flagged as a suggestion is kept: the CRD's separate
primaryService/secondaryService fields indicate the base Service carries the
resource name, and a mismatch fails loudly at pod startup. Written down as a
first-deploy check rather than left implicit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014giKN9N6e5J2DF8QZmVh98
🤖Automated review — model CLI default (complexity: complex), commit aa37fb0a0a8814a3fd812b395cd8401a00cba012.
Advisory review by the review-bot pipeline. It never approves; a human merges. Push new commits to get a fresh review.
Summary
Migrates the renner.uno WordPress site off a Hetzner VM into the cluster: a new mariadb-operator (WordPress needs real MySQL, not the pg4wp shim), a WordPress Deployment/Service/PVC/Ingress, an Authentik proxy-gate for /wp-admin + /wp-login.php only, network policy, and backup/DR wiring. I cross-checked every non-obvious design choice (gate egress scoping, database-namespace ingress rule, OCIRepository chart pattern, PVC/Velero label selection, resource-limit conventions, secret naming) against the closest existing analogue in the repo (mostly paperless) and everything lines up — this PR follows established patterns faithfully rather than improvising. No blocking issues found.
🟡 Suggestions
kubernetes/apps/wordpress/deployment.yaml:40-41 — WORDPRESS_DB_HOST is a guess at the mariadb-operator's default Service name (wordpress-mariadb.wordpress.svc.cluster.local), and the comment says as much ("Not verified against a running cluster"). That's an honest disclosure and the failure mode is loud (pod CrashLoopBackOff on bad DB host) rather than silent, so this isn't blocking — but since the PR body doesn't mention this as a manual follow-up step, it'd be worth calling out in the PR description too, so whoever merges/applies remembers to run the suggested kubectl get svc -n wordpress check before assuming the site is live.
kubernetes/apps/wordpress/externalsecret.yaml:328-335 — hetzner-s3-creds reuses paperless's OpenBao path (seed/paperless/hetzner-creds) rather than a wordpress-specific one. This is called out with a FOLLOW-UP note and is a reasonable stopgap (shared static creds, same bucket-prefix approach woodpecker used), but flagging it here too since it means a credential rotation or scope change for paperless's S3 creds silently also affects wordpress's backup path — worth remembering when that FOLLOW-UP is picked up.
kubernetes/apps/wordpress/deployment.yaml:249-260 — readinessProbe/livenessProbe both hit path: / on the plain WordPress container port. Since WP_HOME/WP_SITEURL are pinned to https://www.renner.uno, and the probe originates from kubelet with no matching Host header/TLS, WordPress may 301-redirect the probe request. That's still a "success" for Kubernetes' httpGet probe (2xx-399 all count), so this isn't a functional bug — just worth knowing if the probe response ever needs inspecting during a live debug session.
🟢 Nits
opentofu/object_storage.tf:1019-1024 — the realignment of the hetzner_buckets_noncurrent_expiry_days map (spacing changed on the pre-existing cnpg/velero lines) is unrelated to the new mariadb entry; harmless, just a slightly noisier diff than necessary.
Tests
This is a GitOps/infra PR (Kubernetes manifests, Flux Kustomizations, Terraform); there's no unit-test harness for it in this repo, consistent with how paperless/trmnl were added — reconciliation success and the healthCheckExprs gates are the closest thing to a test here, and both are present and modeled correctly (kubernetes/clusters/homelab/wordpress.yaml's CEL expressions mirror the existing CNPG/CephCluster ones). No gaps beyond what's already flagged above.
Recommendation
Looks reasonable — no blocking issues; comments above are optional.
🤖 **Automated review** — model `CLI default` (complexity: complex), commit `aa37fb0a0a8814a3fd812b395cd8401a00cba012`.
> Advisory review by the `review-bot` pipeline. It never approves; a human merges. Push new commits to get a fresh review.
---
## Summary
Migrates the `renner.uno` WordPress site off a Hetzner VM into the cluster: a new `mariadb-operator` (WordPress needs real MySQL, not the pg4wp shim), a WordPress Deployment/Service/PVC/Ingress, an Authentik proxy-gate for `/wp-admin` + `/wp-login.php` only, network policy, and backup/DR wiring. I cross-checked every non-obvious design choice (gate egress scoping, `database`-namespace ingress rule, OCIRepository chart pattern, PVC/Velero label selection, resource-limit conventions, secret naming) against the closest existing analogue in the repo (mostly `paperless`) and everything lines up — this PR follows established patterns faithfully rather than improvising. No blocking issues found.
## 🟡 Suggestions
- `kubernetes/apps/wordpress/deployment.yaml:40-41` — `WORDPRESS_DB_HOST` is a guess at the mariadb-operator's default Service name (`wordpress-mariadb.wordpress.svc.cluster.local`), and the comment says as much ("Not verified against a running cluster"). That's an honest disclosure and the failure mode is loud (pod CrashLoopBackOff on bad DB host) rather than silent, so this isn't blocking — but since the PR body doesn't mention this as a manual follow-up step, it'd be worth calling out in the PR description too, so whoever merges/applies remembers to run the suggested `kubectl get svc -n wordpress` check before assuming the site is live.
- `kubernetes/apps/wordpress/externalsecret.yaml:328-335` — `hetzner-s3-creds` reuses paperless's OpenBao path (`seed/paperless/hetzner-creds`) rather than a wordpress-specific one. This is called out with a FOLLOW-UP note and is a reasonable stopgap (shared static creds, same bucket-prefix approach woodpecker used), but flagging it here too since it means a credential rotation or scope change for paperless's S3 creds silently also affects wordpress's backup path — worth remembering when that FOLLOW-UP is picked up.
- `kubernetes/apps/wordpress/deployment.yaml:249-260` — `readinessProbe`/`livenessProbe` both hit `path: /` on the plain WordPress container port. Since `WP_HOME`/`WP_SITEURL` are pinned to `https://www.renner.uno`, and the probe originates from kubelet with no matching Host header/TLS, WordPress may 301-redirect the probe request. That's still a "success" for Kubernetes' httpGet probe (2xx-399 all count), so this isn't a functional bug — just worth knowing if the probe response ever needs inspecting during a live debug session.
## 🟢 Nits
- `opentofu/object_storage.tf:1019-1024` — the realignment of the `hetzner_buckets_noncurrent_expiry_days` map (spacing changed on the pre-existing `cnpg`/`velero` lines) is unrelated to the new `mariadb` entry; harmless, just a slightly noisier diff than necessary.
## Tests
This is a GitOps/infra PR (Kubernetes manifests, Flux Kustomizations, Terraform); there's no unit-test harness for it in this repo, consistent with how `paperless`/`trmnl` were added — reconciliation success and the `healthCheckExprs` gates are the closest thing to a test here, and both are present and modeled correctly (`kubernetes/clusters/homelab/wordpress.yaml`'s CEL expressions mirror the existing CNPG/CephCluster ones). No gaps beyond what's already flagged above.
## Recommendation
Looks reasonable — no blocking issues; comments above are optional.
<!-- pr-review:sha=aa37fb0a0a8814a3fd812b395cd8401a00cba012 -->
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.
Moves the WordPress behind renner.uno off the Hetzner VM
wordpress-hochzeit(Ubuntu 22.04, PHP 8.1, MySQL 8.0) and into the cluster aswww.renner.uno.Why now
The site is not the frozen wedding archive it looks like. It carries an unpublished, fully composed birth announcement from 2026-05-14 (columns, image, pullquote) and a second account —
s.birzer, Editor. So wp-admin has to keep working for a human author; a static export or a Git-only workflow would have frozen the site right before the thing someone was about to publish.Shape
infrastructure/. WordPress speaks MySQL natively; pg4wp lists WordPress 6.5.3 as its newest tested version while this site runs 7.0.4./goes straight to WordPress so the blog stays publicly readable;/wp-adminand/wp-login.phproute through awordpress-gateoutpost. The paperless-style inverse — gate the host,skip_path_regexthe public paths — needs a negative lookahead, and the outpost's RE2 has none.wp-contentis persisted. Upgrading is a tag bump.Verified, not assumed
/wp-adminis safe becausewpgiftregistryis dropped: it was the only emitter ofajaxurlon this site, and of 121 admin-ajax hits in the live log window, none returned 200.utf8mb4_0900_ai_ciappears on exactly 7 tables, all from the dead RSVP plugin — 6 empty, 1 holding 5 lookup rows. They are excluded from the migration dump, so the incompatibility disappears rather than being rewritten.baseline, so the official Apache image needs no namespace label.Still open
The hostname
www.renner.unomust be allowed as SNI on the NetBird reverse proxy. That configuration lives in NetBird management, not in this repo.🤖 Generated with Claude Code
https://claude.ai/code/session_014giKN9N6e5J2DF8QZmVh98
Nach dem Merge zu tun
kubectl get svc -n wordpress— bestätigen, dass der Servicewordpress-mariadbheißt.WORDPRESS_DB_HOSTist darauf fest verdrahtet; bei Abweichung crashloopt der Pod (laut, nicht still).kubectl get pvc -n wordpress --show-labels— die Velero-Auswahl greift aufapp.kubernetes.io/name=wordpress,component=content.https://www.renner.uno/wp-admin/aufrufen und das Zertifikat prüfen: der zweite Ingress bringt keinen eigenentls:-Block mit, sondern verlässt sich auf den gemeinsamen SNI-Zertifikatsspeicher. Erstes Vorkommen dieses Musters in diesem Repo.db-migration.sqlundwp-contenteinspielen, dannwp search-replace https://renner.uno https://www.renner.unoundwp plugin delete wpgiftregistry.Bekannte Kopplung:
hetzner-s3-credsliest weiterhinseed/paperless/hetzner-creds. Eine Rotation dieser Zugangsdaten trifft damit auch das WordPress-Backup.4640397659tob784ce3d38🤖 Automated review — model
CLI default(complexity: complex), commitb784ce3d38c346716ead412ff12283aff01f5663.Summary
This PR migrates www.renner.uno off a Hetzner VM into the cluster: a new
mariadb-operator(WordPress needs real MySQL, not pg4wp), a WordPress Deployment/PVC/Ingress split so the public blog stays open while/wp-adminand/wp-login.phpsit behind a new Authentikwordpress-gateoutpost, backups (operator logical dump + Velero PVC), and the supporting network policies/OpenTofu bucket. The change is well-reasoned and consistently follows this repo's existing conventions (paperless/zone-configurator gate pattern, CNPG-style per-consumer DB placement, Velero labelSelector exclusion for the DB PVC). I found one real defect: a security-relevant comment references a network-policy file that was never added to this PR. No secrets are leaked in the diff.🔴 Blocking
kubernetes/infrastructure/network-policies/wordpress-ns.yaml:781(and the near-identical comment inkubernetes/apps/authentik/blueprints-configmap.yaml:9-21): both claim the public/admin split is enforced by "the narrower rule inkubernetes/apps/wordpress/network-policy.yaml" — quote: "The narrower rule in kubernetes/apps/wordpress/network-policy.yaml is what keeps that split honest." That file does not exist anywhere in this PR or the checkout (kubernetes/apps/wordpress/kustomization.yamllists onlyexternalsecret.yaml, mariadb.yaml, pvc.yaml, deployment.yaml, gate-service.yaml, ingress.yaml; the full 21-file diff has no such path). Compare to the actual precedent this PR is modeling itself on,kubernetes/apps/paperless/network-policy.yaml, which really does exist and scopes ingress to only the gate outpost via a workload-levelendpointSelector.On inspection this isn't just a stale comment pointing at a forgotten file — a paperless-style workload-scoped policy wouldn't even do anything extra here, because (unlike paperless)
traefikmust already reach thewordpresspod directly and unrestricted for the public/route (seeingress.yaml's public Ingress →wordpress:80). So the "split" between public and gated paths is enforced entirely by the two Ingress objects' path rules (/vs/wp-admin,/wp-login.php) plus the Authentik outpost — not by any network policy — and the comment's claim that a CNP "keeps that split honest" is simply false as written. It's misleading: a future reader auditing this namespace's defense-in-depth would go looking for a file that isn't there and wrongly conclude a network-layer control exists that doesn't.Fix by either (a) writing the intended file if there's a real workload-scoped rule to add, or (b) removing/correcting the claim in both comments to accurately describe that the split is Ingress/Authentik-enforced, not network-policy-enforced.
🟡 Suggestions
kubernetes/apps/wordpress/ingress.yaml: this is the first place in the repo that puts two separateIngressobjects on the same host (www.renner.uno), relying on only the first (wordpress) declaring thetls:block/cert-manager.io/cluster-issuerannotation, with the second (wordpress-admin) deliberately omitting both and depending on Traefik's shared TLS-cert store to pick up the certificate by SNI for routers from the second Ingress too. This is generally correct Traefik behavior (TLS certs discovered viaspec.tlsare added to a store keyed by SNI/host, independent of which Ingress resource supplied the router), but there's no other example of this exact split-Ingress/shared-cert pattern elsewhere in this repo to lean on, so it's worth confirming in the actual cluster after this deploys — hithttps://www.renner.uno/wp-admin/and check you get a valid cert (not a default/self-signed fallback) rather than assuming it from the comment alone.kubernetes/infrastructure/post-crds/velero-schedules.yaml:975-977: the exclusionlabelSelector: matchExpressions: key: app.kubernetes.io/instance, operator: NotIn, values: [wordpress-mariadb]assumes mariadb-operator labels the data PVC it creates for thewordpress-mariadbMariaDB CR withapp.kubernetes.io/instance: wordpress-mariadb. The existing CNPG equivalent (trmnl-daily, same file, a few lines up) instead keys offcnpg.io/cluster: DoesNotExist, a label CNPG is known to set. If mariadb-operator's actual instance label differs (or is absent), this selector silently fails to exclude the DB PVC and Velero will also snapshot the live database volume — not data-lossy, just redundant with the operator's own logicalBackup, and worth a one-time check (kubectl get pvc -n wordpress --show-labels) after first deploy.kubernetes/apps/wordpress/deployment.yaml:203-204:WORDPRESS_DB_HOST: wordpress-mariadb.wordpress.svc.cluster.local:3306assumes the mariadb-operator Service name equals the MariaDB CR name (wordpress-mariadb) exactly. Worth a quick post-deploy sanity check (kubectl get svc -n wordpress) since a naming mismatch here would just fail the pod's DB connection loudly at startup rather than silently, but it's an easy thing to catch in the PR description's own "verified, not assumed" spirit.🟢 Nits
None beyond the above.
Tests
There's no unit-test layer for GitOps YAML in this repo; the relevant gate is
.woodpecker/ci-manifests-test.yml'skustomize-buildstep, which builds every kustomization in the tree (including the newkubernetes/apps/wordpress/,kubernetes/infrastructure/mariadb-operator/, and the network-policies/authentik additions) and would catch a resource entry pointing at a missing file or a bad patch target. I manually cross-checked everykustomization.yamltouched or added by this PR against the files it lists and all resolve to real files, and manually verified thedependsOntargets inkubernetes/clusters/homelab/wordpress.yaml(infrastructure,infrastructure-crds,infrastructure-ceph-cluster) exist and match the same pattern every other app-of-apps Kustomization uses.kustomize-buildwill not catch the dangling comment reference above since it's a comment, not a broken resource — that one needed a manual read. I did not havekubectl/kustomizeavailable in this environment to actually run the build myself.Recommendation
Request changes — 1 blocking issue.
🤖 Automated review — model
CLI default(complexity: complex), commitaa37fb0a0a8814a3fd812b395cd8401a00cba012.Summary
Migrates the
renner.unoWordPress site off a Hetzner VM into the cluster: a newmariadb-operator(WordPress needs real MySQL, not the pg4wp shim), a WordPress Deployment/Service/PVC/Ingress, an Authentik proxy-gate for/wp-admin+/wp-login.phponly, network policy, and backup/DR wiring. I cross-checked every non-obvious design choice (gate egress scoping,database-namespace ingress rule, OCIRepository chart pattern, PVC/Velero label selection, resource-limit conventions, secret naming) against the closest existing analogue in the repo (mostlypaperless) and everything lines up — this PR follows established patterns faithfully rather than improvising. No blocking issues found.🟡 Suggestions
kubernetes/apps/wordpress/deployment.yaml:40-41—WORDPRESS_DB_HOSTis a guess at the mariadb-operator's default Service name (wordpress-mariadb.wordpress.svc.cluster.local), and the comment says as much ("Not verified against a running cluster"). That's an honest disclosure and the failure mode is loud (pod CrashLoopBackOff on bad DB host) rather than silent, so this isn't blocking — but since the PR body doesn't mention this as a manual follow-up step, it'd be worth calling out in the PR description too, so whoever merges/applies remembers to run the suggestedkubectl get svc -n wordpresscheck before assuming the site is live.kubernetes/apps/wordpress/externalsecret.yaml:328-335—hetzner-s3-credsreuses paperless's OpenBao path (seed/paperless/hetzner-creds) rather than a wordpress-specific one. This is called out with a FOLLOW-UP note and is a reasonable stopgap (shared static creds, same bucket-prefix approach woodpecker used), but flagging it here too since it means a credential rotation or scope change for paperless's S3 creds silently also affects wordpress's backup path — worth remembering when that FOLLOW-UP is picked up.kubernetes/apps/wordpress/deployment.yaml:249-260—readinessProbe/livenessProbeboth hitpath: /on the plain WordPress container port. SinceWP_HOME/WP_SITEURLare pinned tohttps://www.renner.uno, and the probe originates from kubelet with no matching Host header/TLS, WordPress may 301-redirect the probe request. That's still a "success" for Kubernetes' httpGet probe (2xx-399 all count), so this isn't a functional bug — just worth knowing if the probe response ever needs inspecting during a live debug session.🟢 Nits
opentofu/object_storage.tf:1019-1024— the realignment of thehetzner_buckets_noncurrent_expiry_daysmap (spacing changed on the pre-existingcnpg/velerolines) is unrelated to the newmariadbentry; harmless, just a slightly noisier diff than necessary.Tests
This is a GitOps/infra PR (Kubernetes manifests, Flux Kustomizations, Terraform); there's no unit-test harness for it in this repo, consistent with how
paperless/trmnlwere added — reconciliation success and thehealthCheckExprsgates are the closest thing to a test here, and both are present and modeled correctly (kubernetes/clusters/homelab/wordpress.yaml's CEL expressions mirror the existing CNPG/CephCluster ones). No gaps beyond what's already flagged above.Recommendation
Looks reasonable — no blocking issues; comments above are optional.