From ae1112efc1c28207aa0fb1f79c782268ac1003f6 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 24 Oct 2023 14:42:54 +0200 Subject: [PATCH] Add docker-compose-pgp.yml with basic configuration --- .env | 77 +++++++++ docker-compose-pgp.yml | 31 ++++ docker-compose-standalone.yml | 120 ++++++++++++++ docker-compose.yml | 8 +- haproxy.d/00_global.cfg | 52 +++++++ haproxy.d/10_defaults.cfg | 22 +++ haproxy.d/20_LOCAL_peers.cfg | 15 ++ haproxy.d/20_LOCAL_peers.cfg.tmpl | 15 ++ haproxy.d/30_st_global_ddos.cfg | 27 ++++ haproxy.d/40_fe_http_s_balancer.cfg | 31 ++++ haproxy.d/50_fe_http_handler.cfg | 181 ++++++++++++++++++++++ haproxy.d/60_fe_hockeypuck_ddos_lb.cfg | 109 +++++++++++++ haproxy.d/70_fe_prometheus.cfg | 10 ++ haproxy.d/80_be.cfg | 75 +++++++++ haproxy.d/90_LOCAL_be_hockeypuck.cfg | 32 ++++ haproxy.d/90_LOCAL_be_hockeypuck.cfg.tmpl | 32 ++++ haproxy.d/README.md | 30 ++++ lists/blacklist.list | 0 lists/whitelist.list | 0 19 files changed, 863 insertions(+), 4 deletions(-) create mode 100644 .env create mode 100644 docker-compose-pgp.yml create mode 100644 docker-compose-standalone.yml create mode 100644 haproxy.d/00_global.cfg create mode 100644 haproxy.d/10_defaults.cfg create mode 100644 haproxy.d/20_LOCAL_peers.cfg create mode 100644 haproxy.d/20_LOCAL_peers.cfg.tmpl create mode 100644 haproxy.d/30_st_global_ddos.cfg create mode 100644 haproxy.d/40_fe_http_s_balancer.cfg create mode 100644 haproxy.d/50_fe_http_handler.cfg create mode 100644 haproxy.d/60_fe_hockeypuck_ddos_lb.cfg create mode 100644 haproxy.d/70_fe_prometheus.cfg create mode 100644 haproxy.d/80_be.cfg create mode 100644 haproxy.d/90_LOCAL_be_hockeypuck.cfg create mode 100644 haproxy.d/90_LOCAL_be_hockeypuck.cfg.tmpl create mode 100644 haproxy.d/README.md create mode 100644 lists/blacklist.list create mode 100644 lists/whitelist.list diff --git a/.env b/.env new file mode 100644 index 0000000..85d4754 --- /dev/null +++ b/.env @@ -0,0 +1,77 @@ +########################################################### +## HOCKEYPUCK STANDALONE SITE CONFIGURATION TEMPLATE +## Edit this, then run ./mkconfig.bash +########################################################### + +####################################################################### +## docker-compose<1.29 does not parse quoted values like a POSIX shell. +## This means that normally you should not quote values in this file, +## as docker-compose's old behaviour is highly unintuitive. +## +## The scripts in this directory try to compensate, and can parse +## *double* quotes around ALIAS_FQDNS, CLUSTER_FQDNS and HKP_LOG_FORMAT +## values *only*, as these values will normally contain whitespace and +## so most users will instinctively quote them anyway. +## +## In all other cases, enclosing quotes MUST NOT be used. +####################################################################### + +# This is the primary FQDN of your site +FQDN=pgp.dobrev.it +# Any extra FQDN aliases, space-separated +ALIAS_FQDNS="" +# A contact email address for the site operator (that's you!) +EMAIL=martin@dobrev.it +# PGP encryption key for the above email address +FINGERPRINT=0x283A56AE9544F3C87C71ADB0CAAAE2B8C198C9AE +# ACME Directory Resource URI (use Let's Encrypt if empty) +ACME_SERVER= + +########################################################### +# You normally won't need to change anything below here +########################################################### + +POSTGRES_USER=hkp +POSTGRES_PASSWORD=TDJEMUERMETDU4LKQXEWMAZZODXVOKGER7I6ZN7IEP6YUQNW +RELEASE=standalone + +# Parameterised default values for haproxy config + +# The following is only required in shim mode +#KEYSERVER_HOST_PORT=hockeypuck:11371 + +# Remote URL for fetching tor exit relays list +TOR_EXIT_RELAYS_URL=https://www.dan.me.uk/torlist/?exit + +# Advanced HAProxy configuration options + +# Set this to the host:port that your HAProxy peers will see +#HAP_PEER_HOST_PORT=127.0.0.1:1395 +# Every name and alias of your other cluster members, space-separated +# Note that their IPs should also be added to ./haproxy/etc/lists/whitelist.list +CLUSTER_FQDNS="" + +# Set these to "port" or "host:port" to override the listening hostip/port(s) +HAP_HTTP_HOST_PORT=8081 +#HAP_HTTPS_HOST_PORT=443 +#HAP_HKP_HOST_PORT=11371 + +# Uncomment *at most one* of the BEHIND settings to trust an upstream proxy's request headers. +# This is vital so that rate limiting applies to the client's real IP and not the proxy's. +# +# Trust CF-Connecting-IP: headers +#HAP_BEHIND_CLOUDFLARE=true +# Trust X-Forwarded-For: headers +#HAP_BEHIND_PROXY=true + +# Set this to e.g. /etc/letsencrypt in order to share certificates with the host. +# Note that the certbot container is responsible for renewing these. +#CERTBOT_CONF=certbot_conf + +# MIGRATION_HAPROXY_DONE (DO NOT REMOVE THIS LINE!) + +# Set the HAProxy log format +HAP_LOG_FORMAT="%ci:%cp [%t] %ft %b/%s %Tq/%Tw/%Tc/%Tr/%Tt %ST %U/%B %CC %CS %tsc %ac/%fc/%bc/%sc/%rc %sq/%bq %hr %hs %{+Q}r" + +# MIGRATION_HAPROXY_LOGFORMAT_DONE (DO NOT REMOVE THIS LINE!) +# MIGRATION_3_DONE (DO NOT REMOVE THIS LINE!) diff --git a/docker-compose-pgp.yml b/docker-compose-pgp.yml new file mode 100644 index 0000000..2d8ea57 --- /dev/null +++ b/docker-compose-pgp.yml @@ -0,0 +1,31 @@ +version: '3.3' + +services: + haproxy: + build: + context: . + dockerfile: Dockerfile + image: hockey-stick + user: root + restart: always + environment: + - FQDN + - ALIAS_FQDNS + - KEYSERVER_HOST_PORT=192.168.122.1:11371 + - HAP_CONF_DIR=/usr/local/etc/haproxy + - HAP_CACHE_DIR=/tmp + - HAP_LOG_FORMAT + - HAP_BEHIND_CLOUDFLARE + - HAP_BEHIND_PROXY + - HAP_DISABLE_PROMETHEUS=true + - HAP_DISABLE_CERTBOT=true + - HAP_DISABLE_SSL=true + command: + - haproxy + - -f + - /etc/haproxy/haproxy.d + ports: + - 8081:80 + volumes: + - ./haproxy.d:/etc/haproxy/haproxy.d + - ./lists:/usr/local/etc/haproxy/lists diff --git a/docker-compose-standalone.yml b/docker-compose-standalone.yml new file mode 100644 index 0000000..667e1d9 --- /dev/null +++ b/docker-compose-standalone.yml @@ -0,0 +1,120 @@ +version: '3.7' +services: + hockeypuck: + image: hockeypuck/hockeypuck:${RELEASE} + build: + context: ../../.. + ports: + - "${HKP_RECON_HOST_PORT:-11370}:11370" + restart: always + depends_on: + - postgres + volumes: + - ./hockeypuck/etc:/hockeypuck/etc + - hkp_data:/hockeypuck/data + - pgp_import:/hockeypuck/import + logging: + options: + max-size: "10m" + max-file: "3" + + postgres: + image: postgres:11 + restart: always + environment: + - POSTGRES_USER + - POSTGRES_PASSWORD + - POSTGRES_DB=hkp + volumes: + - pg_data:/var/lib/postgresql/data + + prometheus: + image: prom/prometheus:v2.43.0 + restart: always + volumes: + - prom_data:/prometheus + - ./prometheus/etc:/etc/prometheus + command: + - "--web.external-url=/monitoring/prometheus/" + - "--config.file=/etc/prometheus/prometheus.yml" + - "--storage.tsdb.path=/prometheus" + - "--web.console.libraries=/usr/share/prometheus/console_libraries" + - "--web.console.templates=/usr/share/prometheus/consoles" + + haproxy: + image: haproxy:2.6-alpine + ports: + - "${HAP_HTTP_HOST_PORT:-80}:80" + - "${HAP_HTTPS_HOST_PORT:-443}:443" + - "${HAP_HKP_HOST_PORT:-11371}:11371" + - "${HAP_PEER_HOST_PORT:-127.0.0.1:1395}:1395" + user: root + restart: always + init: true + environment: + - FQDN + - ALIAS_FQDNS + - CLUSTER_FQDNS + - PROMETHEUS_HOST_PORT=prometheus:9090 + - CERTBOT_HOST_PORT=certbot:80 + - KEYSERVER_HOST_PORT=hockeypuck:11371 + - HAP_DHPARAM_FILE=/etc/letsencrypt/ssl-dhparams.pem + - HAP_CONF_DIR=/usr/local/etc/haproxy + - HAP_CACHE_DIR=/var/cache/haproxy + - HAP_CERT_DIR=/etc/letsencrypt/live + - HAP_LOG_FORMAT + - HAP_BEHIND_CLOUDFLARE + - HAP_BEHIND_PROXY + depends_on: + - certbot + - haproxy_cache + - haproxy_internal + - prometheus + volumes: + - ./haproxy-entrypoint.sh:/usr/local/bin/haproxy-entrypoint.sh + - ./haproxy/etc:/usr/local/etc/haproxy + - haproxy_cache:/var/cache/haproxy + - ${CERTBOT_CONF:-certbot_conf}:/etc/letsencrypt + entrypoint: "/usr/local/bin/haproxy-entrypoint.sh -f /usr/local/etc/haproxy/haproxy.d" + logging: + options: + max-size: "10m" + max-file: "3" + + haproxy_internal: + image: haproxy:2.6-alpine + user: root + restart: always + volumes: + - ./haproxy/etc/haproxy-internal.cfg:/usr/local/etc/haproxy/haproxy.cfg + entrypoint: "/bin/sh -c 'export HOSTNAME=$$(hostname); export HOST_IP=$$(hostname -i); haproxy -f /usr/local/etc/haproxy/haproxy.cfg'" + logging: + options: + max-size: "10m" + max-file: "3" + + haproxy_cache: + image: instrumentisto/rsync-ssh + restart: always + volumes: + - haproxy_cache:/var/cache/haproxy + entrypoint: "/bin/sh -c 'trap exit TERM; touch /var/cache/haproxy/tor_exit_relays.list; while :; do sleep 1800; wget \"${TOR_EXIT_RELAYS_URL}\" -O /var/cache/haproxy/tor_exit_relays.list; done'" + logging: + options: + max-size: "10m" + max-file: "1" + + certbot: + image: certbot/certbot + restart: always + volumes: + - ${CERTBOT_CONF:-certbot_conf}:/etc/letsencrypt + entrypoint: "/bin/sh -c 'trap exit TERM; while :; do for i in /etc/letsencrypt/live/*; do [ -d \"$$i\" ] && ln -sf privkey.pem $$i/fullchain.pem.key; certbot --standalone renew; done; sleep 12h & wait $${!}; done;'" + +volumes: + hkp_data: {} + pg_data: {} + prom_data: {} + pgp_import: {} + haproxy_cache: {} + certbot_conf: {} diff --git a/docker-compose.yml b/docker-compose.yml index 74fabfc..acfea47 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,4 @@ -version: '3.8' +version: '3.3' services: haproxy: @@ -7,14 +7,14 @@ services: dockerfile: Dockerfile image: hockey-stick ports: - - 80:80 + - 8081:80 volumes: - ./haproxy.cfg:/etc/haproxy/haproxy.cfg + depends_on: + - http-echo http-echo: image: mendhak/http-https-echo - depends_on: - - haproxy environment: - DISABLE_REQUEST_LOGS=true - HTTP_PORT=5678 diff --git a/haproxy.d/00_global.cfg b/haproxy.d/00_global.cfg new file mode 100644 index 0000000..c51b16a --- /dev/null +++ b/haproxy.d/00_global.cfg @@ -0,0 +1,52 @@ +global + lua-prepend-path /etc/haproxy/lua/lib/?.lua + lua-load /etc/haproxy/lua/dnsbl.lua + + # Map threads to individual CPU cores. Assumes at least 2 available cores. + cpu-map auto:1/1-2 0-1 + + .if defined(HAP_DISABLE_SSL) + .notice "SSL support disabled via environment" + .else + + # generated 2022-10-15, Mozilla Guideline v5.6, HAProxy 2.4, OpenSSL 3.0.2, intermediate configuration + # https://ssl-config.mozilla.org/#server=haproxy&version=2.4&config=intermediate&openssl=3.0.2&guideline=5.6 + ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384 + ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 + ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets + + ssl-default-server-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384 + ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 + ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets + + ssl-dh-param-file "${HAP_DHPARAM_FILE}" + + # lower the record size to improve Time to First Byte (TTFB) + tune.ssl.maxrecord 1419 + + # Tune SSL cache size + tune.ssl.cachesize 500000 + # Tune DH params + tune.ssl.default-dh-param 2048 # TODO: Export as variable. Most systems use 2048 by default + + .endif # HAP_DISABLE_SSL + + log stdout format raw local0 + + # Allow maximum of 200 000 connections + maxconn 200000 + + tune.comp.maxlevel 5 + maxcompcpuusage 98 + + # Number of threads per process + nbthread 12 + + # Allow local admin socket + stats socket "${HAP_CACHE_DIR}"/haproxy.admin.sock mode 660 level admin expose-fd listeners + stats timeout 30s + + # Perform stateless reloads on HUP + master-worker + + server-state-file "${HAP_CACHE_DIR}"/server-state diff --git a/haproxy.d/10_defaults.cfg b/haproxy.d/10_defaults.cfg new file mode 100644 index 0000000..7dc4e1d --- /dev/null +++ b/haproxy.d/10_defaults.cfg @@ -0,0 +1,22 @@ +defaults + load-server-state-from-file global + + option dontlognull + option http-server-close + option splice-response + option clitcpka + option srvtcpka + option tcp-smart-accept + option tcp-smart-connect + option contstats + retries 3 + + timeout http-request 5s + timeout http-keep-alive 5s + timeout connect 5s + timeout client 60s + timeout client-fin 60s + timeout tunnel 40m # timeout to use with WebSocket and CONNECT + timeout server 150s + timeout tarpit 15s + timeout queue 10s diff --git a/haproxy.d/20_LOCAL_peers.cfg b/haproxy.d/20_LOCAL_peers.cfg new file mode 100644 index 0000000..9382613 --- /dev/null +++ b/haproxy.d/20_LOCAL_peers.cfg @@ -0,0 +1,15 @@ +#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# LOCAL site configuration file for haproxy clusters +# This file is NOT overwritten on upgrade +#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +#peers haproxy-peers +# peer "${HOSTNAME}" "${HOST_IP}":1395 +# peer haproxy-internal haproxy_internal:1395 + + #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # Uncomment and edit the below to share firewall state across multiple stacks + #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + #peer haproxy-remote1 10.0.0.1:1395 + #peer haproxy-remote2 10.0.0.2:1395 diff --git a/haproxy.d/20_LOCAL_peers.cfg.tmpl b/haproxy.d/20_LOCAL_peers.cfg.tmpl new file mode 100644 index 0000000..301ab9b --- /dev/null +++ b/haproxy.d/20_LOCAL_peers.cfg.tmpl @@ -0,0 +1,15 @@ +#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# LOCAL site configuration file for haproxy clusters +# This file is NOT overwritten on upgrade +#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +peers haproxy-peers + peer "${HOSTNAME}" "${HOST_IP}":1395 + peer haproxy-internal haproxy_internal:1395 + + #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # Uncomment and edit the below to share firewall state across multiple stacks + #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + #peer haproxy-remote1 10.0.0.1:1395 + #peer haproxy-remote2 10.0.0.2:1395 diff --git a/haproxy.d/30_st_global_ddos.cfg b/haproxy.d/30_st_global_ddos.cfg new file mode 100644 index 0000000..0c35ae0 --- /dev/null +++ b/haproxy.d/30_st_global_ddos.cfg @@ -0,0 +1,27 @@ +backend st_global_ddos + # Stick Table Definitions + # - conn_cur: count active connections + # - conn_rate(10s): average incoming connection rate over 10 seconds + # - http_err_rate(3s): Monitors the number of errors generated by an IP over a period of 3 seconds + # - http_req_rate(10s): Monitors the number of request sent by an IP over a period of 10 seconds + stick-table type ipv6 size 2m expire 30s store conn_cur,conn_rate(10s),http_req_rate(10s),http_err_rate(3s) + +# Cache DNSBL requests +backend st_known_tor_visitors + # Store the IP address of the client after DNSBL lookup + # + # gpc0 > 0 means the IP address is not blacklisted + # gpc1 > 0 means the IP address is blacklisted + # + # Information is stored for 30 minutes + stick-table type ipv6 size 2m expire 30m store gpc0,gpc1 + +backend st_tor_request_rate + stick-table type string len 32 size 10 expire 24h store conn_cur,conn_rate(10s),http_req_rate(10s),http_err_rate(10s) + +backend st_tor_24h_ban + stick-table type ipv6 size 1m expire 24h store gpc0 + +backend st_tor_24days_ban + stick-table type ipv6 size 1m expire 24d store gpc0 + diff --git a/haproxy.d/40_fe_http_s_balancer.cfg b/haproxy.d/40_fe_http_s_balancer.cfg new file mode 100644 index 0000000..d48640c --- /dev/null +++ b/haproxy.d/40_fe_http_s_balancer.cfg @@ -0,0 +1,31 @@ +# Frontend to handle HTTP and HTTPS requests +.if defined(HAP_DISABLE_SSL) +.notice "Not listening on port 443" +.else +.notice "Listening on ports 443 and 11371 in dual-mode HTTP(S)" + +frontend fe_http_s_balancer + bind :443 + bind :::443 + bind :11371 + bind :::11371 + mode tcp + + acl blacklisted src -f "${HAP_CONF_DIR}"/lists/blacklist.list + + tcp-request inspect-delay 5s + + # option tcplog + # log stdout format raw local2 + # log-format "%ci:%cp [%t] %ft %b/%s %Tw/%Tc/%Tt %B %ts %ac/%fc/%bc/%sc/%rc %sq/%bq" + + tcp-request content reject if blacklisted + tcp-request content accept if HTTP + tcp-request content accept if { req.ssl_hello_type 1 } + + use_backend be_forward_http if HTTP + use_backend be_forward_https if { req.ssl_hello_type 1 } + + default_backend be_tarpit + +.endif # HAP_DISABLE_SSL diff --git a/haproxy.d/50_fe_http_handler.cfg b/haproxy.d/50_fe_http_handler.cfg new file mode 100644 index 0000000..ebc65b6 --- /dev/null +++ b/haproxy.d/50_fe_http_handler.cfg @@ -0,0 +1,181 @@ +# Frontend to handle the actual HTTP requests +frontend fe_http_handler + bind :80 + bind :::80 + + # Loopback interface + bind abns@loopback-http accept-proxy + + .if defined(HAP_DISABLE_SSL) + .notice "Listening on port 11371 in single-mode HTTP; disabling HTTPS loopback listener" + bind *:11371 + .else + # Loopback interface for https + bind abns@loopback-https accept-proxy ssl crt "${HAP_CERT_DIR}"/"${FQDN}"/fullchain.pem alpn h2,http/1.1 + .endif # HAP_DISABLE_SSL + + mode http + + # ~~ HAProxy LB Configuration ~~ + # Enable/Disable trust of Cloudflare and/or X-Forwarded-For headers + # + # By default HAProxy is running at the edge of your infrastructure + # and picks the client IP from the TCP connection. If you are running + # HAProxy behind a reverse proxy or Cloudflare, you need to enable + # the following options to trust the headers. + + .if defined(HAP_BEHIND_CLOUDFLARE) + .notice "Enabling Cloudflare support" + http-request set-var(txn.cloudflare) str(ENABLED) + .elseif defined(HAP_BEHIND_PROXY) + .notice "Enabling reverse proxy support" + http-request set-var(txn.cdn) str(ENABLED) + .endif + + # HAProxy behind Cloudflare and/or a reverse proxy + acl is_behind_cloudflare var(txn.cloudflare) -m found + acl is_behind_proxy var(txn.cdn) -m found + + # ~~ End HAProxy LB Configuration ~~ + + # Host header is set + acl is_host_set hdr(host) -m found + + # Instance is stopping + acl is_stopping stopping eq 1 + + # Match ACME challenge + acl acme_challenge path_beg /.well-known/acme-challenge/ + + # Match Prometheus metrics + acl get_prometheus path_beg /monitoring/prometheus + + # .well-known/security.txt settings + acl well_known_sec path_beg -i /security.txt + acl well_known_sec path_beg -i /.well-known/security.txt + + # Check if requestor is blacklisted + acl blacklisted src -f "${HAP_CONF_DIR}"/lists/blacklist.list + tcp-request connection reject if blacklisted + + # Capture request headers + capture request header Host len 253 + capture request header X-REQ-ID len 32 + capture request header X-Forwarded-For len 64 + capture request header Accept-Language len 64 + capture request header Referer len 64 + capture request header User-Agent len 128 + capture request header Content-Length len 10 + + # Add the X-Forwarded-For header + option forwardfor except 127.0.0.0/8 + + # ~~~ DDoS protection ~~~ + # HAproxy tracks client IPs into a global stick table. Each IP is + # stored for a limited amount of time, with several counters attached + # to it. When a new connection comes in, the stick table is evaluated + # to verify that the new connection from this client is allowed to + # continue. + tcp-request inspect-delay 5s + + # Enable tracking of counters for ip in the default stick-table, using CF-Connecting-IP or X-Forwarded-For + acl HAS_CF_CONNECTING_IP hdr_cnt(CF-Connecting-IP) eq 1 + acl HAS_X_FORWARDED_FOR hdr_cnt(X-Forwarded-For) gt 0 + + # Block invalid requests + http-request deny deny_status 400 hdr Denial-Reason "No direct hits allowed. Missing X-Forwarded-For header" if is_behind_proxy !HAS_X_FORWARDED_FOR + http-request deny deny_status 400 hdr Denial-Reason "No direct hits allowed. Missing headers" if is_behind_cloudflare !HAS_CF_CONNECTING_IP !HAS_X_FORWARDED_FOR + + # ~~ HAProxy client IP tracking ~~ + # Track requests from CF-Connecting-IP or X-Forwarded-For header, or source IP into the global ddos stick-table + # Please see above for options to enable/disable tracking of client IPs from CF-Connecting-IP or X-Forwarded-For headers + + # Track CF-Connecting-IP header if present and behind Cloudflare + tcp-request content track-sc0 hdr_ip(CF-Connecting-IP,-1) table st_global_ddos if HTTP HAS_CF_CONNECTING_IP is_behind_cloudflare + http-request set-var(txn.clientIP) hdr_ip(CF-Connecting-IP,-1) if HTTP HAS_CF_CONNECTING_IP is_behind_cloudflare + + # Track X-Forwarded-For header if present and behind a proxy + tcp-request content track-sc0 hdr_ip(X-Forwarded-For,-1) table st_global_ddos if HTTP HAS_X_FORWARDED_FOR is_behind_proxy + http-request set-var(txn.clientIP) hdr_ip(X-Forwarded-For,-1) if HTTP HAS_X_FORWARDED_FOR is_behind_proxy + + # Track source IP + tcp-request content track-sc0 src table st_global_ddos if HTTP !is_behind_cloudflare !is_behind_proxy + http-request set-var(txn.clientIP) src if HTTP !is_behind_cloudflare !is_behind_proxy + + # Set CF-Connecting-IP header to the tracked client IP. All subsequent requests from this client will have this header set. + # And all ACL rules in backends etc. must be evaluated against this header. + http-request set-header CF-Connecting-IP %[var(txn.clientIP)] + # ~~ End HAProxy client IP tracking ~~ + + # Capture hockeypuck backend stats. + http-request set-var(txn.hockeypuck_primary) nbsrv(be_hockeypuck_primary) + http-request set-var(txn.hockeypuck) nbsrv(be_hockeypuck) + http-request capture var(txn.hockeypuck_primary) len 2 + http-request capture var(txn.hockeypuck) len 2 + + # Generic health monitor for umbrella LBs in enterprise deployments. + acl BACKEND_DEAD nbsrv(be_hockeypuck) lt 1 + monitor-uri /_healthz + monitor fail if BACKEND_DEAD + + # Set Host header if not set + http-request set-header Host str("${FQDN}":%[dst_port]) if !is_host_set + + # Store Host header to construct Via response for keyserver backend + http-request set-var(txn.serverName) req.hdr(host) if is_host_set + + # TARPIT the new connection if the client already has 80 opened + http-request tarpit if { src_conn_cur(st_global_ddos) ge 80 } + + # TARPIT the new connection if the client has opened more than 40 connections in 3 seconds + http-request tarpit if { src_conn_rate(st_global_ddos) ge 40 } + + # TARPIT the connection if the client has passed the HTTP error rate (10s) + http-request tarpit if { sc0_http_err_rate(st_global_ddos) gt 20 } + + # TARPIT the connection if the client has passed the HTTP request rate (10s) + http-request tarpit if { sc0_http_req_rate(st_global_ddos) gt 100 } + + .if defined(HAP_DISABLE_PROMETHEUS) + .notice "Bypass Prometheus whitelisting rules" + .else + # Only nice people get to see our internal monitoring + http-request deny if get_prometheus !{ src -f "${HAP_CONF_DIR}"/lists/prometheus_whitelist.list } + .endif + + # Whitelisting options + http-request allow if { req.hdr_ip(CF-Connecting-IP,-1) -f "${HAP_CONF_DIR}"/lists/whitelist.list } + + # Options + option httplog + option http-server-close + option dontlognull + + log stdout format raw local0 + log-format "${HAP_LOG_FORMAT}" + + # X-Forward- settings + http-request set-header X-Forwarded-Proto http if !{ ssl_fc } + http-request set-header X-Forwarded-Proto https if { ssl_fc } + http-request set-header X-Forwarded-Host %[req.hdr(host)] if is_host_set + + # HSTS response header + http-response set-header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" if { ssl_fc } + + # Secure response cookies + http-response replace-header Set-Cookie ^((?:.(?!\ [Ss]ecure))*)$ \1;\ Secure if { ssl_fc } + + # # Backend routing + .if defined(HAP_DISABLE_CERTBOT) + .notice "Disabling certbot front-end route" + .else + use_backend be_acme_challenge if acme_challenge !is_stopping + .endif # HAP_DISABLE_CERTBOT + .if defined(HAP_DISABLE_PROMETHEUS) + .notice "Disabling prometheus front-end route" + .else + use_backend be_prometheus if get_prometheus !is_stopping + .endif + #use_backend be_well_known_sec if well_known_sec + + default_backend be_hockeypuck_rewrite diff --git a/haproxy.d/60_fe_hockeypuck_ddos_lb.cfg b/haproxy.d/60_fe_hockeypuck_ddos_lb.cfg new file mode 100644 index 0000000..af7a012 --- /dev/null +++ b/haproxy.d/60_fe_hockeypuck_ddos_lb.cfg @@ -0,0 +1,109 @@ +frontend fe_hockeypuck_ddos_lb + bind abns@hockeypuck-ddos accept-proxy + mode http + + # Check for valid vhost names + acl keyserver_host hdr_beg(host) -i "${FQDN}" + #acl keyserver_host hdr(host),lower,map_beg("${HAP_CONF_DIR}"/lists/aliases.map) -m found + + # hockeypuck settings + acl get_pks_lookup path_beg /pks/lookup + acl post_pks_add path_beg /pks/add + acl post_pks_delete path_beg /pks/delete + acl post_pks_hashquery path_beg /pks/hashquery + acl post_pks_replace path_beg /pks/replace + acl pks_op_get query -i -m sub op=get + acl pks_op_stats query -i -m sub op=stats + acl pks_options_fprint query -i -m sub fingerprint=on + acl pks_options_mr query -i -m sub options=mr + acl get_webroot path_beg / + + # Tor exit nodes + # These should be refreshed on a schedule using an external script. + # After updating, haproxy should be refreshed using the graceful method described at + # https://www.haproxy.com/blog/hitless-reloads-with-haproxy-howto/ + #acl is_tor_exit_relay hdr_ip(CF-Connecting-IP) -f "${HAP_CACHE_DIR}"/tor_exit_relays.list + + tcp-request inspect-delay 5s + tcp-request content accept if HTTP + + http-request capture hdr(Host) len 253 + http-request capture hdr_ip(CF-Connecting-IP) len 64 + + # ~ Tor exit relay nodes rules ~ + # Track request and connection rate for Tor exit nodes to POST /pks/add + # Use DNSBL to mark Tor exit relays + acl is_permitted_ip sc0_get_gpc0(st_known_tor_visitors) gt 0 + acl is_banned_ip sc0_get_gpc1(st_known_tor_visitors) gt 0 + + acl is_banned_user sc1_get_gpc0(st_tor_24days_ban) gt 0 + + http-request track-sc0 src table st_known_tor_visitors + http-request track-sc1 src table st_tor_24days_ban + + http-request lua.dnsbl_query "st_known_tor_visitors" "torexit.dan.me.uk" nil nil + http-request sc-inc-gpc0(1) if is_banned_ip !is_banned_user + http-request lua.dnsbl_block "st_tor_24days_ban" + + http-request track-sc2 str(TOR_EXIT_RELAY) table st_tor_request_rate if is_banned_ip keyserver_host post_pks_add METH_POST + + # Apply rate limiting for Tor exit nodes + acl tor_request_rate_limit sc2_inc_gpc0(st_tor_request_rate) gt 2 + acl tor_conn_count sc2_conn_cur(st_tor_request_rate) gt 1 + acl tor_conn_rate_limit sc2_conn_rate(st_tor_request_rate) gt 1 + + # Check if the IP is already banned + #acl tor_ip_24h_banned sc1_get_gpc0(st_tor_24h_ban) gt 0 + #acl tor_ip_1month_banned sc2_get_gpc0(st_tor_24days_ban) gt 0 + + # Increment the gpc0 counter in the 1-month ban stick table if the IP attempts another request + # within the 24h or 1-month ban period and block the request + #http-request sc-inc-gpc0(2) if tor_ip_1month_banned + #http-request deny deny_status 429 hdr Denial-Reason "This Tor exit relay is a repeat offender used in DDoS attack on this service. Hard ban enforced for 1 month." if tor_ip_1month_banned + + #http-request sc-inc-gpc0(1) if tor_ip_24h_banned !tor_ip_1month_banned + #http-request sc-inc-gpc0(2) if tor_ip_24h_banned !tor_ip_1month_banned + #http-request deny deny_status 429 hdr Denial-Reason "This Tor exit relay was used in DDoS attack on this service. Do not make any further requests. This will only make things worse." if tor_ip_24h_banned + + # Block requests if either the connection rate or request rate limits are exceeded + # Increment the gpc0 counter in the 24h ban stick table if the request is blocked + http-request set-var(txn.ratelimited) str(RATE-LIMITED) if is_banned_ip tor_request_rate_limit + http-request set-var(txn.ratelimited) str(RATE-LIMITED) if is_banned_ip tor_conn_rate_limit + http-request set-var(txn.ratelimited) str(RATE-LIMITED) if is_banned_ip tor_conn_count + http-request capture var(txn.ratelimited) len 12 if is_banned_ip tor_request_rate_limit + http-request capture var(txn.ratelimited) len 12 if is_banned_ip tor_conn_rate_limit + http-request capture var(txn.ratelimited) len 12 if is_banned_ip tor_conn_count + + # Check if the request was rate limited + acl tor_request_rate_limited var(txn.ratelimited) -m found + + # Block requests if the request was rate limited and increment the gpc0 counter in the 24h ban stick table + #http-request sc-inc-gpc0(1) if tor_request_rate_limited + http-request deny deny_status 429 hdr Denial-Reason "Exceeded rate limit. You had: %[sc0_conn_cur(st_tor_request_rate)] established connections, %[sc0_conn_rate(st_tor_request_rate)] connection rate and %[sc0_http_req_rate(st_tor_request_rate)] requests." if tor_request_rate_limited + # ~ End of Tor exit relay nodes rules ~ + + # Options + option httplog + option http-server-close + option dontlognull + + log stdout format raw local1 + log-format "${HAP_LOG_FORMAT}" + + # Fetch stats from the primary Hockeypuck instance + use_backend be_hockeypuck_primary if keyserver_host get_pks_lookup pks_op_stats METH_GET + # Fetch recon hash queries from the primary Hockeypuck instance + use_backend be_hockeypuck_primary if post_pks_hashquery METH_POST + # Use cluster backend for all other requests + use_backend be_hockeypuck if keyserver_host get_pks_lookup METH_GET + use_backend be_hockeypuck if keyserver_host post_pks_delete METH_POST + use_backend be_hockeypuck if keyserver_host post_pks_replace METH_POST + # Allow POST /pks/add requests from all IPs to the primary Hockeypuck instance + use_backend be_hockeypuck_primary if keyserver_host post_pks_add METH_POST # TODO: Can we add without Host set? + # Completely block POST /pks/add requests from Tor exit nodes + #use_backend be_hockeypuck_primary if keyserver_host post_pks_add !is_tor_exit_relay METH_POST + #use_backend be_tarpit if keyserver_host post_pks_add is_tor_exit_relay METH_POST + use_backend be_hockeypuck if keyserver_host get_webroot !post_pks_add !post_pks_hashquery !post_pks_delete !post_pks_replace METH_GET + + # Block all other requests + use_backend be_tarpit diff --git a/haproxy.d/70_fe_prometheus.cfg b/haproxy.d/70_fe_prometheus.cfg new file mode 100644 index 0000000..9f4b7ca --- /dev/null +++ b/haproxy.d/70_fe_prometheus.cfg @@ -0,0 +1,10 @@ +.if defined(HAP_DISABLE_PROMETHEUS) +.notice "Disabling prometheus front-end route" +.else +# Frontend to export stats to prometheus +frontend fe_prometheus + bind :8405 + mode http + http-request use-service prometheus-exporter + no log +.endif diff --git a/haproxy.d/80_be.cfg b/haproxy.d/80_be.cfg new file mode 100644 index 0000000..964b621 --- /dev/null +++ b/haproxy.d/80_be.cfg @@ -0,0 +1,75 @@ +# Backend to forward HTTP requests +backend be_forward_http + mode tcp + description Forward HTTP requests to the HTTP frontend + server srv_http abns@loopback-http send-proxy-v2 + +.if defined(HAP_DISABLE_SSL) +.notice "Disabling HTTPS loopback back-end forwarder" +.else + +# Backend to forward HTTPS requests +backend be_forward_https + mode tcp + description Forward HTTPS requests to the HTTP frontend + server srv_https abns@loopback-https send-proxy-v2 + +.endif # HAP_DISABLE_SSL + +.if defined(HAP_DISABLE_CERTBOT) +.notice "Disabling certbot back-end forwarder" +.else + +# Backend to serve ACME HTTP-01 challenge requests +backend be_acme_challenge + mode http + description certbot ACME HTTP-1 validation endpoint + server certbot "${CERTBOT_HOST_PORT}" maxconn 20 + +.endif # HAP_DISABLE_CERTBOT + +# Backend to tarpit connections +backend be_tarpit + mode http + description Tarpit invalid connections + + stick-table type ip size 1m expire 1m store conn_cur,conn_rate(10s),http_req_rate(10s) + + http-request silent-drop if { src_conn_cur gt 10 } + http-request silent-drop if { src_conn_cur gt 5 } { src_http_req_rate gt 10 } + + http-request tarpit + +.if defined(HAP_DISABLE_PROMETHEUS) +.notice "Disabling prometheus backend" +.else +# Backend to prometheus web interface +backend be_prometheus + mode http + + # Set the Via header + http-response set-header Via "1.1 %[var(txn.serverName)] (Hockey stick)" if { var(txn.serverName) -m found } + + server prometheus "${PROMETHEUS_HOST_PORT}" maxconn 20 +.endif + +# Backend to apply request and response rewriting rules +backend be_hockeypuck_rewrite + mode http + + # Set the Via header + http-response set-header Via "1.1 %[var(txn.serverName)] (Hockey stick)" if { var(txn.serverName) -m found } + + # ~~ URL rewriting rules ~~ + http-request replace-path ^/stats([^\ ]*) /pks/lookup?op=stats\1 + http-request replace-path ^/s/(.*) /pks/lookup?op=index&options=mr&search=\1 + http-request replace-path ^/search/(.*) /pks/lookup?op=index&options=mr&search=\1 + http-request replace-path ^/g/(.*) /pks/lookup?op=get&search=\1 + http-request replace-path ^/get/(.*) /pks/lookup?op=get&search=\1 + http-request replace-path ^/d/(.*) /pks/lookup?op=get&options=mr&search=\1 + http-request replace-path ^/download/(.*) /pks/lookup?op=get&options=mr&search=\1 + # ~~ End of URL rewriting rules ~~ + + http-request set-header CF-Connecting-IP %[var(txn.clientIP)] if { var(txn.clientIP) -m found } + + server hockeypuck_lb_ddos abns@hockeypuck-ddos send-proxy-v2 diff --git a/haproxy.d/90_LOCAL_be_hockeypuck.cfg b/haproxy.d/90_LOCAL_be_hockeypuck.cfg new file mode 100644 index 0000000..4d10d11 --- /dev/null +++ b/haproxy.d/90_LOCAL_be_hockeypuck.cfg @@ -0,0 +1,32 @@ +#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# LOCAL site configuration file for load-balancing across multiple back ends +# This file is NOT overwritten on upgrade +#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +backend be_hockeypuck_primary + mode http + + option httpchk + http-check send meth GET uri /pks/lookup?op=stats hdr Host "${FQDN}" + http-check expect status 200 + http-check send-state + + server srv_hockeypuck "${KEYSERVER_HOST_PORT}" check inter 5s on-error mark-down rise 2 fall 3 + +backend be_hockeypuck + mode http + + option httpchk + http-check send meth GET uri /pks/lookup?op=stats hdr Host "${FQDN}" + http-check expect status 200 + http-check send-state + + # This entry should be the same as the primary above + server srv_keyserver "${KEYSERVER_HOST_PORT}" check inter 5s on-error mark-down rise 2 fall 3 + + #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # Uncomment and edit the below to load-balance across multiple stacks + #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + #server srv_keyserver_remote1 10.0.0.1:11371 backup check inter 5s on-error mark-down rise 2 fall 3 + #server srv_keyserver_remote2 10.0.0.2:11371 backup check inter 5s on-error mark-down rise 2 fall 3 diff --git a/haproxy.d/90_LOCAL_be_hockeypuck.cfg.tmpl b/haproxy.d/90_LOCAL_be_hockeypuck.cfg.tmpl new file mode 100644 index 0000000..4d10d11 --- /dev/null +++ b/haproxy.d/90_LOCAL_be_hockeypuck.cfg.tmpl @@ -0,0 +1,32 @@ +#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# LOCAL site configuration file for load-balancing across multiple back ends +# This file is NOT overwritten on upgrade +#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +backend be_hockeypuck_primary + mode http + + option httpchk + http-check send meth GET uri /pks/lookup?op=stats hdr Host "${FQDN}" + http-check expect status 200 + http-check send-state + + server srv_hockeypuck "${KEYSERVER_HOST_PORT}" check inter 5s on-error mark-down rise 2 fall 3 + +backend be_hockeypuck + mode http + + option httpchk + http-check send meth GET uri /pks/lookup?op=stats hdr Host "${FQDN}" + http-check expect status 200 + http-check send-state + + # This entry should be the same as the primary above + server srv_keyserver "${KEYSERVER_HOST_PORT}" check inter 5s on-error mark-down rise 2 fall 3 + + #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # Uncomment and edit the below to load-balance across multiple stacks + #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + #server srv_keyserver_remote1 10.0.0.1:11371 backup check inter 5s on-error mark-down rise 2 fall 3 + #server srv_keyserver_remote2 10.0.0.2:11371 backup check inter 5s on-error mark-down rise 2 fall 3 diff --git a/haproxy.d/README.md b/haproxy.d/README.md new file mode 100644 index 0000000..a6dbf81 --- /dev/null +++ b/haproxy.d/README.md @@ -0,0 +1,30 @@ +# Sample haproxy configuration for keyservers +(c) Martin Dobrev, Andrew Gallagher 2023 + +Supply this directory to `haproxy` using the `-f DIRECTORY` command-line option. +The files in this directory are optimised for hockeypuck's `docker-compose/standalone` deployment. + +You MUST copy the two `*LOCAL*.cfg.tmpl` files to the corresponding `*LOCAL*.cfg` file before use. +Hockeypuck's `contrib/docker-compose/standalone/mkconfig.bash` script will do this automatically. +(see `contrib/docker-compose/standalone/README.md` for full instructions) +This allows you to edit the `*LOCAL*.cfg` files without introducing git merge conflicts. + +To facilitate portability, these files have been parameterised using envar substitution, i.e. "${...}" +You can populate these values at runtime by setting the corresponding environment variables: + +* FQDN the FQDN of this server, note that aliases must also be configured +* CERTBOT_HOST_PORT backend for ACME requests, in the form `host:port` +* PROMETHEUS_HOST_PORT backend for prometheus monitoring, in the form `host:port` +* KEYSERVER_HOST_PORT backend for the keyserver, in the form `host:port` +* HAP_CONF_DIR location of config files + normally `/etc/haproxy` for baremetal, `/usr/local/etc/haproxy` for docker + it must have a subdir `lists` containing `blacklist.list` and `whitelist.list` (can be empty files) +* HAP_CACHE_DIR persistent state store, must contain `tor_exit_relays.list` (refreshed externally) +* HAP_CERT_DIR parent directory of SSL/TLS certificate directory + it must contain a subdirectory named after the FQDN, itself containing `fullchain.pem` and `fullchain.pem.key` + e.g. for letsencrypt this will be `/etc/letsencrypt/live` +* HAP_DHPARAM_FILE Diffie-Hellman parameters for SSL/TLS + +These envars are normally supplied by `contrib/docker-compose/standalone/docker-compose.yml`. + +Note that after the cache files or the SSL certs are updated externally, haproxy should be soft reloaded by sending it a HUP signal. diff --git a/lists/blacklist.list b/lists/blacklist.list new file mode 100644 index 0000000..e69de29 diff --git a/lists/whitelist.list b/lists/whitelist.list new file mode 100644 index 0000000..e69de29