deploy(updater): gate public UpdateCenter host

This commit is contained in:
Andrew Stoltz
2026-06-17 23:47:07 -05:00
parent 63fde0a593
commit b7d34da3d6
6 changed files with 23 additions and 13 deletions

View File

@@ -101,7 +101,7 @@ curl -sk -X DELETE https://dns.iamworkin.lan/api/v1/servers/<serverId>/zones/iam
- **StatefulSet PVC drift**: `volumeClaimTemplates` needs explicit `volumeMode: Filesystem` or ArgoCD SSA self-heals forever. See memory `feedback_argocd_statefulset_pvc_drift.md`. - **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. - **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-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. - **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. - **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. - **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.

View File

@@ -57,6 +57,10 @@
"name": "FlowerCore__Updater__PublicShares__RequirePublicVisibilityOnPublicHosts", "name": "FlowerCore__Updater__PublicShares__RequirePublicVisibilityOnPublicHosts",
"value": "true" "value": "true"
}, },
{
"name": "FlowerCore__Updater__PublicShares__RequireShareLinkOnPublicHosts",
"value": "true"
},
{ {
"name": "FlowerCore__Updater__PublicShares__Links__0__Code", "name": "FlowerCore__Updater__PublicShares__Links__0__Code",
"value": "8f3c2a9e7d41" "value": "8f3c2a9e7d41"
@@ -195,7 +199,7 @@
"value": "26843545600" "value": "26843545600"
} }
], ],
"image": "localhost/fc-updater-web:v20260617-sec5-913c6a9", "image": "localhost/fc-updater-web:v20260618-public-exposure-6c0d0e4",
"imagePullPolicy": "Never", "imagePullPolicy": "Never",
"securityContext": { "securityContext": {
"allowPrivilegeEscalation": false, "allowPrivilegeEscalation": false,

View File

@@ -12,7 +12,7 @@
"routes": [ "routes": [
{ {
"kind": "Rule", "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, "priority": 100,
"services": [ "services": [
{ {

View File

@@ -16,6 +16,8 @@ public sealed class FleetManifestLintTests
{ {
"brochure.flowercore.io", "brochure.flowercore.io",
"dist.flowercore.io", "dist.flowercore.io",
"update.flowercore.io",
"updates.flowercore.io",
}; };
// Hosts that allow a tightly bounded write surface in addition to GET/HEAD. // Hosts that allow a tightly bounded write surface in addition to GET/HEAD.

View File

@@ -1,6 +1,12 @@
package bluejayinfra.public_method_allowlist 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] { deny[msg] {
input.kind == "IngressRoute" input.kind == "IngressRoute"

View File

@@ -9,8 +9,6 @@ package bluejayinfra.public_readwrite_allowlist
public_readwrite_hosts := { public_readwrite_hosts := {
"updatecenter.iamworkin.lan", "updatecenter.iamworkin.lan",
"updates.iamworkin.lan", "updates.iamworkin.lan",
"update.flowercore.io",
"updates.flowercore.io",
} }
required_methods := {"GET", "HEAD", "POST", "OPTIONS"} required_methods := {"GET", "HEAD", "POST", "OPTIONS"}