Harden geoblock, remove secrets from config, support dev host
Refs #1 ## Security * Remove the CrowdSec LAPI key from config.yml. Traefik does not interpolate env vars in its YAML config, so the key had to be literal there - which is why the file kept drifting out of version control. It is now defined as a docker-compose label, where compose substitutes ${CROWDSEC_LAPI_KEY} from .env. config.yml is therefore secret-free and stays in git. NOTE: this renames the middleware crowdsec-bouncer@file -> @docker. Site stacks referencing @file must be updated. The old key remains in this repo's history and must still be rotated and scrubbed - removing it here does not un-expose it. * Move the dashboard basic-auth hash out of docker-compose.yml into .env. In .env every '$' must be doubled to '$$', or compose silently collapses the hash to an empty string and the dashboard rejects every password. * Stop publishing port 8080. The API is not insecure-mode, so nothing served there; publishing it only widened exposure. ## Geoblock (the availability fix) * allowUnknownCountries: false -> true. Previously a geojs.io outage made every lookup "unknown" and therefore blocked, taking sites down for US visitors too - a third-party service in the critical path with no degraded mode. Now an outage degrades to "geoblocking ineffective". CrowdSec still blocks actual attackers. * cacheSize: 25 -> 5000. At 25, nearly every visitor triggered a fresh API call, adding up to 750ms and risking rate limits. ## Dev host support * Add traefik.dev.yml, selected by TRAEFIK_STATIC_CONFIG in .env (defaults to traefik.yml, so prod behaviour is unchanged). It differs from traefik.yml ONLY in the ACME challenge type: public port 80 forwards to prod for its renewals, so an HTTP-01 challenge for a dev hostname hits prod's Traefik, matches no router, and 404s. Dev uses DNS-01 via Route 53 instead, which needs no inbound connectivity. * Add .env.example documenting every variable. ## Captured from prod's working tree Prod had uncommitted drift; these were running but never committed: plugin bumps (bouncer v1.3.3->v1.3.5, geoblock v0.2.7->v0.2.8), the entryPoints HTTP->HTTPS redirect with priority 1, and httpClientTimeout: 0. The priority: 1 on that redirect is load-bearing - it keeps the redirect below normal routers so basil's Host(`localhost`) HTTP route still serves. Do not raise it. ## Verified on dev (prkl10), Traefik 3.7.10 7 routers / 8 middlewares, 0 warnings, 0 errors. Certificates issued via DNS-01. pihole and technitium routers preserved. Sites serving 200.
This commit is contained in:
88
config.yml
88
config.yml
@@ -1,3 +1,8 @@
|
||||
# Traefik dynamic configuration.
|
||||
#
|
||||
# This file is SECRET-FREE and is committed to git. The CrowdSec bouncer, which
|
||||
# is the only middleware needing a credential, is defined as a Docker label in
|
||||
# docker-compose.yml so its key stays in .env (gitignored).
|
||||
http:
|
||||
routers:
|
||||
# Router for Gogs (if it's running outside Docker or on different network)
|
||||
@@ -14,6 +19,30 @@ http:
|
||||
# tls:
|
||||
# certResolver: letsencrypt
|
||||
|
||||
# Pi-hole admin UI - internal only, hosted on 10.20.10.3
|
||||
pihole:
|
||||
rule: "Host(`dns.pkartchner.com`)"
|
||||
entryPoints:
|
||||
- https
|
||||
service: pihole
|
||||
middlewares:
|
||||
- internal-whitelist
|
||||
- secure-headers
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
# Technitium DNS UI - internal only, hosted on 10.20.10.3
|
||||
technitium:
|
||||
rule: "Host(`technitium.pkartchner.com`)"
|
||||
entryPoints:
|
||||
- https
|
||||
service: technitium
|
||||
middlewares:
|
||||
- internal-whitelist
|
||||
- secure-headers
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
services:
|
||||
# Service for Gogs
|
||||
# Disabled - now using Gitea with Docker labels
|
||||
@@ -22,8 +51,20 @@ http:
|
||||
# servers:
|
||||
# - url: "http://gogs.pkartchner.com:3000"
|
||||
|
||||
# Pi-hole on 10.20.10.3
|
||||
pihole:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://10.20.10.3:80"
|
||||
|
||||
# Technitium on 10.20.10.3
|
||||
technitium:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://10.20.10.3:5380"
|
||||
|
||||
middlewares:
|
||||
# Security headers
|
||||
# Security headers — applied to every public site router.
|
||||
secure-headers:
|
||||
headers:
|
||||
forceSTSHeader: true
|
||||
@@ -35,26 +76,28 @@ http:
|
||||
browserXssFilter: true
|
||||
referrerPolicy: "same-origin"
|
||||
|
||||
# IP whitelist for internal network access only
|
||||
# Restrict a router to the internal LAN only.
|
||||
internal-whitelist:
|
||||
ipWhiteList:
|
||||
ipAllowList:
|
||||
sourceRange:
|
||||
- "10.20.10.0/24"
|
||||
- "10.20.140.0/24"
|
||||
- "127.0.0.1/32"
|
||||
|
||||
# Crowdsec bouncer middleware
|
||||
crowdsec-bouncer:
|
||||
plugin:
|
||||
bouncer:
|
||||
enabled: true
|
||||
crowdsecMode: live
|
||||
crowdsecLapiKey: ***REMOVED***
|
||||
crowdsecLapiHost: crowdsec:8080
|
||||
crowdsecLapiScheme: http
|
||||
forwardedHeadersCustomName: X-Custom-Header
|
||||
# NOTE: crowdsec-bouncer is deliberately NOT defined here.
|
||||
#
|
||||
# It carries an API key, and Traefik does not interpolate environment
|
||||
# variables in this file — so defining it here would force a secret into
|
||||
# version control, or force this whole file to be gitignored (which is what
|
||||
# previously happened, costing the repo its "config lives in git" property).
|
||||
#
|
||||
# It is defined as a Docker label on the traefik container instead, where
|
||||
# compose substitutes ${CROWDSEC_LAPI_KEY} from .env.
|
||||
#
|
||||
# Reference it as `crowdsec-bouncer@docker`, NOT `@file`.
|
||||
# This file is therefore secret-free and safe to commit.
|
||||
|
||||
# GeoIP blocking - Allow only US traffic
|
||||
# GeoIP blocking — allow US traffic only.
|
||||
geoblock:
|
||||
plugin:
|
||||
geoblock:
|
||||
@@ -65,14 +108,25 @@ http:
|
||||
logApiRequests: true
|
||||
api: https://get.geojs.io/v1/ip/country/{ip}
|
||||
apiTimeoutMs: 750
|
||||
cacheSize: 25
|
||||
# 25 was the upstream default and is far too small: only 25 IPs are
|
||||
# remembered, so nearly every visitor triggers a fresh lookup against
|
||||
# a free third-party API, adding up to 750ms before WordPress starts
|
||||
# and risking rate limits. A few thousand entries is a trivial amount
|
||||
# of memory and turns the API call into a rare event.
|
||||
cacheSize: 5000
|
||||
forceMonthlyUpdate: true
|
||||
allowUnknownCountries: false
|
||||
# FAIL OPEN. With this false, a geojs.io outage or slow response makes
|
||||
# every lookup "unknown" and therefore blocked — taking the site down
|
||||
# for US visitors too, because of a third-party service we do not
|
||||
# control sitting in the path of every request. Allowing unknowns
|
||||
# means an outage degrades to "geoblocking temporarily ineffective"
|
||||
# instead of "site is down". CrowdSec still blocks actual attackers.
|
||||
allowUnknownCountries: true
|
||||
unknownCountryApiResponse: nil
|
||||
countries:
|
||||
- US
|
||||
|
||||
# Rate limiting for Harbor - Prevent brute force attacks
|
||||
# Rate limiting, used by Harbor on prod.
|
||||
harbor-ratelimit:
|
||||
rateLimit:
|
||||
average: 100
|
||||
|
||||
Reference in New Issue
Block a user