From 9ff96c1626c7f4dae16ba593c49a13b75bf198d8 Mon Sep 17 00:00:00 2001 From: Andrew Meier Date: Tue, 24 Mar 2026 07:42:47 -0400 Subject: [PATCH 1/3] MEIER-320: Filter health check logs/traces and add Cloudflare WAF scanner block --- app/src/App/src/Program.fs | 7 +++++- pulumi/src/cloudflare/index.ts | 1 + pulumi/src/cloudflare/waf.ts | 46 ++++++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 pulumi/src/cloudflare/waf.ts diff --git a/app/src/App/src/Program.fs b/app/src/App/src/Program.fs index a9f124e..577fcb1 100644 --- a/app/src/App/src/Program.fs +++ b/app/src/App/src/Program.fs @@ -20,7 +20,8 @@ let configureTracerProvider (config: Config) = .AddSource(config.appName) .ConfigureResource(fun resourceBuilder -> resourceBuilder.AddService(serviceName = config.appName) |> ignore) - .AddAspNetCoreInstrumentation() + .AddAspNetCoreInstrumentation(fun opts -> + opts.Filter <- fun ctx -> ctx.Request.Path.Value <> "/health") .AddHttpClientInstrumentation() .AddOtlpExporter(fun opts -> opts.Endpoint <- Uri(config.seq.endpoint + "/ingest/otlp/v1/traces") @@ -54,6 +55,10 @@ let configureServices (serviceCollection: IServiceCollection) (tracerProvider: T let configureApp (services: Services) (app: WebApplication) = app + .UseSerilogRequestLogging(fun opts -> + opts.GetLevel <- fun ctx _ _ -> + if ctx.Request.Path.Value = "/health" then LogEventLevel.Verbose + else LogEventLevel.Information) .UseStaticFiles() .UseGiraffe(Index.Handler.handler services) diff --git a/pulumi/src/cloudflare/index.ts b/pulumi/src/cloudflare/index.ts index 87a1ee7..c71c02b 100644 --- a/pulumi/src/cloudflare/index.ts +++ b/pulumi/src/cloudflare/index.ts @@ -1,3 +1,4 @@ import './record' import './redirect' import './tunnel' +import './waf' diff --git a/pulumi/src/cloudflare/waf.ts b/pulumi/src/cloudflare/waf.ts new file mode 100644 index 0000000..d388dd6 --- /dev/null +++ b/pulumi/src/cloudflare/waf.ts @@ -0,0 +1,46 @@ +import * as cloudflare from '@pulumi/cloudflare' +import { provider } from './provider' +import * as config from '../config' + +const zone = cloudflare.getZoneOutput({ + filter: { + account: { + id: config.cloudflareConfig.accountId + }, + name: 'andymeier.dev' + } +}, { provider }) + +const expression = [ + '(http.request.uri.path contains "/.env")', + '(http.request.uri.path contains "/.git")', + '(http.request.uri.path contains "/wp-")', + '(http.request.uri.path contains "/wordpress")', + '(http.request.uri.path contains "/xmlrpc")', + '(http.request.uri.path contains "/phpMyAdmin")', + '(http.request.uri.path contains "/phpmyadmin")', + '(http.request.uri.path contains "/pma")', + '(http.request.uri.path contains "/cgi-bin")', + '(http.request.uri.path contains "/actuator")', + '(http.request.uri.path contains "/solr")', + '(http.request.uri.path contains "/telescope")', + '(http.request.uri.path contains "/vendor")', + '(http.request.uri.path contains ".php")', + '(http.request.uri.path contains ".asp")', + '(http.request.uri.path contains ".jsp")', + '(http.request.uri.path contains ".cgi")', +].join(' or ') + +new cloudflare.Ruleset(`${config.identifier}-waf`, { + zoneId: zone.id, + name: 'Block vulnerability scanners', + kind: 'zone', + phase: 'http_request_firewall_custom', + rules: [{ + ref: 'block_scan_probes', + description: 'Block common vulnerability scanner paths and file extensions', + enabled: true, + expression, + action: 'block', + }] +}, { provider }) From 8078439f8ebab294fe6bd0e4af3316b3004ad5b8 Mon Sep 17 00:00:00 2001 From: Andrew Meier Date: Tue, 24 Mar 2026 07:49:26 -0400 Subject: [PATCH 2/3] MEIER-320: Extract shared zone lookups into zone.ts --- pulumi/src/cloudflare/index.ts | 1 + pulumi/src/cloudflare/record.ts | 30 ++---------------------------- pulumi/src/cloudflare/redirect.ts | 19 +------------------ pulumi/src/cloudflare/waf.ts | 12 ++---------- pulumi/src/cloudflare/zone.ts | 17 +++++++++++++++++ 5 files changed, 23 insertions(+), 56 deletions(-) create mode 100644 pulumi/src/cloudflare/zone.ts diff --git a/pulumi/src/cloudflare/index.ts b/pulumi/src/cloudflare/index.ts index c71c02b..ad0291f 100644 --- a/pulumi/src/cloudflare/index.ts +++ b/pulumi/src/cloudflare/index.ts @@ -1,3 +1,4 @@ +import './zone' import './record' import './redirect' import './tunnel' diff --git a/pulumi/src/cloudflare/record.ts b/pulumi/src/cloudflare/record.ts index 9053a2b..b2d62f0 100644 --- a/pulumi/src/cloudflare/record.ts +++ b/pulumi/src/cloudflare/record.ts @@ -1,20 +1,12 @@ import * as cloudflare from '@pulumi/cloudflare' import { tunnelHostname } from './tunnel' import { provider } from './provider' +import { andymeierZone, andrewmeierZone, meiermadeZone } from './zone' import * as config from '../config' -const zone = cloudflare.getZoneOutput({ - filter: { - account: { - id: config.cloudflareConfig.accountId - }, - name: 'andymeier.dev' - } -}, { provider }) - export const andymeier = new cloudflare.DnsRecord(config.identifier, { name: '@', - zoneId: zone.id, + zoneId: andymeierZone.id, type: 'CNAME', content: tunnelHostname, proxied: true, @@ -24,15 +16,6 @@ export const andymeier = new cloudflare.DnsRecord(config.identifier, { // DNS records for redirect domains — proxied A records pointing to a dummy IP // so Cloudflare can intercept requests and apply redirect rulesets. -const andrewmeierZone = cloudflare.getZoneOutput({ - filter: { - account: { - id: config.cloudflareConfig.accountId - }, - name: 'andrewmeier.dev' - } -}, { provider }) - new cloudflare.DnsRecord(`${config.identifier}-andrewmeier-root`, { name: '@', zoneId: andrewmeierZone.id, @@ -51,15 +34,6 @@ new cloudflare.DnsRecord(`${config.identifier}-andrewmeier-www`, { ttl: 1 }, { provider }) -const meiermadeZone = cloudflare.getZoneOutput({ - filter: { - account: { - id: config.cloudflareConfig.accountId - }, - name: 'meiermade.com' - } -}, { provider }) - new cloudflare.DnsRecord(`${config.identifier}-meiermade-root`, { name: '@', zoneId: meiermadeZone.id, diff --git a/pulumi/src/cloudflare/redirect.ts b/pulumi/src/cloudflare/redirect.ts index 95ca9b3..62e55a3 100644 --- a/pulumi/src/cloudflare/redirect.ts +++ b/pulumi/src/cloudflare/redirect.ts @@ -1,17 +1,9 @@ import * as cloudflare from '@pulumi/cloudflare' import { provider } from './provider' +import { andrewmeierZone, meiermadeZone } from './zone' import * as config from '../config' // andrewmeier.dev -> andymeier.dev -const andrewmeierZone = cloudflare.getZoneOutput({ - filter: { - account: { - id: config.cloudflareConfig.accountId - }, - name: 'andrewmeier.dev' - } -}, { provider }) - new cloudflare.Ruleset(`${config.identifier}-andrewmeier-redirect`, { zoneId: andrewmeierZone.id, name: 'Redirect andrewmeier.dev to andymeier.dev', @@ -36,15 +28,6 @@ new cloudflare.Ruleset(`${config.identifier}-andrewmeier-redirect`, { }, { provider }) // meiermade.com -> andymeier.dev/services -const meiermadeZone = cloudflare.getZoneOutput({ - filter: { - account: { - id: config.cloudflareConfig.accountId - }, - name: 'meiermade.com' - } -}, { provider }) - new cloudflare.Ruleset(`${config.identifier}-meiermade-redirect`, { zoneId: meiermadeZone.id, name: 'Redirect meiermade.com to andymeier.dev/services', diff --git a/pulumi/src/cloudflare/waf.ts b/pulumi/src/cloudflare/waf.ts index d388dd6..18c93d1 100644 --- a/pulumi/src/cloudflare/waf.ts +++ b/pulumi/src/cloudflare/waf.ts @@ -1,16 +1,8 @@ import * as cloudflare from '@pulumi/cloudflare' import { provider } from './provider' +import { andymeierZone } from './zone' import * as config from '../config' -const zone = cloudflare.getZoneOutput({ - filter: { - account: { - id: config.cloudflareConfig.accountId - }, - name: 'andymeier.dev' - } -}, { provider }) - const expression = [ '(http.request.uri.path contains "/.env")', '(http.request.uri.path contains "/.git")', @@ -32,7 +24,7 @@ const expression = [ ].join(' or ') new cloudflare.Ruleset(`${config.identifier}-waf`, { - zoneId: zone.id, + zoneId: andymeierZone.id, name: 'Block vulnerability scanners', kind: 'zone', phase: 'http_request_firewall_custom', diff --git a/pulumi/src/cloudflare/zone.ts b/pulumi/src/cloudflare/zone.ts new file mode 100644 index 0000000..77fb789 --- /dev/null +++ b/pulumi/src/cloudflare/zone.ts @@ -0,0 +1,17 @@ +import * as cloudflare from '@pulumi/cloudflare' +import { provider } from './provider' +import * as config from '../config' + +const getZone = (name: string) => + cloudflare.getZoneOutput({ + filter: { + account: { + id: config.cloudflareConfig.accountId + }, + name + } + }, { provider }) + +export const andymeierZone = getZone('andymeier.dev') +export const andrewmeierZone = getZone('andrewmeier.dev') +export const meiermadeZone = getZone('meiermade.com') From 0d26be051466d1724799c44729d9980036a96cac Mon Sep 17 00:00:00 2001 From: Andrew Meier Date: Tue, 24 Mar 2026 07:53:45 -0400 Subject: [PATCH 3/3] MEIER-320: Expand WAF rule to cover all observed scanner paths --- pulumi/src/cloudflare/waf.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/pulumi/src/cloudflare/waf.ts b/pulumi/src/cloudflare/waf.ts index 18c93d1..cefd6ab 100644 --- a/pulumi/src/cloudflare/waf.ts +++ b/pulumi/src/cloudflare/waf.ts @@ -4,23 +4,47 @@ import { andymeierZone } from './zone' import * as config from '../config' const expression = [ + // Sensitive dotfiles and directories '(http.request.uri.path contains "/.env")', '(http.request.uri.path contains "/.git")', + '(http.request.uri.path contains "/.aws")', + '(http.request.uri.path contains "/.ssh")', + '(http.request.uri.path contains "/.terraform")', + + // CMS and framework probes '(http.request.uri.path contains "/wp-")', '(http.request.uri.path contains "/wordpress")', '(http.request.uri.path contains "/xmlrpc")', '(http.request.uri.path contains "/phpMyAdmin")', '(http.request.uri.path contains "/phpmyadmin")', '(http.request.uri.path contains "/pma")', + + // Admin and server management + '(http.request.uri.path contains "/admin")', '(http.request.uri.path contains "/cgi-bin")', '(http.request.uri.path contains "/actuator")', '(http.request.uri.path contains "/solr")', '(http.request.uri.path contains "/telescope")', '(http.request.uri.path contains "/vendor")', + '(http.request.uri.path contains "/invoker")', + '(http.request.uri.path contains "/balancer-manager")', + + // Credential and config probes + '(http.request.uri.path contains "/credentials")', + '(http.request.uri.path contains "/known_hosts")', + '(http.request.uri.path contains "sendgrid")', + '(http.request.uri.path contains "codecommit")', + '(http.request.uri.path contains "/env.cfg")', + + // Dangerous file extensions '(http.request.uri.path contains ".php")', '(http.request.uri.path contains ".asp")', '(http.request.uri.path contains ".jsp")', '(http.request.uri.path contains ".cgi")', + '(http.request.uri.path contains ".yml")', + '(http.request.uri.path contains ".xml")', + '(http.request.uri.path contains ".bak")', + '(http.request.uri.path contains ".rb")', ].join(' or ') new cloudflare.Ruleset(`${config.identifier}-waf`, {