Skip to content

MEIER-354: Add Serilog with OpenTelemetry sink and harden WAF rules#22

Merged
andymeierdev merged 2 commits intomainfrom
andymeierdev/MEIER-354/add-serilog-otel-and-harden-waf
Mar 25, 2026
Merged

MEIER-354: Add Serilog with OpenTelemetry sink and harden WAF rules#22
andymeierdev merged 2 commits intomainfrom
andymeierdev/MEIER-354/add-serilog-otel-and-harden-waf

Conversation

@andymeierdev
Copy link
Copy Markdown
Collaborator

Summary

Adds structured logging with Serilog (using OpenTelemetry sink to Seq) to the Docs app, and hardens the Cloudflare WAF rules to prevent case and URL-encoding bypass attacks.

Changes

Serilog with OpenTelemetry Sink

  • sln/paket.dependencies / sln/src/Docs/paket.references: Added Serilog, Serilog.AspNetCore, Serilog.Sinks.Console, and Serilog.Sinks.OpenTelemetry packages.
  • sln/src/Docs/Config.fs:
    • Added Env module with variable and variableOrDefault helpers for cleaner env var access.
    • Added SeqConfig type with endpoint (defaults to http://localhost:5341).
    • Extended Config with debug, appName, and seq fields.
  • sln/src/Docs/Program.fs:
    • Added configureLogger that sends logs to Seq via the OpenTelemetry OTLP ingest endpoint (/ingest/otlp/v1/logs) using HttpProtobuf protocol.
    • Integrated UseSerilogRequestLogging with /health endpoint filtered to Verbose to reduce noise.
    • Wrapped main in try/finally with Log.CloseAndFlush() and Log.Fatal on startup failure.

Pulumi — Seq Endpoint Config

  • pulumi/src/config.ts: Added seqConfig reading seq:endpoint from Pulumi config.
  • pulumi/src/k8s/deployment.ts: Passes SEQ_ENDPOINT to the app ConfigMap from the Pulumi seq config.

WAF Hardening

  • pulumi/src/cloudflare/waf.ts:
    • Wrapped all expressions in lower(url_decode(...)) to normalize both case and percent-encoding, preventing bypass via mixed case (e.g. /ReAcT/.EnV) or URL encoding (e.g. %2F).
    • Extracted the path expression into a const p variable to DRY up the rules.
    • Removed duplicate /phpMyAdmin rule (now redundant with lower()).
    • Added new block rules: /login, /api/config, /careers_not_hosted, and .env. (catches .env.sample, .env.prod, etc.).

Validation

  • dotnet build src/Docs/Docs.fsproj — builds with 0 warnings, 0 errors.
  • pulumi preview — shows expected resource changes (ConfigMap replace, WAF ruleset create, deployment replace).

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 24, 2026

🍹 preview on fsharp-view-engine/prod

Pulumi report

View in Pulumi Cloud

  Previewing update (prod)

View Live: https://app.pulumi.com/meiermade/fsharp-view-engine/prod/previews/c16e9a0c-8dda-45e5-86e6-37afd4948ea2

pulumi:pulumi:Stack: (same)
  [urn=urn:pulumi:prod::fsharp-view-engine::pulumi:pulumi:Stack::fsharp-view-engine-prod]
  +-kubernetes:core/v1:ConfigMap: (replace)
      [id=fsharpviewengine/fsharpviewengine]
      [urn=urn:pulumi:prod::fsharp-view-engine::kubernetes:core/v1:ConfigMap::fsharpviewengine]
    ~ data: {
        + SEQ_ENDPOINT: "http://seq.platform:5341"
      }
  ~ docker-build:index:Image: (update)
      [id=sha256:7dce1dac725cac480a57d3912a6d0747a6e506fead31a37c5d0e1cf2750b1959]
      [urn=urn:pulumi:prod::fsharp-view-engine::docker-build:index:Image::fsharpviewengine]
    + cacheFrom  : [
    +     [0]: {
            + disabled: false
            + gha     : {
                + scope: "buildkit"
              }
            + raw     : ""
          }
      ]
    + cacheTo    : [
    +     [0]: {
            + disabled: false
            + gha     : {
                + ignoreError: true
                + mode       : "max"
                + scope      : "buildkit"
              }
            + raw     : ""
          }
      ]
    ~ context    : {
        ~ location: "C:\\Users\\ameier\\repos\\github\\meiermade\\FSharp.ViewEngine\\sln" => "/home/runner/work/FSharp.ViewEngine/FSharp.ViewEngine/sln"
      }
    - contextHash: "497c795efc4aab38aa21d6d2683e809f0801012dc7b035ae14b8669a2f022d91"
    ~ dockerfile : {
        ~ location: "C:\\Users\\ameier\\repos\\github\\meiermade\\FSharp.ViewEngine\\sln\\Dockerfile" => "/home/runner/work/FSharp.ViewEngine/FSharp.ViewEngine/sln/Dockerfile"
      }
  ~ cloudflare:index/zeroTrustTunnelCloudflaredConfig:ZeroTrustTunnelCloudflaredConfig: (update)
      [id=29a22278-9fe7-4d10-a3a4-69834d0dffc0]
      [urn=urn:pulumi:prod::fsharp-view-engine::cloudflare:index/zeroTrustTunnelCloudflaredConfig:ZeroTrustTunnelCloudflaredConfig::fsharpviewengine]
    ~ config: {
        ~ ingresses: [
            ~ [0]: {
                      hostname: "fsharpviewengine.meiermade.com"
                    ~ service : "http://fsharpviewengine.fsharpviewengine.svc.cluster.local:80" => "http://localhost:5000"
                  }
              [1]: {
                      service: "http_status:404"
                  }
          ]
      }
  +-kubernetes:apps/v1:Deployment: (replace)
      [id=fsharpviewengine/fsharpviewengine]
      [urn=urn:pulumi:prod::fsharp-view-engine::kubernetes:apps/v1:Deployment::fsharpviewengine]
      apiVersion: "apps/v1"
      kind      : "Deployment"
      metadata  : {
          name     : "fsharpviewengine"
          namespace: "fsharpviewengine"
      }
    ~ spec      : {
          replicas: 1
          selector: {
              matchLabels: {
                  app.kubernetes.io/name: "fsharpviewengine"
              }
          }
        ~ template: {
              metadata: {
                  labels: {
                      app.kubernetes.io/name: "fsharpviewengine"
                  }
              }
            ~ spec    : {
                ~ containers     : [
                    ~ [0]: {
                            ~ envFrom        : [
                                ~ [0]: {
                                        ~ configMapRef: {
                                            ~ name: "fsharpviewengine" => "fsharpviewenginefql6p"
                                          }
                                      }
                              ]
                            ~ image          : "us-east1-docker.pkg.dev/meiermade-platform/platform/fsharpviewengine:latest@sha256:ec195b7211e17d82d42127323d1e9ef89fc8f77395da67b66d5b188fd6b3c5dc" => [unknown]
                              imagePullPolicy: "IfNotPresent"
                              livenessProbe  : {
                                  httpGet            : {
                                      path: "/health"
                                      port: 5000
                                  }
                                  initialDelaySeconds: 5
                              }
                              name           : "fsharpviewengine"
                              readinessProbe : {
                                  httpGet            : {
                                      path: "/health"
                                      port: 5000
                                  }
                                  initialDelaySeconds: 5
                              }
                              securityContext: {
                                  allowPrivilegeEscalation: false
                                  capabilities            : {
                                      drop: [
                                          [0]: "ALL"
                                      ]
                                  }
                              }
                          }
                      [1]: {
                              args           : [
                                  [0]: "tunnel"
                                  [1]: "--no-autoupdate"
                                  [2]: "run"
                              ]
                              envFrom        : [
                                  [0]: {
                                      secretRef: {
                                          name: "fsharpviewengine-cloudflared"
                                      }
                                  }
                              ]
                              image          : "cloudflare/cloudflared:2026.2.0"
                              livenessProbe  : {
                                  failureThreshold   : 1
                                  httpGet            : {
                                      path: "/ready"
                                      port: 2000
                                  }
                                  initialDelaySeconds: 10
                                  periodSeconds      : 10
                              }
                              name           : "cloudflared"
                              securityContext: {
                                  allowPrivilegeEscalation: false
                                  capabilities            : {
                                      drop: [
                                          [0]: "ALL"
                                      ]
                                  }
                              }
                          }
                  ]
                  securityContext: {
                      runAsNonRoot  : true
                      seccompProfile: {
                          type: "RuntimeDefault"
                      }
                  }
              }
          }
      }
  + cloudflare:index/ruleset:Ruleset: (create)
      [urn=urn:pulumi:prod::fsharp-view-engine::cloudflare:index/ruleset:Ruleset::fsharpviewengine-waf]
      kind  : "zone"
      name  : "Block vulnerability scanners"
      phase : "http_request_firewall_custom"
      rules : [
          [0]: {
              action     : "block"
              description: "Block common vulnerability scanner paths and file extensions"
              enabled    : true
              expression : "(lower(url_decode(http.request.uri.path)) contains \"/.env\") or (lower(url_decode(http.request.uri.path)) contains \"/.git\") or (lower(url_decode(http.r..."
              ref        : "block_scan_probes"
          }
      ]
      zoneId: "1d09a5f3c5efd0a617f98a9ac32abfc4"
Resources:
  + 1 to create
  ~ 2 to update
  +-2 to replace
  5 changes. 5 unchanged
  

@andymeierdev andymeierdev merged commit d0b086f into main Mar 25, 2026
2 checks passed
@andymeierdev andymeierdev deleted the andymeierdev/MEIER-354/add-serilog-otel-and-harden-waf branch March 25, 2026 09:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant