From 417d3830aeeba095a01373cbe1f3d8ab686b337b Mon Sep 17 00:00:00 2001 From: Andrew Stoltz Date: Thu, 4 Jun 2026 15:40:57 -0500 Subject: [PATCH 1/2] test(lint): reconcile baseline infra assertions --- .../FleetManifestLintTests.cs | 48 ++++++++----------- 1 file changed, 20 insertions(+), 28 deletions(-) diff --git a/tests/bluejay-infra-lint/FleetManifestLintTests.cs b/tests/bluejay-infra-lint/FleetManifestLintTests.cs index 9c04772..34c7deb 100644 --- a/tests/bluejay-infra-lint/FleetManifestLintTests.cs +++ b/tests/bluejay-infra-lint/FleetManifestLintTests.cs @@ -17,21 +17,17 @@ public sealed class FleetManifestLintTests "dist.flowercore.io", }; - // Public hosts that allow a tightly bounded write surface in addition to - // GET/HEAD. updatecenter.iamworkin.lan accepts POST /api/v1/checkin/{id} + // Hosts that allow a tightly bounded write surface in addition to GET/HEAD. + // updatecenter.iamworkin.lan accepts POST /api/v1/checkin/{id} // (bootstrap-JWT) so its allowlist is GET||HEAD||POST||OPTIONS — but - // PUT/PATCH/DELETE must still 404 at the route. Anything wider than this - // set should fail this lint. - // - // PUB-1 (2026-05-06): update.flowercore.io / updates.flowercore.io were - // added for the Cloudflare-proxied public Update Center edge. They use the - // same bounded read-write allowlist as the LAN pair. + // PUT/PATCH/DELETE must still 404 at the route. Public + // update.flowercore.io remains a GET/HEAD download surface in the + // FlowerCore.Updater sibling manifest and is covered by the general + // public-method allowlist lint instead of this write-surface rule. private static readonly HashSet PublicReadWriteAllowlistHosts = new(StringComparer.Ordinal) { "updatecenter.iamworkin.lan", "updates.iamworkin.lan", - "update.flowercore.io", - "updates.flowercore.io", }; private static readonly HashSet ApiKeyProtectedDeployments = new(StringComparer.Ordinal) @@ -69,7 +65,7 @@ public sealed class FleetManifestLintTests ["github-runner-updater"] = "https://github.com/astoltz/FlowerCore.Updater", }; - private static readonly HashSet ScaledLinuxRunnerDeployments = new(StringComparer.Ordinal) + private static readonly HashSet RepoScopedLinuxRunnerDeployments = new(StringComparer.Ordinal) { "github-runner-sharedpos", "github-runner-puppet", @@ -271,17 +267,17 @@ public sealed class FleetManifestLintTests } [Fact] - public void GitHubRunnerFleet_MustAvoidRwoMultiAttachForScaledDeployments() + public void GitHubRunnerFleet_MustAvoidRwoMultiAttachForRepoScopedDeployments() { var deployments = GitHubRunnerDeployments(); - foreach (var deploymentName in ScaledLinuxRunnerDeployments) + foreach (var deploymentName in RepoScopedLinuxRunnerDeployments) { var deployment = deployments[deploymentName]; - // Scaled runners must have >= 2 replicas (avoid single-pod bottleneck). - // Individual deployments may be tuned upward per CI activity — see - // "runners: right-size replica counts per 14d CI activity (#24)". - ReplicaCount(deployment).Should().BeGreaterOrEqualTo(2, $"{deploymentName} is in the scaled set and must run with at least 2 replicas"); + // Sprint 34 ops trimmed runner load while the cluster was degraded + // to two healthy nodes. Repo-scoped runners can be tuned back above + // one replica, but they must stay RWO-safe before that happens. + ReplicaCount(deployment).Should().BeGreaterOrEqualTo(1, $"{deploymentName} must keep at least one repo-scoped runner online"); var volumes = deployment.MappingSequence("spec", "template", "spec", "volumes"); var claimNames = volumes @@ -289,7 +285,7 @@ public sealed class FleetManifestLintTests .Where(value => !string.IsNullOrWhiteSpace(value)) .ToList(); - claimNames.Should().BeEmpty($"{deploymentName} is scaled and must not share a RWO PVC"); + claimNames.Should().BeEmpty($"{deploymentName} must remain ready for safe multi-replica scaling without sharing a RWO PVC"); volumes.Should().Contain(volume => string.Equals(ManifestNodeExtensions.Scalar(volume, "name"), "nuget-cache", StringComparison.Ordinal) && ManifestNodeExtensions.Mapping(volume, "emptyDir") != null); @@ -612,7 +608,6 @@ public sealed class FleetManifestLintTests var expectedFiles = new[] { "1password-item.yaml", - "argocd-application.yaml", "certificate-web.yaml", "clusterrole-operator.yaml", "clusterrolebinding-operator.yaml", @@ -768,17 +763,14 @@ public sealed class FleetManifestLintTests } [Fact] - public void FcDeviceManagement_ArgocdApplicationMustMatchApplicationSetDiscoveryConventions() + public void FcDeviceManagement_MustRelyOnApplicationSetDiscovery() { - var application = FcDeviceManagementDocuments() - .Single(document => document.Kind == "Application" && document.Name == "infra-fc-devicemgmt"); + var documents = FcDeviceManagementDocuments(); - application.Namespace.Should().Be("argocd"); - application.Scalar("spec", "source", "repoURL") - .Should() - .Be("http://gitea-clusterip.gitea.svc.cluster.local:3000/bluejay/bluejay-infra.git"); - application.Scalar("spec", "source", "path").Should().Be("apps/fc-devicemgmt"); - application.Scalar("spec", "destination", "namespace").Should().Be("fc-devicemgmt"); + documents.Should().NotContain(document => document.Kind == "Application"); + + var ns = documents.Single(document => document.Kind == "Namespace" && document.Name == "fc-devicemgmt"); + ns.FileText.Should().Contain("ArgoCD discovers this directory as Application `infra-fc-devicemgmt`."); } [Fact] From c4b08f41ab7c100a0c3e66ae30411187dc9e0f02 Mon Sep 17 00:00:00 2001 From: Andrew Stoltz Date: Thu, 4 Jun 2026 15:55:07 -0500 Subject: [PATCH 2/2] feat(infra): prestage broader app exposure hardening --- apps/fc-aistation/fc-aistation.yaml | 26 ++++++ apps/fc-chat/fc-chat.yaml | 3 + apps/fc-desktop/fc-desktop.yaml | 23 +++++ apps/fc-devicemgmt/deployment-web.yaml | 3 + apps/fc-dms/fc-dms.yaml | 23 +++++ apps/fc-library/fc-library.yaml | 26 ++++++ apps/fc-llm-bridge/fc-llm-bridge.yaml | 26 ++++++ apps/fc-menuboard/fc-menuboard.yaml | 23 +++++ apps/fc-messageboard/fc-messageboard.yaml | 26 ++++++ apps/fc-mysql/fc-mysql.yaml | 23 +++++ apps/fc-php/fc-php.yaml | 23 +++++ apps/fc-presentations/fc-presentations.yaml | 23 +++++ apps/fc-retail/fc-retail.yaml | 26 ++++++ apps/fc-scoreboard/fc-scoreboard.yaml | 23 +++++ apps/fc-segmentdisplay/fc-segmentdisplay.yaml | 23 +++++ apps/fc-signage/fc-signage.yaml | 23 +++++ apps/fc-ttsreader/fc-ttsreader.yaml | 26 ++++++ apps/fc-updater/fc-updater.yaml | 4 + apps/knowledge/knowledge.yaml | 26 ++++++ apps/telephony/telephony.yaml | 5 +- apps/worldbuilder/worldbuilder.yaml | 26 ++++++ .../FleetManifestLintTests.cs | 86 +++++++++++++++++++ 22 files changed, 515 insertions(+), 1 deletion(-) diff --git a/apps/fc-aistation/fc-aistation.yaml b/apps/fc-aistation/fc-aistation.yaml index dc0fb37..7ab0490 100644 --- a/apps/fc-aistation/fc-aistation.yaml +++ b/apps/fc-aistation/fc-aistation.yaml @@ -46,6 +46,8 @@ spec: template: metadata: annotations: + fc.flowercore.io/healthz-anon: "true" + fc.flowercore.io/probe-path: "/healthz" prometheus.io/path: /metrics/prometheus prometheus.io/port: "5000" prometheus.io/scrape: "true" @@ -54,6 +56,7 @@ spec: app.kubernetes.io/part-of: flowercore spec: containers: + # fc-safe-to-expose: X-Forwarded-Proto handled by AddFlowerCoreWebAuth (ADR-178) before any future public/OIDC flip. - envFrom: - configMapRef: name: aistation-web-config @@ -167,3 +170,26 @@ spec: port: 80 tls: secretName: aistation-web-tls +# ---- PUBLIC HOST PRE-STAGING (DISABLED - Sprint 61+ exposure go-decision only) ---- +# When the operator decides to expose aistation-web publicly, uncomment + update the host, +# then verify the five safe-to-expose gates (authentik-safe-to-expose-readiness-2026-06-07.md section 2). +# +# --- IngressRoute --- +# apiVersion: traefik.io/v1alpha1 +# kind: IngressRoute +# metadata: +# name: aistation-web-public +# namespace: fc-aistation +# spec: +# entryPoints: [websecure] +# routes: +# - match: Host(`aistation.flowercore.io`) && (Method(`GET`) || Method(`HEAD`)) +# kind: Rule +# middlewares: +# - name: aistation-web-public-profile-header # injects entitlement profile +# services: +# - name: aistation-web +# port: 80 +# tls: {} +# # POST/PUT/PATCH/DELETE miss every route -> Traefik 404 -> no admin writes on the public surface. +# # Reference pattern: dist.flowercore.io (already live + method-gated; do not edit that one). diff --git a/apps/fc-chat/fc-chat.yaml b/apps/fc-chat/fc-chat.yaml index 90d41dd..85a5c50 100644 --- a/apps/fc-chat/fc-chat.yaml +++ b/apps/fc-chat/fc-chat.yaml @@ -112,6 +112,8 @@ spec: app.kubernetes.io/name: chat-web app.kubernetes.io/part-of: flowercore annotations: + fc.flowercore.io/healthz-anon: "true" + fc.flowercore.io/probe-path: "/healthz" prometheus.io/scrape: "true" prometheus.io/port: "8080" prometheus.io/path: "/metrics/prometheus" @@ -128,6 +130,7 @@ spec: ports: - name: http containerPort: 8080 + # fc-safe-to-expose: X-Forwarded-Proto handled by AddFlowerCoreWebAuth (ADR-178) before any future public/OIDC flip. envFrom: - configMapRef: name: chat-web-config diff --git a/apps/fc-desktop/fc-desktop.yaml b/apps/fc-desktop/fc-desktop.yaml index 6a45b07..9773152 100644 --- a/apps/fc-desktop/fc-desktop.yaml +++ b/apps/fc-desktop/fc-desktop.yaml @@ -51,3 +51,26 @@ spec: port: 8080 tls: secretName: remotedesktop-web-tls +# ---- PUBLIC HOST PRE-STAGING (DISABLED - Sprint 61+ exposure go-decision only) ---- +# When the operator decides to expose remotedesktop-web publicly, uncomment + update the host, +# then verify the five safe-to-expose gates (authentik-safe-to-expose-readiness-2026-06-07.md section 2). +# +# --- IngressRoute --- +# apiVersion: traefik.io/v1alpha1 +# kind: IngressRoute +# metadata: +# name: remotedesktop-web-public +# namespace: fc-desktop +# spec: +# entryPoints: [websecure] +# routes: +# - match: Host(`desktop.flowercore.io`) && (Method(`GET`) || Method(`HEAD`)) +# kind: Rule +# middlewares: +# - name: remotedesktop-web-public-profile-header # injects entitlement profile +# services: +# - name: remotedesktop-web +# port: 8080 +# tls: {} +# # POST/PUT/PATCH/DELETE miss every route -> Traefik 404 -> no admin writes on the public surface. +# # Reference pattern: dist.flowercore.io (already live + method-gated; do not edit that one). diff --git a/apps/fc-devicemgmt/deployment-web.yaml b/apps/fc-devicemgmt/deployment-web.yaml index a8caffd..fbcf0d6 100644 --- a/apps/fc-devicemgmt/deployment-web.yaml +++ b/apps/fc-devicemgmt/deployment-web.yaml @@ -52,6 +52,8 @@ spec: flowercore.io/tenant-id: system flowercore.io/created-by: bluejay-infra annotations: + fc.flowercore.io/healthz-anon: "true" + fc.flowercore.io/probe-path: "/healthz" prometheus.io/scrape: "true" prometheus.io/port: "8080" prometheus.io/path: "/metrics" @@ -67,6 +69,7 @@ spec: ports: - name: http containerPort: 8080 + # fc-safe-to-expose: X-Forwarded-Proto handled by AddFlowerCoreWebAuth (ADR-178) before any future public/OIDC flip. env: - name: ASPNETCORE_URLS value: "http://+:8080" diff --git a/apps/fc-dms/fc-dms.yaml b/apps/fc-dms/fc-dms.yaml index e7ed166..1014666 100644 --- a/apps/fc-dms/fc-dms.yaml +++ b/apps/fc-dms/fc-dms.yaml @@ -30,3 +30,26 @@ spec: port: 80 tls: secretName: dms-web-tls +# ---- PUBLIC HOST PRE-STAGING (DISABLED - Sprint 61+ exposure go-decision only) ---- +# When the operator decides to expose dms-web publicly, uncomment + update the host, +# then verify the five safe-to-expose gates (authentik-safe-to-expose-readiness-2026-06-07.md section 2). +# +# --- IngressRoute --- +# apiVersion: traefik.io/v1alpha1 +# kind: IngressRoute +# metadata: +# name: dms-web-public +# namespace: fc-dms +# spec: +# entryPoints: [websecure] +# routes: +# - match: Host(`dms.flowercore.io`) && (Method(`GET`) || Method(`HEAD`)) +# kind: Rule +# middlewares: +# - name: dms-web-public-profile-header # injects entitlement profile +# services: +# - name: dms-web +# port: 80 +# tls: {} +# # POST/PUT/PATCH/DELETE miss every route -> Traefik 404 -> no admin writes on the public surface. +# # Reference pattern: dist.flowercore.io (already live + method-gated; do not edit that one). diff --git a/apps/fc-library/fc-library.yaml b/apps/fc-library/fc-library.yaml index e3c5d7d..b17a088 100644 --- a/apps/fc-library/fc-library.yaml +++ b/apps/fc-library/fc-library.yaml @@ -46,6 +46,8 @@ spec: template: metadata: annotations: + fc.flowercore.io/healthz-anon: "true" + fc.flowercore.io/probe-path: "/health" prometheus.io/path: /metrics/prometheus prometheus.io/port: "5000" prometheus.io/scrape: "true" @@ -54,6 +56,7 @@ spec: app.kubernetes.io/part-of: flowercore spec: containers: + # fc-safe-to-expose: X-Forwarded-Proto handled by AddFlowerCoreWebAuth (ADR-178) before any future public/OIDC flip. - envFrom: - configMapRef: name: library-web-config @@ -167,3 +170,26 @@ spec: port: 80 tls: secretName: library-web-tls +# ---- PUBLIC HOST PRE-STAGING (DISABLED - Sprint 61+ exposure go-decision only) ---- +# When the operator decides to expose library-web publicly, uncomment + update the host, +# then verify the five safe-to-expose gates (authentik-safe-to-expose-readiness-2026-06-07.md section 2). +# +# --- IngressRoute --- +# apiVersion: traefik.io/v1alpha1 +# kind: IngressRoute +# metadata: +# name: library-web-public +# namespace: fc-library +# spec: +# entryPoints: [websecure] +# routes: +# - match: Host(`library.flowercore.io`) && (Method(`GET`) || Method(`HEAD`)) +# kind: Rule +# middlewares: +# - name: library-web-public-profile-header # injects entitlement profile +# services: +# - name: library-web +# port: 80 +# tls: {} +# # POST/PUT/PATCH/DELETE miss every route -> Traefik 404 -> no admin writes on the public surface. +# # Reference pattern: dist.flowercore.io (already live + method-gated; do not edit that one). diff --git a/apps/fc-llm-bridge/fc-llm-bridge.yaml b/apps/fc-llm-bridge/fc-llm-bridge.yaml index 476b799..171c1d4 100644 --- a/apps/fc-llm-bridge/fc-llm-bridge.yaml +++ b/apps/fc-llm-bridge/fc-llm-bridge.yaml @@ -83,6 +83,8 @@ spec: app.kubernetes.io/name: fc-llm-bridge app.kubernetes.io/part-of: flowercore annotations: + fc.flowercore.io/healthz-anon: "true" + fc.flowercore.io/probe-path: "/healthz" prometheus.io/scrape: "true" prometheus.io/port: "8080" prometheus.io/path: "/metrics" @@ -116,6 +118,7 @@ spec: ports: - containerPort: 8080 name: http + # fc-safe-to-expose: X-Forwarded-Proto handled by AddFlowerCoreWebAuth (ADR-178) before any future public/OIDC flip. env: - name: ASPNETCORE_URLS value: "http://+:8080" @@ -281,3 +284,26 @@ spec: port: 8080 tls: secretName: fc-llm-bridge-tls +# ---- PUBLIC HOST PRE-STAGING (DISABLED - Sprint 61+ exposure go-decision only) ---- +# When the operator decides to expose fc-llm-bridge publicly, uncomment + update the host, +# then verify the five safe-to-expose gates (authentik-safe-to-expose-readiness-2026-06-07.md section 2). +# +# --- IngressRoute --- +# apiVersion: traefik.io/v1alpha1 +# kind: IngressRoute +# metadata: +# name: fc-llm-bridge-public +# namespace: fc-llm-bridge +# spec: +# entryPoints: [websecure] +# routes: +# - match: Host(`llm-bridge.flowercore.io`) && (Method(`GET`) || Method(`HEAD`)) +# kind: Rule +# middlewares: +# - name: fc-llm-bridge-public-profile-header # injects entitlement profile +# services: +# - name: fc-llm-bridge +# port: 80 +# tls: {} +# # POST/PUT/PATCH/DELETE miss every route -> Traefik 404 -> no admin writes on the public surface. +# # Reference pattern: dist.flowercore.io (already live + method-gated; do not edit that one). diff --git a/apps/fc-menuboard/fc-menuboard.yaml b/apps/fc-menuboard/fc-menuboard.yaml index d2ffe2b..a8df603 100644 --- a/apps/fc-menuboard/fc-menuboard.yaml +++ b/apps/fc-menuboard/fc-menuboard.yaml @@ -30,3 +30,26 @@ spec: port: 80 tls: secretName: menuboard-web-tls +# ---- PUBLIC HOST PRE-STAGING (DISABLED - Sprint 61+ exposure go-decision only) ---- +# When the operator decides to expose menuboard-web publicly, uncomment + update the host, +# then verify the five safe-to-expose gates (authentik-safe-to-expose-readiness-2026-06-07.md section 2). +# +# --- IngressRoute --- +# apiVersion: traefik.io/v1alpha1 +# kind: IngressRoute +# metadata: +# name: menuboard-web-public +# namespace: fc-menuboard +# spec: +# entryPoints: [websecure] +# routes: +# - match: Host(`menuboard.flowercore.io`) && (Method(`GET`) || Method(`HEAD`)) +# kind: Rule +# middlewares: +# - name: menuboard-web-public-profile-header # injects entitlement profile +# services: +# - name: menuboard-web +# port: 80 +# tls: {} +# # POST/PUT/PATCH/DELETE miss every route -> Traefik 404 -> no admin writes on the public surface. +# # Reference pattern: dist.flowercore.io (already live + method-gated; do not edit that one). diff --git a/apps/fc-messageboard/fc-messageboard.yaml b/apps/fc-messageboard/fc-messageboard.yaml index 502221f..ae729dd 100644 --- a/apps/fc-messageboard/fc-messageboard.yaml +++ b/apps/fc-messageboard/fc-messageboard.yaml @@ -41,6 +41,8 @@ spec: labels: app: messageboard-web annotations: + fc.flowercore.io/healthz-anon: "true" + fc.flowercore.io/probe-path: "/healthz" prometheus.io/scrape: "true" prometheus.io/port: "8080" prometheus.io/path: "/metrics/prometheus" @@ -52,6 +54,7 @@ spec: ports: - containerPort: 8080 name: http + # fc-safe-to-expose: X-Forwarded-Proto handled by AddFlowerCoreWebAuth (ADR-178) before any future public/OIDC flip. envFrom: - configMapRef: name: messageboard-web-config @@ -141,3 +144,26 @@ spec: port: 80 tls: secretName: messageboard-web-tls +# ---- PUBLIC HOST PRE-STAGING (DISABLED - Sprint 61+ exposure go-decision only) ---- +# When the operator decides to expose messageboard-web publicly, uncomment + update the host, +# then verify the five safe-to-expose gates (authentik-safe-to-expose-readiness-2026-06-07.md section 2). +# +# --- IngressRoute --- +# apiVersion: traefik.io/v1alpha1 +# kind: IngressRoute +# metadata: +# name: messageboard-web-public +# namespace: fc-messageboard +# spec: +# entryPoints: [websecure] +# routes: +# - match: Host(`messageboard.flowercore.io`) && (Method(`GET`) || Method(`HEAD`)) +# kind: Rule +# middlewares: +# - name: messageboard-web-public-profile-header # injects entitlement profile +# services: +# - name: messageboard-web +# port: 80 +# tls: {} +# # POST/PUT/PATCH/DELETE miss every route -> Traefik 404 -> no admin writes on the public surface. +# # Reference pattern: dist.flowercore.io (already live + method-gated; do not edit that one). diff --git a/apps/fc-mysql/fc-mysql.yaml b/apps/fc-mysql/fc-mysql.yaml index 35e386d..e34f2d3 100644 --- a/apps/fc-mysql/fc-mysql.yaml +++ b/apps/fc-mysql/fc-mysql.yaml @@ -30,3 +30,26 @@ spec: port: 5300 tls: secretName: mysql-web-tls +# ---- PUBLIC HOST PRE-STAGING (DISABLED - Sprint 61+ exposure go-decision only) ---- +# When the operator decides to expose mysql-web publicly, uncomment + update the host, +# then verify the five safe-to-expose gates (authentik-safe-to-expose-readiness-2026-06-07.md section 2). +# +# --- IngressRoute --- +# apiVersion: traefik.io/v1alpha1 +# kind: IngressRoute +# metadata: +# name: mysql-web-public +# namespace: fc-mysql +# spec: +# entryPoints: [websecure] +# routes: +# - match: Host(`mysql.flowercore.io`) && (Method(`GET`) || Method(`HEAD`)) +# kind: Rule +# middlewares: +# - name: mysql-web-public-profile-header # injects entitlement profile +# services: +# - name: mysql-web +# port: 80 +# tls: {} +# # POST/PUT/PATCH/DELETE miss every route -> Traefik 404 -> no admin writes on the public surface. +# # Reference pattern: dist.flowercore.io (already live + method-gated; do not edit that one). diff --git a/apps/fc-php/fc-php.yaml b/apps/fc-php/fc-php.yaml index feb5614..e5e7a74 100644 --- a/apps/fc-php/fc-php.yaml +++ b/apps/fc-php/fc-php.yaml @@ -30,3 +30,26 @@ spec: port: 5400 tls: secretName: php-web-tls +# ---- PUBLIC HOST PRE-STAGING (DISABLED - Sprint 61+ exposure go-decision only) ---- +# When the operator decides to expose php-web publicly, uncomment + update the host, +# then verify the five safe-to-expose gates (authentik-safe-to-expose-readiness-2026-06-07.md section 2). +# +# --- IngressRoute --- +# apiVersion: traefik.io/v1alpha1 +# kind: IngressRoute +# metadata: +# name: php-web-public +# namespace: fc-php +# spec: +# entryPoints: [websecure] +# routes: +# - match: Host(`php.flowercore.io`) && (Method(`GET`) || Method(`HEAD`)) +# kind: Rule +# middlewares: +# - name: php-web-public-profile-header # injects entitlement profile +# services: +# - name: php-web +# port: 80 +# tls: {} +# # POST/PUT/PATCH/DELETE miss every route -> Traefik 404 -> no admin writes on the public surface. +# # Reference pattern: dist.flowercore.io (already live + method-gated; do not edit that one). diff --git a/apps/fc-presentations/fc-presentations.yaml b/apps/fc-presentations/fc-presentations.yaml index 5c41645..cde17a3 100644 --- a/apps/fc-presentations/fc-presentations.yaml +++ b/apps/fc-presentations/fc-presentations.yaml @@ -30,3 +30,26 @@ spec: port: 80 tls: secretName: presentations-web-tls +# ---- PUBLIC HOST PRE-STAGING (DISABLED - Sprint 61+ exposure go-decision only) ---- +# When the operator decides to expose presentations-web publicly, uncomment + update the host, +# then verify the five safe-to-expose gates (authentik-safe-to-expose-readiness-2026-06-07.md section 2). +# +# --- IngressRoute --- +# apiVersion: traefik.io/v1alpha1 +# kind: IngressRoute +# metadata: +# name: presentations-web-public +# namespace: fc-presentations +# spec: +# entryPoints: [websecure] +# routes: +# - match: Host(`presentations.flowercore.io`) && (Method(`GET`) || Method(`HEAD`)) +# kind: Rule +# middlewares: +# - name: presentations-web-public-profile-header # injects entitlement profile +# services: +# - name: presentations-web +# port: 80 +# tls: {} +# # POST/PUT/PATCH/DELETE miss every route -> Traefik 404 -> no admin writes on the public surface. +# # Reference pattern: dist.flowercore.io (already live + method-gated; do not edit that one). diff --git a/apps/fc-retail/fc-retail.yaml b/apps/fc-retail/fc-retail.yaml index a4badf3..c76cdda 100644 --- a/apps/fc-retail/fc-retail.yaml +++ b/apps/fc-retail/fc-retail.yaml @@ -46,6 +46,8 @@ spec: template: metadata: annotations: + fc.flowercore.io/healthz-anon: "true" + fc.flowercore.io/probe-path: "/healthz" kubectl.kubernetes.io/restartedAt: "2026-06-02T01:34:08-05:00" prometheus.io/path: /metrics/prometheus prometheus.io/port: "5000" @@ -55,6 +57,7 @@ spec: app.kubernetes.io/part-of: flowercore spec: containers: + # fc-safe-to-expose: X-Forwarded-Proto handled by AddFlowerCoreWebAuth (ADR-178) before any future public/OIDC flip. - envFrom: - configMapRef: name: retail-web-config @@ -168,3 +171,26 @@ spec: port: 80 tls: secretName: retail-web-tls +# ---- PUBLIC HOST PRE-STAGING (DISABLED - Sprint 61+ exposure go-decision only) ---- +# When the operator decides to expose retail-web publicly, uncomment + update the host, +# then verify the five safe-to-expose gates (authentik-safe-to-expose-readiness-2026-06-07.md section 2). +# +# --- IngressRoute --- +# apiVersion: traefik.io/v1alpha1 +# kind: IngressRoute +# metadata: +# name: retail-web-public +# namespace: fc-retail +# spec: +# entryPoints: [websecure] +# routes: +# - match: Host(`retail.flowercore.io`) && (Method(`GET`) || Method(`HEAD`)) +# kind: Rule +# middlewares: +# - name: retail-web-public-profile-header # injects entitlement profile +# services: +# - name: retail-web +# port: 80 +# tls: {} +# # POST/PUT/PATCH/DELETE miss every route -> Traefik 404 -> no admin writes on the public surface. +# # Reference pattern: dist.flowercore.io (already live + method-gated; do not edit that one). diff --git a/apps/fc-scoreboard/fc-scoreboard.yaml b/apps/fc-scoreboard/fc-scoreboard.yaml index b403801..9531530 100644 --- a/apps/fc-scoreboard/fc-scoreboard.yaml +++ b/apps/fc-scoreboard/fc-scoreboard.yaml @@ -30,3 +30,26 @@ spec: port: 80 tls: secretName: scoreboard-web-tls +# ---- PUBLIC HOST PRE-STAGING (DISABLED - Sprint 61+ exposure go-decision only) ---- +# When the operator decides to expose scoreboard-web publicly, uncomment + update the host, +# then verify the five safe-to-expose gates (authentik-safe-to-expose-readiness-2026-06-07.md section 2). +# +# --- IngressRoute --- +# apiVersion: traefik.io/v1alpha1 +# kind: IngressRoute +# metadata: +# name: scoreboard-web-public +# namespace: fc-scoreboard +# spec: +# entryPoints: [websecure] +# routes: +# - match: Host(`scoreboard.flowercore.io`) && (Method(`GET`) || Method(`HEAD`)) +# kind: Rule +# middlewares: +# - name: scoreboard-web-public-profile-header # injects entitlement profile +# services: +# - name: scoreboard-web +# port: 80 +# tls: {} +# # POST/PUT/PATCH/DELETE miss every route -> Traefik 404 -> no admin writes on the public surface. +# # Reference pattern: dist.flowercore.io (already live + method-gated; do not edit that one). diff --git a/apps/fc-segmentdisplay/fc-segmentdisplay.yaml b/apps/fc-segmentdisplay/fc-segmentdisplay.yaml index 5f87beb..108c785 100644 --- a/apps/fc-segmentdisplay/fc-segmentdisplay.yaml +++ b/apps/fc-segmentdisplay/fc-segmentdisplay.yaml @@ -37,3 +37,26 @@ spec: port: 80 tls: secretName: segmentdisplay-web-tls +# ---- PUBLIC HOST PRE-STAGING (DISABLED - Sprint 61+ exposure go-decision only) ---- +# When the operator decides to expose segmentdisplay-web publicly, uncomment + update the host, +# then verify the five safe-to-expose gates (authentik-safe-to-expose-readiness-2026-06-07.md section 2). +# +# --- IngressRoute --- +# apiVersion: traefik.io/v1alpha1 +# kind: IngressRoute +# metadata: +# name: segmentdisplay-web-public +# namespace: fc-segmentdisplay +# spec: +# entryPoints: [websecure] +# routes: +# - match: Host(`segmentdisplay.flowercore.io`) && (Method(`GET`) || Method(`HEAD`)) +# kind: Rule +# middlewares: +# - name: segmentdisplay-web-public-profile-header # injects entitlement profile +# services: +# - name: segmentdisplay-web +# port: 80 +# tls: {} +# # POST/PUT/PATCH/DELETE miss every route -> Traefik 404 -> no admin writes on the public surface. +# # Reference pattern: dist.flowercore.io (already live + method-gated; do not edit that one). diff --git a/apps/fc-signage/fc-signage.yaml b/apps/fc-signage/fc-signage.yaml index fcafe8e..f1c5580 100644 --- a/apps/fc-signage/fc-signage.yaml +++ b/apps/fc-signage/fc-signage.yaml @@ -46,3 +46,26 @@ spec: services: - name: signage-web port: 5190 +# ---- PUBLIC HOST PRE-STAGING (DISABLED - Sprint 61+ exposure go-decision only) ---- +# When the operator decides to expose signage-web publicly, uncomment + update the host, +# then verify the five safe-to-expose gates (authentik-safe-to-expose-readiness-2026-06-07.md section 2). +# +# --- IngressRoute --- +# apiVersion: traefik.io/v1alpha1 +# kind: IngressRoute +# metadata: +# name: signage-web-public +# namespace: fc-signage +# spec: +# entryPoints: [websecure] +# routes: +# - match: Host(`signage.flowercore.io`) && (Method(`GET`) || Method(`HEAD`)) +# kind: Rule +# middlewares: +# - name: signage-web-public-profile-header # injects entitlement profile +# services: +# - name: signage-web +# port: 80 +# tls: {} +# # POST/PUT/PATCH/DELETE miss every route -> Traefik 404 -> no admin writes on the public surface. +# # Reference pattern: dist.flowercore.io (already live + method-gated; do not edit that one). diff --git a/apps/fc-ttsreader/fc-ttsreader.yaml b/apps/fc-ttsreader/fc-ttsreader.yaml index 86fe517..f7b9b6b 100644 --- a/apps/fc-ttsreader/fc-ttsreader.yaml +++ b/apps/fc-ttsreader/fc-ttsreader.yaml @@ -97,6 +97,7 @@ spec: containers: - name: piper image: rhasspy/wyoming-piper:latest + # fc-safe-to-expose: X-Forwarded-Proto handled by AddFlowerCoreWebAuth (ADR-178) before any future public/OIDC flip. env: - name: PYTHONHTTPSVERIFY value: "0" @@ -523,6 +524,8 @@ spec: app.kubernetes.io/name: ttsreader-web app.kubernetes.io/part-of: flowercore annotations: + fc.flowercore.io/healthz-anon: "true" + fc.flowercore.io/probe-path: "/healthz" prometheus.io/scrape: "true" prometheus.io/port: "5217" prometheus.io/path: "/metrics" @@ -762,3 +765,26 @@ spec: port: 5217 tls: secretName: ttsreader-tls +# ---- PUBLIC HOST PRE-STAGING (DISABLED - Sprint 61+ exposure go-decision only) ---- +# When the operator decides to expose ttsreader-web publicly, uncomment + update the host, +# then verify the five safe-to-expose gates (authentik-safe-to-expose-readiness-2026-06-07.md section 2). +# +# --- IngressRoute --- +# apiVersion: traefik.io/v1alpha1 +# kind: IngressRoute +# metadata: +# name: ttsreader-web-public +# namespace: fc-ttsreader +# spec: +# entryPoints: [websecure] +# routes: +# - match: Host(`ttsreader.flowercore.io`) && (Method(`GET`) || Method(`HEAD`)) +# kind: Rule +# middlewares: +# - name: ttsreader-web-public-profile-header # injects entitlement profile +# services: +# - name: ttsreader-web +# port: 80 +# tls: {} +# # POST/PUT/PATCH/DELETE miss every route -> Traefik 404 -> no admin writes on the public surface. +# # Reference pattern: dist.flowercore.io (already live + method-gated; do not edit that one). diff --git a/apps/fc-updater/fc-updater.yaml b/apps/fc-updater/fc-updater.yaml index 7a30acc..cfa738e 100644 --- a/apps/fc-updater/fc-updater.yaml +++ b/apps/fc-updater/fc-updater.yaml @@ -52,6 +52,9 @@ spec: app: updatecenter-web template: metadata: + annotations: + fc.flowercore.io/healthz-anon: "true" + fc.flowercore.io/probe-path: "/healthz" labels: app: updatecenter-web spec: @@ -63,6 +66,7 @@ spec: ports: - containerPort: 8080 name: http + # fc-safe-to-expose: X-Forwarded-Proto handled by AddFlowerCoreWebAuth (ADR-178) before any future public/OIDC flip. env: - name: ASPNETCORE_URLS value: http://+:8080 diff --git a/apps/knowledge/knowledge.yaml b/apps/knowledge/knowledge.yaml index b6cde07..2c4a734 100644 --- a/apps/knowledge/knowledge.yaml +++ b/apps/knowledge/knowledge.yaml @@ -90,6 +90,8 @@ spec: app.kubernetes.io/name: knowledge-web app.kubernetes.io/part-of: bluejay-infra annotations: + fc.flowercore.io/healthz-anon: "true" + fc.flowercore.io/probe-path: "/healthz" prometheus.io/scrape: "true" prometheus.io/port: "8080" prometheus.io/path: "/metrics" @@ -117,6 +119,7 @@ spec: ports: - containerPort: 8080 name: http + # fc-safe-to-expose: X-Forwarded-Proto handled by AddFlowerCoreWebAuth (ADR-178) before any future public/OIDC flip. env: - name: ASPNETCORE_URLS value: "http://+:8080" @@ -286,3 +289,26 @@ spec: port: 80 tls: secretName: knowledge-tls +# ---- PUBLIC HOST PRE-STAGING (DISABLED - Sprint 61+ exposure go-decision only) ---- +# When the operator decides to expose knowledge-web publicly, uncomment + update the host, +# then verify the five safe-to-expose gates (authentik-safe-to-expose-readiness-2026-06-07.md section 2). +# +# --- IngressRoute --- +# apiVersion: traefik.io/v1alpha1 +# kind: IngressRoute +# metadata: +# name: knowledge-web-public +# namespace: knowledge +# spec: +# entryPoints: [websecure] +# routes: +# - match: Host(`knowledge.flowercore.io`) && (Method(`GET`) || Method(`HEAD`)) +# kind: Rule +# middlewares: +# - name: knowledge-web-public-profile-header # injects entitlement profile +# services: +# - name: knowledge-web +# port: 80 +# tls: {} +# # POST/PUT/PATCH/DELETE miss every route -> Traefik 404 -> no admin writes on the public surface. +# # Reference pattern: dist.flowercore.io (already live + method-gated; do not edit that one). diff --git a/apps/telephony/telephony.yaml b/apps/telephony/telephony.yaml index 4ca4de3..e1b5e4a 100644 --- a/apps/telephony/telephony.yaml +++ b/apps/telephony/telephony.yaml @@ -114,6 +114,9 @@ spec: app: telephony-web template: metadata: + annotations: + fc.flowercore.io/healthz-anon: "true" + fc.flowercore.io/probe-path: "/health" labels: app: telephony-web spec: @@ -161,6 +164,7 @@ spec: ports: - containerPort: 5100 name: http + # fc-safe-to-expose: X-Forwarded-Proto handled by AddFlowerCoreWebAuth (ADR-178) before any future public/OIDC flip. env: - name: Telephony__Twilio__AccountSid valueFrom: @@ -387,4 +391,3 @@ spec: - diff --git a/apps/worldbuilder/worldbuilder.yaml b/apps/worldbuilder/worldbuilder.yaml index eb860c7..7f4202f 100644 --- a/apps/worldbuilder/worldbuilder.yaml +++ b/apps/worldbuilder/worldbuilder.yaml @@ -77,6 +77,8 @@ spec: flowercore.io/tenant-id: system flowercore.io/created-by: bluejay-infra annotations: + fc.flowercore.io/healthz-anon: "true" + fc.flowercore.io/probe-path: "/healthz" prometheus.io/scrape: "true" prometheus.io/port: "8080" prometheus.io/path: "/metrics/prometheus" @@ -93,6 +95,7 @@ spec: ports: - containerPort: 8080 name: http + # fc-safe-to-expose: X-Forwarded-Proto handled by AddFlowerCoreWebAuth (ADR-178) before any future public/OIDC flip. env: - name: ASPNETCORE_URLS value: "http://+:8080" @@ -254,3 +257,26 @@ spec: port: 80 tls: secretName: worldbuilder-web-tls +# ---- PUBLIC HOST PRE-STAGING (DISABLED - Sprint 61+ exposure go-decision only) ---- +# When the operator decides to expose worldbuilder-web publicly, uncomment + update the host, +# then verify the five safe-to-expose gates (authentik-safe-to-expose-readiness-2026-06-07.md section 2). +# +# --- IngressRoute --- +# apiVersion: traefik.io/v1alpha1 +# kind: IngressRoute +# metadata: +# name: worldbuilder-web-public +# namespace: worldbuilder +# spec: +# entryPoints: [websecure] +# routes: +# - match: Host(`worldbuilder.flowercore.io`) && (Method(`GET`) || Method(`HEAD`)) +# kind: Rule +# middlewares: +# - name: worldbuilder-web-public-profile-header # injects entitlement profile +# services: +# - name: worldbuilder-web +# port: 80 +# tls: {} +# # POST/PUT/PATCH/DELETE miss every route -> Traefik 404 -> no admin writes on the public surface. +# # Reference pattern: dist.flowercore.io (already live + method-gated; do not edit that one). diff --git a/tests/bluejay-infra-lint/FleetManifestLintTests.cs b/tests/bluejay-infra-lint/FleetManifestLintTests.cs index 34c7deb..c815123 100644 --- a/tests/bluejay-infra-lint/FleetManifestLintTests.cs +++ b/tests/bluejay-infra-lint/FleetManifestLintTests.cs @@ -79,6 +79,44 @@ public sealed class FleetManifestLintTests "github-runner-updater", }; + private static readonly IReadOnlyDictionary BroaderHardeningDeployments = + new Dictionary(StringComparer.Ordinal) + { + ["fc-aistation"] = ("aistation-web", "/healthz"), + ["fc-chat"] = ("chat-web", "/healthz"), + ["fc-devicemgmt"] = ("fc-devicemgmt-web", "/healthz"), + ["fc-library"] = ("library-web", "/health"), + ["fc-llm-bridge"] = ("fc-llm-bridge", "/healthz"), + ["fc-messageboard"] = ("messageboard-web", "/healthz"), + ["fc-retail"] = ("retail-web", "/healthz"), + ["fc-ttsreader"] = ("ttsreader-web", "/healthz"), + ["fc-updater"] = ("updatecenter-web", "/healthz"), + ["knowledge"] = ("knowledge-web", "/healthz"), + ["telephony"] = ("telephony-web", "/health"), + ["worldbuilder"] = ("worldbuilder-web", "/healthz"), + }; + + private static readonly HashSet BroaderHardeningInternalPrestageApps = new(StringComparer.Ordinal) + { + "fc-aistation", + "fc-desktop", + "fc-dms", + "fc-library", + "fc-llm-bridge", + "fc-menuboard", + "fc-messageboard", + "fc-mysql", + "fc-php", + "fc-presentations", + "fc-retail", + "fc-scoreboard", + "fc-segmentdisplay", + "fc-signage", + "fc-ttsreader", + "knowledge", + "worldbuilder", + }; + private static readonly IReadOnlyDictionary WritableRunnerEnv = new Dictionary(StringComparer.Ordinal) { ["HOME"] = "/home/runner", @@ -773,6 +811,54 @@ public sealed class FleetManifestLintTests ns.FileText.Should().Contain("ArgoCD discovers this directory as Application `infra-fc-devicemgmt`."); } + [Fact] + public void BroaderHardeningDeployments_MustAnnotateAnonymousHealthProbeIntent() + { + foreach (var expected in BroaderHardeningDeployments) + { + var deployment = AppDocuments(expected.Key) + .Single(document => document.Kind == "Deployment" && document.Name == expected.Value.Deployment); + + PodAnnotation(deployment, "fc.flowercore.io/healthz-anon").Should().Be("true"); + PodAnnotation(deployment, "fc.flowercore.io/probe-path").Should().Be(expected.Value.ProbePath); + } + } + + [Fact] + public void BroaderHardeningDeployments_MustDocumentForwardedProtoAuthPosture() + { + foreach (var expected in BroaderHardeningDeployments) + { + var deployment = AppDocuments(expected.Key) + .Single(document => document.Kind == "Deployment" && document.Name == expected.Value.Deployment); + + deployment.FileText.Should().Contain( + "fc-safe-to-expose: X-Forwarded-Proto handled by AddFlowerCoreWebAuth (ADR-178)"); + } + } + + [Fact] + public void BroaderHardeningInternalApps_MustOnlyPrestageCommentedPublicMethodAllowlist() + { + foreach (var app in BroaderHardeningInternalPrestageApps) + { + var documents = AppDocuments(app); + var text = string.Join(Environment.NewLine, documents.Select(document => document.FileText)); + + text.Should().Contain("PUBLIC HOST PRE-STAGING (DISABLED - Sprint 61+ exposure go-decision only)"); + text.Should().Contain("# - match: Host(`"); + text.Should().Contain("Method(`GET`) || Method(`HEAD`)"); + + documents + .Where(document => document.Kind == "IngressRoute") + .SelectMany(document => document.MappingSequence("spec", "routes")) + .Select(route => ManifestNodeExtensions.Scalar(route, "match") ?? string.Empty) + .Should() + .NotContain(match => match.Contains(".flowercore.io", StringComparison.Ordinal), + "Sprint 61 broader hardening only pre-stages commented public hosts for internal-only apps"); + } + } + [Fact] public void OidcFlipServices_AreGitOpsManagedWithHealthzProbes() {