Skip to content

feat(backend): per-policy DDoS rate limiting and connection throttling#274

Merged
bihius merged 2 commits into
mainfrom
feat/ddos-rate-limiting
Jul 21, 2026
Merged

feat(backend): per-policy DDoS rate limiting and connection throttling#274
bihius merged 2 commits into
mainfrom
feat/ddos-rate-limiting

Conversation

@bihius

@bihius bihius commented Jul 21, 2026

Copy link
Copy Markdown
Owner

Summary

Implements the MVP core of #176 (DDoS protection): per-source-IP request-rate limiting, connection throttling, and slow-loris timeout hardening, configurable per WAF policy.

  • Added ddos_protection_enabled, rate_limit_requests, rate_limit_window_seconds, max_connections_per_ip columns to Policy (Alembic migration included).
  • Exposed the fields through the existing policy create/update API and admin form (toggle + numeric inputs, disabled when protection is off).
  • Config generator now threads each vhost's policy into a HaproxyDdos render context; the HAProxy template emits a dedicated per-vhost stick-table (http_req_rate + conn_cur) and 429 denies when enabled, and nothing at all when disabled (byte-identical config for existing policies).
  • Added timeout http-request 5s globally for slow-loris protection.

Scope

This PR intentionally covers only the config-only core. The remaining scope of #176 is split into follow-up issues so this stays reviewable:

Related to #176

Test plan

  • uv run pytest --cov=app — 547 passed (added unit tests for HaproxyDdos validation/rendering, config generator DDoS wiring, policy service, and integration tests for the policy API)
  • uv run mypy app/ — clean
  • uv run ruff check app/ — clean
  • pnpm run type-check — clean
  • pnpm run lint — clean
  • pnpm test — 111 passed (new PolicyFormModal tests for the DDoS fields)
  • haproxy -c validates the generated config both with and without DDoS protection enabled (asserted in tests)

…tling

Add DDoS protection knobs (rate limit, window, max connections per IP) to
the Policy model, exposed through the existing policy API and admin form.
The HAProxy config generator emits a per-vhost stick-table with
http_req_rate/conn_cur tracking and 429 denies when a policy has DDoS
protection enabled, plus a global slow-loris timeout hardening default.

Related to #176
Copilot AI review requested due to automatic review settings July 21, 2026 19:51

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds per-policy DDoS mitigation controls (request-rate limiting + per-IP concurrent connection throttling) and threads them through the backend policy model/API, HAProxy config generation/template rendering, and the admin UI/tests.

Changes:

  • Extend Policy with DDoS-related fields (enabled + thresholds), including DB constraints + Alembic migration, and expose them via create/update endpoints and response schema.
  • Introduce HaproxyDdos render context and emit per-vhost HAProxy stick-tables + 429 deny rules when enabled; add timeout http-request 5s for slow-loris hardening.
  • Update frontend policy form + types and add/adjust unit/integration/UI tests to cover the new fields.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/frontend/src/pages/vhosts/VHostDetailPage.test.tsx Extend mocked vhost policy data with new DDoS fields.
