From b7d34da3d6dbced36c247ab9c463cabc4557d6ce Mon Sep 17 00:00:00 2001 From: Andrew Stoltz <1578013+astoltz@users.noreply.github.com> Date: Wed, 17 Jun 2026 23:47:07 -0500 Subject: [PATCH] deploy(updater): gate public UpdateCenter host --- README.md | 2 +- .../deployment-updatecenter-web.json | 20 +++++++++++-------- ...essroute-updatecenter-web-public-gx10.json | 2 +- .../FleetManifestLintTests.cs | 2 ++ .../02_public_method_allowlist.rego | 8 +++++++- .../08_public_readwrite_allowlist.rego | 2 -- 6 files changed, 23 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 6ef4c02..03518bc 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ curl -sk -X DELETE https://dns.iamworkin.lan/api/v1/servers//zones/iam - **StatefulSet PVC drift**: `volumeClaimTemplates` needs explicit `volumeMode: Filesystem` or ArgoCD SSA self-heals forever. See memory `feedback_argocd_statefulset_pvc_drift.md`. - **IngressRoute namespace split**: this RKE2 Traefik install does not allow cross-namespace service refs. Keep the `IngressRoute`, backend `Service`, and TLS secret in the same namespace; if one host is shared across namespaces, duplicate the `Certificate` and move the route next to the destination service. - **Public read-only hosts**: if a public host fronts a service that also exposes admin writes internally, add a Traefik route match like `Host(...) && (Method(GET) || Method(HEAD))` on the public edge instead of trusting the app to reject unsafe methods. -- **Public read-write allowlist hosts**: if a public host accepts a tightly bounded write surface (e.g. bootstrap-JWT POST), pin the allowlist as `(Method(GET) || Method(HEAD) || Method(POST) || Method(OPTIONS))`. PUT/PATCH/DELETE must still 404 at the route. Track A's `updatecenter.iamworkin.lan` / `updates.iamworkin.lan` are the canonical example. The lint test enforces this invariant. +- **Public read-write allowlist hosts**: if a public host accepts a tightly bounded write surface (e.g. bootstrap-JWT POST), pin the allowlist as `(Method(GET) || Method(HEAD) || Method(POST) || Method(OPTIONS))`. PUT/PATCH/DELETE must still 404 at the route. Internal UpdateCenter hosts (`updatecenter.iamworkin.lan` / `updates.iamworkin.lan`) are the canonical example. Public UpdateCenter delivery hosts (`update.flowercore.io` / `updates.flowercore.io`) stay GET/HEAD-only and share-link gated until an explicit operator decision changes that posture. - **Traefik VIP netpols**: when a `NetworkPolicy` allows `10.0.56.200`, also allow the post-DNAT backend ports (`8443` for TLS plus `8080` or `8000` for HTTP) or Calico will drop the rewritten flow. - **Auth-safe probes**: services behind API-key or global auth middleware should prefer `tcpSocket` probes unless `/health` is explicitly exempted before the middleware runs. - **ArgoCD must use internal Gitea URL**: `http://gitea-clusterip.gitea.svc.cluster.local:3000/bluejay/bluejay-infra.git`, not the external HTTPS URL (step-ca cert isn't trusted by ArgoCD). The `ApplicationSet` and any hand-created `Application` must both use the internal URL. diff --git a/apps-gx10/fc-updater/deployment-updatecenter-web.json b/apps-gx10/fc-updater/deployment-updatecenter-web.json index b857454..d004350 100644 --- a/apps-gx10/fc-updater/deployment-updatecenter-web.json +++ b/apps-gx10/fc-updater/deployment-updatecenter-web.json @@ -53,13 +53,17 @@ "name": "FlowerCore__Updater__BundleStorage__LocalFs__RootDirectory", "value": "/data/bundles" }, - { - "name": "FlowerCore__Updater__PublicShares__RequirePublicVisibilityOnPublicHosts", - "value": "true" - }, - { - "name": "FlowerCore__Updater__PublicShares__Links__0__Code", - "value": "8f3c2a9e7d41" + { + "name": "FlowerCore__Updater__PublicShares__RequirePublicVisibilityOnPublicHosts", + "value": "true" + }, + { + "name": "FlowerCore__Updater__PublicShares__RequireShareLinkOnPublicHosts", + "value": "true" + }, + { + "name": "FlowerCore__Updater__PublicShares__Links__0__Code", + "value": "8f3c2a9e7d41" }, { "name": "FlowerCore__Updater__PublicShares__Links__0__AppId", @@ -195,7 +199,7 @@ "value": "26843545600" } ], - "image": "localhost/fc-updater-web:v20260617-sec5-913c6a9", + "image": "localhost/fc-updater-web:v20260618-public-exposure-6c0d0e4", "imagePullPolicy": "Never", "securityContext": { "allowPrivilegeEscalation": false, diff --git a/apps-gx10/fc-updater/ingressroute-updatecenter-web-public-gx10.json b/apps-gx10/fc-updater/ingressroute-updatecenter-web-public-gx10.json index 941d258..d3a69da 100644 --- a/apps-gx10/fc-updater/ingressroute-updatecenter-web-public-gx10.json +++ b/apps-gx10/fc-updater/ingressroute-updatecenter-web-public-gx10.json @@ -12,7 +12,7 @@ "routes": [ { "kind": "Rule", - "match": "(Host(`update.flowercore.io`) || Host(`updates.flowercore.io`)) && (Method(`GET`) || Method(`HEAD`) || Method(`POST`) || Method(`OPTIONS`))", + "match": "(Host(`update.flowercore.io`) || Host(`updates.flowercore.io`)) && (Method(`GET`) || Method(`HEAD`))", "priority": 100, "services": [ { diff --git a/tests/bluejay-infra-lint/FleetManifestLintTests.cs b/tests/bluejay-infra-lint/FleetManifestLintTests.cs index 3135a41..f2c35e9 100644 --- a/tests/bluejay-infra-lint/FleetManifestLintTests.cs +++ b/tests/bluejay-infra-lint/FleetManifestLintTests.cs @@ -16,6 +16,8 @@ public sealed class FleetManifestLintTests { "brochure.flowercore.io", "dist.flowercore.io", + "update.flowercore.io", + "updates.flowercore.io", }; // Hosts that allow a tightly bounded write surface in addition to GET/HEAD. diff --git a/tests/bluejay-infra-lint/conftest.dev/02_public_method_allowlist.rego b/tests/bluejay-infra-lint/conftest.dev/02_public_method_allowlist.rego index 1422287..cb216d8 100644 --- a/tests/bluejay-infra-lint/conftest.dev/02_public_method_allowlist.rego +++ b/tests/bluejay-infra-lint/conftest.dev/02_public_method_allowlist.rego @@ -1,6 +1,12 @@ package bluejayinfra.public_method_allowlist -public_hosts := {"brochure.flowercore.io", "dist.flowercore.io", "dns.iamworkin.lan"} +public_hosts := { + "brochure.flowercore.io", + "dist.flowercore.io", + "dns.iamworkin.lan", + "update.flowercore.io", + "updates.flowercore.io", +} deny[msg] { input.kind == "IngressRoute" diff --git a/tests/bluejay-infra-lint/conftest.dev/08_public_readwrite_allowlist.rego b/tests/bluejay-infra-lint/conftest.dev/08_public_readwrite_allowlist.rego index 00a701c..9386f5e 100644 --- a/tests/bluejay-infra-lint/conftest.dev/08_public_readwrite_allowlist.rego +++ b/tests/bluejay-infra-lint/conftest.dev/08_public_readwrite_allowlist.rego @@ -9,8 +9,6 @@ package bluejayinfra.public_readwrite_allowlist public_readwrite_hosts := { "updatecenter.iamworkin.lan", "updates.iamworkin.lan", - "update.flowercore.io", - "updates.flowercore.io", } required_methods := {"GET", "HEAD", "POST", "OPTIONS"}