From ef782ed56dcf0b8f780f916faa0000706ed6e8ee Mon Sep 17 00:00:00 2001 From: Andrew Stoltz <1578013+astoltz@users.noreply.github.com> Date: Wed, 17 Jun 2026 19:24:36 -0500 Subject: [PATCH] whc4: front PHP route with CRS WAF --- apps-gx10/fc-php/deployment-php-waf.json | 170 ++++++++++++++++++ apps-gx10/fc-php/ingressroute-php-web.json | 4 +- apps-gx10/fc-php/service-php-waf.json | 24 +++ .../FleetManifestLintTests.cs | 51 ++++++ 4 files changed, 247 insertions(+), 2 deletions(-) create mode 100644 apps-gx10/fc-php/deployment-php-waf.json create mode 100644 apps-gx10/fc-php/service-php-waf.json diff --git a/apps-gx10/fc-php/deployment-php-waf.json b/apps-gx10/fc-php/deployment-php-waf.json new file mode 100644 index 0000000..8d9ebc0 --- /dev/null +++ b/apps-gx10/fc-php/deployment-php-waf.json @@ -0,0 +1,170 @@ +{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "labels": { + "app.kubernetes.io/managed-by": "flowercore", + "app.kubernetes.io/name": "php-waf" + }, + "name": "php-waf", + "namespace": "fc-php" + }, + "spec": { + "progressDeadlineSeconds": 600, + "replicas": 1, + "revisionHistoryLimit": 10, + "selector": { + "matchLabels": { + "app.kubernetes.io/name": "php-waf" + } + }, + "strategy": { + "type": "Recreate" + }, + "template": { + "metadata": { + "labels": { + "app.kubernetes.io/name": "php-waf" + } + }, + "spec": { + "containers": [ + { + "env": [ + { + "name": "BACKEND", + "value": "http://php-web.fc-php.svc.cluster.local:5400" + }, + { + "name": "SERVER_NAME", + "value": "php.iamworkin.lan" + }, + { + "name": "PORT", + "value": "8080" + }, + { + "name": "PROXY_PRESERVE_HOST", + "value": "on" + }, + { + "name": "PROXY_TIMEOUT", + "value": "60s" + }, + { + "name": "MODSEC_RULE_ENGINE", + "value": "On" + }, + { + "name": "MODSEC_AUDIT_ENGINE", + "value": "RelevantOnly" + }, + { + "name": "MODSEC_AUDIT_LOG", + "value": "/dev/stdout" + }, + { + "name": "MODSEC_AUDIT_LOG_TYPE", + "value": "Serial" + }, + { + "name": "LOGLEVEL", + "value": "warn" + }, + { + "name": "ERRORLOG", + "value": "/dev/stderr" + }, + { + "name": "ACCESSLOG", + "value": "/dev/stdout" + }, + { + "name": "BLOCKING_PARANOIA", + "value": "1" + }, + { + "name": "DETECTION_PARANOIA", + "value": "1" + }, + { + "name": "ANOMALY_INBOUND", + "value": "5" + }, + { + "name": "ANOMALY_OUTBOUND", + "value": "4" + } + ], + "image": "owasp/modsecurity-crs:4.25-nginx-alpine-lts@sha256:88b59911549723e71beabf3b4aa47bbd31b00e79401f442e65ddfc430ae46343", + "imagePullPolicy": "IfNotPresent", + "livenessProbe": { + "failureThreshold": 3, + "httpGet": { + "path": "/healthz", + "port": 8080, + "scheme": "HTTP" + }, + "initialDelaySeconds": 20, + "periodSeconds": 30, + "successThreshold": 1, + "timeoutSeconds": 2 + }, + "name": "php-waf", + "ports": [ + { + "containerPort": 8080, + "name": "http", + "protocol": "TCP" + } + ], + "readinessProbe": { + "failureThreshold": 3, + "httpGet": { + "path": "/healthz", + "port": 8080, + "scheme": "HTTP" + }, + "initialDelaySeconds": 10, + "periodSeconds": 10, + "successThreshold": 1, + "timeoutSeconds": 2 + }, + "resources": { + "limits": { + "cpu": "500m", + "memory": "512Mi" + }, + "requests": { + "cpu": "100m", + "memory": "128Mi" + } + }, + "securityContext": { + "allowPrivilegeEscalation": false, + "capabilities": { + "drop": [ + "ALL" + ] + } + }, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File" + } + ], + "enableServiceLinks": false, + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": { + "fsGroup": 101, + "runAsGroup": 101, + "runAsNonRoot": true, + "runAsUser": 101 + }, + "serviceAccount": "php-web", + "serviceAccountName": "php-web", + "terminationGracePeriodSeconds": 30 + } + } + } +} diff --git a/apps-gx10/fc-php/ingressroute-php-web.json b/apps-gx10/fc-php/ingressroute-php-web.json index 178abc2..486a6f4 100644 --- a/apps-gx10/fc-php/ingressroute-php-web.json +++ b/apps-gx10/fc-php/ingressroute-php-web.json @@ -16,8 +16,8 @@ "priority": 100, "services": [ { - "name": "php-web", - "port": 5400 + "name": "php-waf", + "port": 8080 } ] } diff --git a/apps-gx10/fc-php/service-php-waf.json b/apps-gx10/fc-php/service-php-waf.json new file mode 100644 index 0000000..b0ec8a7 --- /dev/null +++ b/apps-gx10/fc-php/service-php-waf.json @@ -0,0 +1,24 @@ +{ + "apiVersion": "v1", + "kind": "Service", + "metadata": { + "name": "php-waf", + "namespace": "fc-php" + }, + "spec": { + "internalTrafficPolicy": "Cluster", + "ports": [ + { + "name": "http", + "port": 8080, + "protocol": "TCP", + "targetPort": 8080 + } + ], + "selector": { + "app.kubernetes.io/name": "php-waf" + }, + "sessionAffinity": "None", + "type": "ClusterIP" + } +} diff --git a/tests/bluejay-infra-lint/FleetManifestLintTests.cs b/tests/bluejay-infra-lint/FleetManifestLintTests.cs index f333221..aae2a93 100644 --- a/tests/bluejay-infra-lint/FleetManifestLintTests.cs +++ b/tests/bluejay-infra-lint/FleetManifestLintTests.cs @@ -1014,6 +1014,49 @@ public sealed class FleetManifestLintTests tlsOption.RootElement.GetProperty("spec").GetProperty("minVersion").GetString().Should().Be("VersionTLS13"); } + [Fact] + public void Gx10PhpManagerRoute_IsFrontedByOwaspCrsWaf() + { + var appRoot = Path.Combine(Inventory.BluejayRoot, "apps-gx10", "fc-php"); + var wafContainer = Gx10DeploymentContainer("fc-php", "deployment-php-waf.json"); + wafContainer.GetProperty("image").GetString() + .Should() + .Be("owasp/modsecurity-crs:4.25-nginx-alpine-lts@sha256:88b59911549723e71beabf3b4aa47bbd31b00e79401f442e65ddfc430ae46343"); + wafContainer.GetProperty("imagePullPolicy").GetString().Should().Be("IfNotPresent"); + JsonEnvValue(wafContainer, "BACKEND").Should().Be("http://php-web.fc-php.svc.cluster.local:5400"); + JsonEnvValue(wafContainer, "SERVER_NAME").Should().Be("php.iamworkin.lan"); + JsonEnvValue(wafContainer, "MODSEC_RULE_ENGINE").Should().Be("On"); + JsonEnvValue(wafContainer, "MODSEC_AUDIT_ENGINE").Should().Be("RelevantOnly"); + JsonEnvValue(wafContainer, "MODSEC_AUDIT_LOG").Should().Be("/dev/stdout"); + + using var wafDeployment = JsonDocument.Parse(File.ReadAllText(Path.Combine(appRoot, "deployment-php-waf.json"))); + var podSpec = wafDeployment.RootElement + .GetProperty("spec") + .GetProperty("template") + .GetProperty("spec"); + podSpec.GetProperty("enableServiceLinks").GetBoolean().Should().BeFalse(); + podSpec.GetProperty("securityContext").GetProperty("runAsUser").GetInt32().Should().Be(101); + podSpec.GetProperty("securityContext").GetProperty("runAsNonRoot").GetBoolean().Should().BeTrue(); + + using var service = JsonDocument.Parse(File.ReadAllText(Path.Combine(appRoot, "service-php-waf.json"))); + service.RootElement.GetProperty("spec").GetProperty("selector").GetProperty("app.kubernetes.io/name").GetString().Should().Be("php-waf"); + var servicePort = service.RootElement.GetProperty("spec").GetProperty("ports").EnumerateArray().Should().ContainSingle().Subject; + servicePort.GetProperty("port").GetInt32().Should().Be(8080); + servicePort.GetProperty("targetPort").GetInt32().Should().Be(8080); + + using var ingressRoute = JsonDocument.Parse(File.ReadAllText(Path.Combine(appRoot, "ingressroute-php-web.json"))); + var serviceRef = ingressRoute.RootElement + .GetProperty("spec") + .GetProperty("routes")[0] + .GetProperty("services") + .EnumerateArray() + .Should() + .ContainSingle() + .Subject; + serviceRef.GetProperty("name").GetString().Should().Be("php-waf"); + serviceRef.GetProperty("port").GetInt32().Should().Be(8080); + } + [Fact] public void Gx10HostingManagers_ProvisioningCrdsAndRbacMustBeGitOpsOwned() { @@ -1231,6 +1274,14 @@ public sealed class FleetManifestLintTests : null; } + private static string? JsonEnvValue(JsonElement container, string name) + { + return JsonEnvMapping(container, name) is { } env + && env.TryGetProperty("value", out var value) + ? value.GetString() + : null; + } + private static JsonElement? JsonEnvMapping(JsonElement container, string name) { foreach (var env in container.GetProperty("env").EnumerateArray())