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..ad0291f 100644 --- a/pulumi/src/cloudflare/index.ts +++ b/pulumi/src/cloudflare/index.ts @@ -1,3 +1,5 @@ +import './zone' import './record' import './redirect' import './tunnel' +import './waf' 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 new file mode 100644 index 0000000..cefd6ab --- /dev/null +++ b/pulumi/src/cloudflare/waf.ts @@ -0,0 +1,62 @@ +import * as cloudflare from '@pulumi/cloudflare' +import { provider } from './provider' +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`, { + zoneId: andymeierZone.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 }) 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')