# Generated by edgeguard-api — DO NOT EDIT. # Source: internal/haproxy/haproxy.go (template: haproxy.cfg.tpl). # Re-generate via `edgeguard-ctl render-config`. global log /dev/log local0 info log /dev/log local1 notice user haproxy group haproxy daemon ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 # HAProxy 3.0.x in Trixie ist mit USE_QUIC=1 + USE_QUIC_OPENSSL_COMPAT=1 # gebaut; die System-OpenSSL kann QUIC aber nur über den Kompat-Layer. # Ohne dieses Flag verweigert HAProxy `bind quic4@…` mit "this SSL # library does not support the QUIC protocol". limited-quic # Runtime-API-Socket für edgeguard-api (show stat, show info, …). # mode 0660 + group haproxy → edgeguard-user (in haproxy-group) kann lesen. stats socket /run/haproxy/admin.sock mode 0660 level admin expose-fd listeners defaults log global mode http option httplog option dontlognull option forwardfor timeout connect 5s timeout client 60s timeout server 60s timeout http-request 10s # timeout tunnel wird PER BACKEND aktiviert (websocket-Flag), # damit nur WS-Workloads die lange Idle-Toleranz haben und normale # HTTP-Backends saubere Connection-Hygiene behalten. # ── Public :80 ───────────────────────────────────────────────────────── # ACME-01 challenges proxy to edgeguard-api which serves the webroot. # Domains with http_to_https=false bypass the redirect and are proxied # directly. Everything else redirects to HTTPS. frontend public_http bind :80 {{- if .IPv6Enabled}} bind [::]:80 {{- end}} acl is_acme path_beg /.well-known/acme-challenge/ {{- range $d := .HTTPDomains}} acl is_http_only hdr(host) -i {{$d.Name}} {{- end}} # Redirect to HTTPS (skipped for ACME and http-only domains). http-request redirect scheme https code 301 unless is_acme{{if .HTTPDomains}} or is_http_only{{end}} {{- range $d := .HTTPDomains}} {{- range $r := $d.Routes}} use_backend eg_backend_{{$r.BackendID}} if { hdr(host) -i {{$d.Name}} } { path_beg {{$r.PathPrefix}} } {{- end}} {{- if $d.PrimaryBackendID}} use_backend eg_backend_{{$d.PrimaryBackendID}} if { hdr(host) -i {{$d.Name}} } {{- end}} {{- end}} use_backend api_backend if is_acme # ── Public :443 (Customer-Backends only) ────────────────────────────── # TLS termination. Reads certs from /etc/edgeguard/tls/ — postinst # seeds a self-signed _default.pem so HAProxy starts before certbot # has issued anything. # # WICHTIG: kein default_backend → unbekannte Hosts kriegen 503. Die # Management-UI sitzt bewusst auf :3443 (siehe mgmt_https unten), # damit ein versehentlich offengelassenes Wildcard-DNS nie auf das # Admin-Panel fällt. mailgateway/enconf-Pattern. frontend public_https bind :443 ssl crt /etc/edgeguard/tls/ alpn h2,http/1.1 # HTTP/3 via QUIC (UDP/443). HAProxy 3.0.x ist mit USE_QUIC=1 # gebaut; Browser fallen via Alt-Svc-Header (siehe unten) für # Folge-Requests auf h3 zurück. bind quic4@:443 ssl crt /etc/edgeguard/tls/ alpn h3 {{- if .IPv6Enabled}} bind [::]:443 ssl crt /etc/edgeguard/tls/ alpn h2,http/1.1 bind quic6@:443 ssl crt /etc/edgeguard/tls/ alpn h3 {{- end}} {{- if .WAFEnabled}} # WAF: SPOE-Filter — edgeguard-waf inspiziert jeden Request. # filter muss vor allen http-request/http-response-Direktiven stehen. filter spoe engine edgeguard-waf config /etc/edgeguard/haproxy/coraza-spoe.cfg {{- end}} # Alt-Svc: signalisiert dass h3 auf demselben Port verfügbar ist. # ma=86400 = Browser darf den Hinweis 24h cachen. http-response set-header Alt-Svc "h3=\":443\"; ma=86400" # Client-IP-Weiterleitung an Backends. `option forwardfor` (defaults) # setzt X-Forwarded-For; wir ergänzen Proto + RealIP damit Apps # erkennen können (a) dass der Client HTTPS sprach und (b) die # echte Source-IP ohne XFF-Chain-Parsing brauchen. http-request set-header X-Forwarded-Proto https http-request set-header X-Real-IP %[src] {{- if .WAFEnabled}} # WAF: Request blockieren wenn edgeguard-waf txn.waf.status gesetzt hat. http-request deny deny_status 403 if { var(txn.waf.status) -m found } {{- end}} {{- if .GlobalMaintenance}} # Whole-Box-Maintenance — Settings → Maintenance-Mode aktiv. Dieser # Block kommt VOR allen per-Domain ACLs und blockt JEDEN Customer- # Request. Mgmt-UI auf :3443 (mgmt_https) ist davon nicht betroffen. http-request return status 503 content-type "text/plain; charset=utf-8" string "{{.GlobalMaintenanceMessage}}" {{- end}} {{- range $d := .Domains}} {{- if $d.RedirectFromHost}} # www-Redirect: {{$d.RedirectFromHost}} → {{$d.Name}} http-request redirect prefix https://{{$d.Name}} code 301 if { hdr(host) -i {{$d.RedirectFromHost}} } {{- end}} {{- if $d.MaintenanceMode}} # Wartungs-Modus für {{$d.Name}} — alle Requests werden mit 503 beantwortet. http-request return status 503 content-type "text/plain; charset=utf-8" string "{{$d.MaintMessage}}" if { hdr(host) -i {{$d.Name}} } {{- end}} {{- if $d.MaxBodyBytes}} # Body-Size-Cap für {{$d.Name}}: {{$d.MaxBodyBytes}} Bytes. # Nur Content-Length wird geprüft — Chunked-Bodies wären erst nach # http-buffer-request abgreifbar, das wollen wir auf Frontend-Level # nicht aktivieren (verbraucht RAM pro Connection). http-request deny deny_status 413 if { hdr(host) -i {{$d.Name}} } { req.hdr_val(content-length) -m int gt {{$d.MaxBodyBytes}} } {{- end}} {{- if $d.RateLimitThreshold}} # Rate-Limit für {{$d.Name}}: {{$d.RateLimitRPS}} req/s pro Client-IP # (Schwelle = rps × 10s-Fenster = {{$d.RateLimitThreshold}} hits). http-request track-sc0 src table rl_{{$d.ID}} if { hdr(host) -i {{$d.Name}} } http-request deny deny_status 429 if { hdr(host) -i {{$d.Name}} } { sc_http_req_rate(0) gt {{$d.RateLimitThreshold}} } {{- end}} {{- if $d.HSTSHeader}} # del + set damit ein vom Backend gesetztes HSTS (z. B. nginx mit # eigener add_header-Direktive) garantiert von unserer Policy # überschrieben wird — sonst tauchen 2 Strict-Transport-Security # Header in der Response auf. http-response del-header Strict-Transport-Security if { hdr(host) -i {{$d.Name}} } http-response set-header Strict-Transport-Security "{{$d.HSTSHeader}}" if { hdr(host) -i {{$d.Name}} } {{- end}} {{- if $d.DisableH3}} # HTTP/3 (QUIC) Werbung für diese Domain unterdrücken: Alt-Svc-Header # entfernen damit Browser nicht auf h3 upgraden. Der globale set-header # oben hat ihn bereits gesetzt; dieses del-header kommt DANACH und # überschreibt ihn für Requests an diese Domain. http-response del-header Alt-Svc if { hdr(host) -i {{$d.Name}} } {{- end}} {{- range $h := $d.ResponseHeaders}} http-response del-header {{$h.Name}} if { hdr(host) -i {{$d.Name}} } http-response set-header {{$h.Name}} "{{$h.Value}}" if { hdr(host) -i {{$d.Name}} } {{- end}} {{- range $r := $d.Routes}} use_backend eg_backend_{{$r.BackendID}} if { hdr(host) -i {{$d.Name}} } { path_beg {{$r.PathPrefix}} } {{- end}} {{- if $d.PrimaryBackendID}} use_backend eg_backend_{{$d.PrimaryBackendID}} if { hdr(host) -i {{$d.Name}} } {{- end}} {{- end}} # ── Mgmt :3443 (Admin-UI only) ──────────────────────────────────────── # Eigener Port für die Management-UI — gleicher Cert-Pool, aber kein # Customer-Routing. Anti-Lockout-Regel im nft-Template lässt 3443 # immer durch. Erreichbar über jede Domain die auf die Box zeigt # (Hostname egal — default_backend), inkl. der direkten IP. frontend mgmt_https bind :3443 ssl crt /etc/edgeguard/tls/ alpn h2,http/1.1 {{- if .IPv6Enabled}} bind [::]:3443 ssl crt /etc/edgeguard/tls/ alpn h2,http/1.1 {{- end}} http-response set-header Strict-Transport-Security "max-age=31536000" http-request set-header X-Forwarded-Proto https http-request set-header X-Real-IP %[src] default_backend api_backend # ── Internal stats ───────────────────────────────────────────────────── frontend internal_stats bind 127.0.0.1:8404 stats enable stats uri /stats stats refresh 10s stats admin if { src 127.0.0.1 } # ── Backends ─────────────────────────────────────────────────────────── # edgeguard-api itself: management UI, REST API, ACME webroot. # timeout tunnel 1h: /api/v1/firewall/log/live ist ein langlebiger # WebSocket-Stream — ohne Tunnel-Override fällt er nach `timeout # server 60s` ohne Events um. backend api_backend timeout tunnel 1h server api1 127.0.0.1:9443 check {{- /* Per-Domain Rate-Limit Stick-Tables. Eigenes Backend pro Domain damit verschiedene Schwellen nicht miteinander interferieren; HAProxy 3.0 erlaubt mehrere stick-table-Backends die per Frontend mit track-sc0 angesteuert werden. expire 10s hält den Speicher knapp. */ -}} {{- range $d := .Domains}} {{- if $d.RateLimitThreshold}} backend rl_{{$d.ID}} # Rate-Limit-Counter für {{$d.Name}}; gefüllt von public_https. stick-table type ip size 100k expire 10s store http_req_rate(10s) {{- end}} {{- end}} {{- if .WAFEnabled}} # SPOE-Backend für edgeguard-waf (TCP, kein HTTP-Parsing). backend spoe-edgeguard-waf mode tcp timeout connect 100ms timeout server 1s server spoe-waf-1 127.0.0.1:9000 {{- end}} {{- range $b := .Backends}} backend eg_backend_{{$b.ID}} balance {{$b.LBAlgorithm}} {{- if $b.WebSocket}} timeout tunnel 1h {{- end}} {{- if $b.HealthCheckPath}} option httpchk http-check send meth GET uri {{$b.HealthCheckPath}} {{- end}} {{- range $s := $b.Servers}} server {{$s.Name | safeID}} {{$s.Address}}:{{$s.Port}}{{if eq $b.Scheme "https"}} ssl verify none alpn {{if $b.ForceHTTP1}}http/1.1{{else}}h2,http/1.1{{end}}{{end}}{{if $b.HealthCheckPath}} check inter 5s{{if eq $b.Scheme "https"}} check-alpn http/1.1{{end}}{{end}} weight {{$s.Weight}}{{if $s.Backup}} backup{{end}} {{- end}} {{- end}}