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.
73 lines
3.4 KiB
YAML
73 lines
3.4 KiB
YAML
services:
|
|
traefik:
|
|
image: traefik:latest
|
|
container_name: traefik
|
|
restart: always
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
networks:
|
|
- traefik
|
|
- crowdsec
|
|
ports:
|
|
- "80:80"
|
|
- "443:443"
|
|
# Port 8080 is deliberately NOT published. The dashboard is reached
|
|
# through the authenticated router below. Publishing 8080 only exposes
|
|
# the API when `api.insecure: true`, which we do not set.
|
|
environment:
|
|
- TZ=America/Denver
|
|
# Route 53 DNS-01 challenge. Values live in .env (gitignored).
|
|
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
|
|
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
|
|
- AWS_REGION=${AWS_REGION}
|
|
volumes:
|
|
- /etc/localtime:/etc/localtime:ro
|
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
|
# Static config differs between hosts ONLY in the ACME challenge type:
|
|
# prod uses HTTP-01 (public port 80 lands there), dev must use DNS-01.
|
|
# Selected via .env so both environments share one branch instead of
|
|
# diverging permanently.
|
|
- ${TRAEFIK_STATIC_CONFIG:-./traefik.yml}:/traefik.yml:ro
|
|
- ./acme.json:/acme.json
|
|
- ./config.yml:/config.yml:ro
|
|
- ./logs:/var/log/traefik
|
|
labels:
|
|
- "traefik.enable=true"
|
|
# Dashboard — LAN-only AND password protected.
|
|
- "traefik.http.routers.traefik.rule=Host(`${TRAEFIK_DASHBOARD_HOST}`)"
|
|
- "traefik.http.routers.traefik.entrypoints=https"
|
|
- "traefik.http.routers.traefik.tls.certresolver=letsencrypt"
|
|
- "traefik.http.routers.traefik.service=api@internal"
|
|
- "traefik.http.routers.traefik.middlewares=traefik-auth,internal-whitelist@file"
|
|
# Credentials come from .env (gitignored), not from this file.
|
|
# Generate with: htpasswd -nb admin 'yourpassword'
|
|
- "traefik.http.middlewares.traefik-auth.basicauth.users=${TRAEFIK_DASHBOARD_AUTH}"
|
|
# Named redirect middleware, referenced by site stacks on their HTTP router.
|
|
- "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
|
|
# CrowdSec bouncer.
|
|
#
|
|
# Defined HERE rather than in config.yml on purpose: Traefik does not
|
|
# interpolate environment variables in its YAML config files, so the API
|
|
# key would have to be written literally into config.yml — which is why
|
|
# that file was previously gitignored and the repo lost its "config lives
|
|
# in git" property. Compose DOES substitute ${...} in labels, so defining
|
|
# it here keeps the key in .env alone and lets config.yml be committed.
|
|
#
|
|
# Consequence: this middleware is `crowdsec-bouncer@docker`, NOT `@file`.
|
|
# Site stacks must reference it with the @docker suffix.
|
|
- "traefik.http.middlewares.crowdsec-bouncer.plugin.bouncer.enabled=true"
|
|
- "traefik.http.middlewares.crowdsec-bouncer.plugin.bouncer.crowdsecMode=live"
|
|
- "traefik.http.middlewares.crowdsec-bouncer.plugin.bouncer.crowdsecLapiKey=${CROWDSEC_LAPI_KEY}"
|
|
- "traefik.http.middlewares.crowdsec-bouncer.plugin.bouncer.crowdsecLapiHost=crowdsec:8080"
|
|
- "traefik.http.middlewares.crowdsec-bouncer.plugin.bouncer.crowdsecLapiScheme=http"
|
|
- "traefik.http.middlewares.crowdsec-bouncer.plugin.bouncer.forwardedHeadersCustomName=X-Custom-Header"
|
|
|
|
networks:
|
|
# Pre-create once per host: docker network create traefik
|
|
traefik:
|
|
name: traefik
|
|
external: true
|
|
crowdsec:
|
|
name: crowdsec
|
|
external: true
|