Episode 10: hardening - firewall rules Docker cannot bypass, Caddy access log and rate limiting, fail2ban in the right chain
- hardening/: ufw after.rules block for DOCKER-USER (ctorigdstport, not dport), fail2ban jail.local with sshd (systemd backend) and caddy-probes (chain DOCKER-USER), filter for the JSON access log, sshd hardening.conf, README with the order and the two traps measured in the episode - caddy/Dockerfile: xcaddy build with caddy-ratelimit, compose builds the image - Caddyfile: common snippet (JSON access log, Server header stripped), vault /admin 404, rate_limit zones on the vault token endpoint and the git login form - HARDENING.md: firewall and fail2ban sections rewritten around what the port scan saw Claude-Session: https://claude.ai/code/session_017iP581CYtPfxdZYPPAyWZA
This commit is contained in:
parent
ccbddab46c
commit
bed41cfaf3
12 changed files with 319 additions and 16 deletions
51
Caddyfile
51
Caddyfile
|
|
@ -3,26 +3,74 @@
|
|||
|
||||
{
|
||||
email {$ACME_EMAIL}
|
||||
# episode 10: rate_limit has to run before reverse_proxy hands the request on
|
||||
order rate_limit before basic_auth
|
||||
}
|
||||
|
||||
# episode 10: one snippet, imported into every site. JSON access log (fail2ban
|
||||
# on the host reads it) and the Server header stripped so responses no longer
|
||||
# announce Rocket/Caddy. client_ip in the log is the real remote address, so no
|
||||
# trusted_proxies needed - the Docker DNAT already carries it through.
|
||||
(common) {
|
||||
log {
|
||||
output file /var/log/caddy/access.log {
|
||||
roll_size 50mb
|
||||
roll_keep 5
|
||||
}
|
||||
format json
|
||||
}
|
||||
header -Server
|
||||
}
|
||||
|
||||
git.{$DOMAIN} {
|
||||
import common
|
||||
# episode 10: throttle login attempts (10 per minute per source IP)
|
||||
rate_limit {
|
||||
zone git_login {
|
||||
match {
|
||||
method POST
|
||||
path /user/login
|
||||
}
|
||||
key {remote_host}
|
||||
events 10
|
||||
window 1m
|
||||
}
|
||||
}
|
||||
reverse_proxy forgejo:3000
|
||||
}
|
||||
|
||||
wiki.{$DOMAIN} {
|
||||
import common
|
||||
reverse_proxy outline:3000
|
||||
}
|
||||
|
||||
vault.{$DOMAIN} {
|
||||
import common
|
||||
# episode 10: the admin panel is a setup tool, not a public page (was 200)
|
||||
@admin path /admin*
|
||||
respond @admin 404
|
||||
# episode 10: throttle the login endpoints (10 per minute per source IP)
|
||||
rate_limit {
|
||||
zone vault_login {
|
||||
match {
|
||||
path /identity/connect/token /api/accounts/prelogin
|
||||
}
|
||||
key {remote_host}
|
||||
events 10
|
||||
window 1m
|
||||
}
|
||||
}
|
||||
reverse_proxy vaultwarden:80
|
||||
}
|
||||
|
||||
status.{$DOMAIN} {
|
||||
import common
|
||||
reverse_proxy uptime-kuma:3001
|
||||
}
|
||||
|
||||
# Push notifications (episode 6). Websockets and long polling pass through as-is.
|
||||
ntfy.{$DOMAIN} {
|
||||
import common
|
||||
reverse_proxy ntfy:80
|
||||
}
|
||||
|
||||
|
|
@ -31,6 +79,7 @@ ntfy.{$DOMAIN} {
|
|||
# A webhook sender out on the internet cannot join your mesh, so this is the
|
||||
# one door that has to stay open - and it is exactly one door wide.
|
||||
n8n.{$DOMAIN} {
|
||||
import common
|
||||
@webhooks path /webhook/* /webhook-test/* /webhook-waiting/*
|
||||
handle @webhooks {
|
||||
reverse_proxy n8n:5678
|
||||
|
|
@ -42,6 +91,7 @@ n8n.{$DOMAIN} {
|
|||
|
||||
# NetBird control plane (episode 5) - dashboard, API, gRPC, relay, built-in IdP
|
||||
vpn.{$DOMAIN} {
|
||||
import common
|
||||
# Native gRPC (needs HTTP/2 cleartext to backend)
|
||||
@grpc header Content-Type application/grpc*
|
||||
reverse_proxy @grpc h2c://netbird-server:80
|
||||
|
|
@ -53,4 +103,3 @@ vpn.{$DOMAIN} {
|
|||
# Dashboard (everything else)
|
||||
reverse_proxy /* netbird-dashboard:80
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue