selfhostedops-stack/.env.example
Maximilian Grimm 88670f028f Episode 12/13: CrowdSec next to fail2ban, and an AppSec WAF in Caddy
Measured on the demo box against the same 24h of real traffic.

Episode 12 (detection, blocklist, firewall):
- crowdsec/ with install notes and the traps: port 8080 clash, empty first
  blocklist pull, replay forensic-mode dropping events (FIFO workaround),
  ssh-time-based-bf shipping remediation:false, the AppSec collections that
  refuse to start without extra rules.
- firewall bouncer in nftables mode (input + forward), which blocks
  Docker-published ports and survives ufw reload; iptables/DOCKER-USER does not.
- HARDENING.md episode 12 section with the numbers: fail2ban 4 banned IPs vs
  CrowdSec 23 detected / 1 blocked, blocklist overlap 50% of IPs but 19% of
  attempts, and the mode=normal blind spot for key-only root attempts.

Episode 13 (AppSec WAF):
- caddy/Dockerfile now builds the CrowdSec bouncer and AppSec handler alongside
  rate-limit, pinned to v0.14.1.
- Caddyfile imports a crowdsec snippet (bouncer + appsec) on every site except
  vpn; compose passes CROWDSEC_BOUNCER_KEY and host.docker.internal.
- Measured: virtual patching blocks known CVEs only (7 of 41 probes, near-zero
  false positives); OWASP CRS raises that to 27 of 41 but breaks git push and
  wiki editing at default paranoia. Default stays vpatch+generic.

fail2ban config unchanged. No secrets or real domains/IPs in tracked files.
2026-09-16 20:34:18 +02:00

86 lines
3.8 KiB
Text

# ~$ selfhostedops — copy me: cp .env.example .env
# Generate secrets with: openssl rand -hex 32
# Your base domain. Services live at git.* wiki.* vault.* status.* ntfy.* vpn.*
# DNS: one wildcard A (and AAAA) record *.ops.example.com -> your server IP
DOMAIN=ops.example.com
# Let's Encrypt notifications + expiry mails
ACME_EMAIL=you@example.com
TZ=Europe/Berlin
# --- Outline ---
OUTLINE_PG_PASSWORD=CHANGE_ME_openssl_rand_hex_32
OUTLINE_SECRET_KEY=CHANGE_ME_openssl_rand_hex_32
OUTLINE_UTILS_SECRET=CHANGE_ME_openssl_rand_hex_32
# From Forgejo: Site Administration -> Applications -> OAuth2
# Redirect URI: https://wiki.<DOMAIN>/auth/oidc.callback
OUTLINE_OIDC_CLIENT_ID=
OUTLINE_OIDC_CLIENT_SECRET=
# --- Vaultwarden ---
# Admin panel token (argon2 hash recommended, plain string works to start)
VAULTWARDEN_ADMIN_TOKEN=CHANGE_ME_openssl_rand_hex_32
# Flip to false once your team has signed up:
VAULTWARDEN_SIGNUPS_ALLOWED=true
# Single sign-on against your own Forgejo (episode 4).
# From Forgejo: Site Administration -> Applications -> OAuth2 Application
# Redirect URI must be EXACTLY: https://vault.<DOMAIN>/identity/connect/oidc-signin
VAULTWARDEN_SSO_CLIENT_ID=
VAULTWARDEN_SSO_CLIENT_SECRET=
# --- wg-easy (optional, --profile vpn) ---
# Generate with: docker run --rm ghcr.io/wg-easy/wg-easy wgpw 'yourpassword'
WGEASY_PASSWORD_HASH=
# --- CI (episode 2, optional: docker compose --profile ci up -d) ---
# One shared secret registers the runner declaratively. Generate it with:
# openssl rand -hex 20
# Then derive the UUID Forgejo will know it by:
# python3 -c 'import uuid,sys;print(uuid.UUID(bytes=sys.argv[1][:16].encode()))' <secret>
# And register it on the Forgejo side:
# docker compose exec -u git forgejo forgejo forgejo-cli actions register \
# --name box-runner --labels docker --secret <secret>
# A registration token copied from the web UI does NOT work here.
RUNNER_SECRET=
RUNNER_UUID=
# --- ntfy push notifications (episode 6) ---
# No secrets here: ntfy keeps its users in its own database. Create them once:
# docker compose exec -e NTFY_PASSWORD=<pw> ntfy ntfy user add --role=user kuma
# docker compose exec ntfy ntfy access kuma alerts write-only
# See monitoring/README.md for the full walkthrough.
# --- n8n automation (episode 8) ---
# No variables needed here either: the workflows keep their credentials inside
# n8n, encrypted with the key in /home/node/.n8n/config. That file is inside the
# n8n_data volume and therefore inside the nightly backup from episode 7.
#
# If you would rather pin the key yourself (recommended once more than one
# person can restore this box), uncomment the line below and put the same value
# in your password manager. It wins over the file, and the file is then never
# written. Changing it later makes every existing credential undecryptable.
# openssl rand -base64 24
# N8N_ENCRYPTION_KEY=
#
# The ntfy user the workflows publish as:
# docker compose exec -e NTFY_PASSWORD=<pw> ntfy ntfy user add --role=user n8n
# docker compose exec ntfy ntfy access n8n alerts write-only
# docker compose exec ntfy ntfy access n8n deploys write-only
# See automation/README.md for the rest.
# --- Episode 9: the ops triage agent ---------------------------------------
# The shared secret between n8n and the ops-actions runner. Anything that can
# present this header can ask for a container restart, so treat it like a key:
# openssl rand -hex 24
# The runner still refuses any action or any target that is not on its own
# hard-coded allowlists, so a leaked token cannot widen the blast radius.
OPS_ACTIONS_TOKEN=
# --- Episode 12: CrowdSec -------------------------------------------------
# The Caddy bouncer's key for the CrowdSec Local API on the host:
# cscli bouncers add caddy-bouncer -o raw
# Without it Caddy still starts, but blocks nothing and logs auth errors.
CROWDSEC_BOUNCER_KEY=