src/frontend/src/pages/policies/PolicyDetailPage.test.tsx Extend mocked policy detail data with new DDoS fields.
src/frontend/src/pages/policies/PoliciesPage.test.tsx Extend mocked policy list data with new DDoS fields.
src/frontend/src/features/vhosts/types.ts Add DDoS fields to the assigned-policy type used by vhosts.
src/frontend/src/features/policies/types.ts Add DDoS fields to policy types (read/create/update).
src/frontend/src/features/policies/PolicyFormModal.tsx Add UI controls/state and submit wiring for DDoS settings.
src/frontend/src/features/policies/PolicyFormModal.test.tsx Add UI tests for DDoS toggle + field enablement + API payload.
src/backend/tests/unit/test_policy_service.py Add unit tests for persisting/updating DDoS policy fields.
src/backend/tests/unit/test_config_renderer_haproxy.py Add unit tests asserting HAProxy template output with/without DDoS blocks and validation.
src/backend/tests/unit/test_config_generator.py Add generator tests for DDoS wiring and haproxy -c validation with DDoS enabled.
src/backend/tests/integration/test_policies_router.py Add integration tests covering defaulting/round-tripping/validation for DDoS policy fields.
src/backend/app/templates/haproxy.cfg.j2 Emit per-vhost stick-table tracking + 429 denies and add timeout http-request 5s.
src/backend/app/services/policy_service.py Allow create/patch for new DDoS fields via service layer.
src/backend/app/services/config_renderer.py Add HaproxyDdos dataclass + uniqueness validation for ddos table names.
src/backend/app/services/config_generator.py Thread vhost policy into HAProxy render context and build HaproxyDdos when enabled.
src/backend/app/schemas/policy.py Add DDoS fields + validation constraints to create/update/response schemas.
src/backend/app/routers/policies.py Pass DDoS fields from request schema into policy creation.
src/backend/app/models/policy.py Add DB columns + check constraints for DDoS policy settings.
src/backend/alembic/versions/473d83df7b55_add_ddos_protection_fields_to_policies.py Migration adding DDoS columns and constraints.
configs/haproxy/haproxy.cfg Update checked-in HAProxy config to include timeout http-request 5s.

Comment on lines +61 to +63
http-request track-sc0 src table {{ route.ddos.stick_table_name }} if {{ route.vhost_acl_name }}
http-request deny deny_status 429 if {{ route.vhost_acl_name }} { sc_http_req_rate(0,{{ route.ddos.stick_table_name }}) gt {{ route.ddos.rate_limit_requests }} }
http-request deny deny_status 429 if {{ route.vhost_acl_name }} { sc_conn_cur(0,{{ route.ddos.stick_table_name }}) gt {{ route.ddos.max_connections_per_ip }} }

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked against the HAProxy documentation for http_req_rate(<period>): it returns the count of events observed during the trailing <period>, not a per-second rate (this is the standard stick-table rate-limiting pattern — e.g. store http_req_rate(10s) + gt 100 denies a client making more than 100 requests in the last 10 seconds). So rate_limit_requests is already "requests per rate_limit_window_seconds" as intended, and a 100/10s policy behaves like ~10 req/s sustained, not ~100 req/s. No behavior change needed here; added a clarifying comment in the template to prevent future confusion (5800f42).

Comment on lines +61 to +63
http-request track-sc0 src table {{ route.ddos.stick_table_name }} if {{ route.vhost_acl_name }}
http-request deny deny_status 429 if {{ route.vhost_acl_name }} { sc_http_req_rate(0,{{ route.ddos.stick_table_name }}) gt {{ route.ddos.rate_limit_requests }} }
http-request deny deny_status 429 if {{ route.vhost_acl_name }} { sc_conn_cur(0,{{ route.ddos.stick_table_name }}) gt {{ route.ddos.max_connections_per_ip }} }

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Fixed by adding !is_health !is_acme to both the track-sc0 and 429-deny conditions in fe_http and fe_https, so health checks and ACME HTTP-01 renewals are never throttled or denied by the DDoS stick-table. Updated the corresponding renderer/generator unit tests. (5800f42)

Comment on lines +145 to +150
"""Per-vhost DDoS protection settings (rate limiting + connection throttling).

All fields are validated on construction; ``stick_table_name`` is derived
from the vhost's existing ACL suffix so it is guaranteed to be a safe and
unique HAProxy identifier.
"""

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reworded — the docstring no longer claims uniqueness/derivation is enforced by this class; it now points to the config generator and HaproxyRenderContext's uniqueness check as the actual owners of that invariant. (5800f42)

Health checks and Let's Encrypt HTTP-01 challenges could previously be
starved by the same stick-table limits applied to regular traffic on a
vhost. Exclude is_health and is_acme from DDoS tracking and denies, and
clarify the HaproxyDdos docstring about which layer owns table-name
uniqueness.
@bihius
bihius merged commit 134e86c into main Jul 21, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants