diff --git a/docs/plans/2026-06-08-matrix-synapse-to-tuwunel-plan.md b/docs/plans/2026-06-08-matrix-synapse-to-tuwunel-plan.md index 2eb894ee..0789fb44 100644 --- a/docs/plans/2026-06-08-matrix-synapse-to-tuwunel-plan.md +++ b/docs/plans/2026-06-08-matrix-synapse-to-tuwunel-plan.md @@ -57,3 +57,36 @@ RocksDB dir. predates this migration and was **not** applied. Resolve separately. - **Synapse leftover files** remain on the encrypted PVC volume root (unused by tuwunel). Can be `rm`'d after confidence in the new server. + +## Follow-up: open registration + bot mitigations (2026-06-08, user-chosen) + +Registration was opened **fully (tokenless)** — `TUWUNEL_ALLOW_REGISTRATION=true` ++ `TUWUNEL_YES_I_AM_VERY_VERY_SURE_I_WANT_AN_OPEN_REGISTRATION_SERVER_PRONE_TO_ABUSE=true`, +dropped the `TUWUNEL_REGISTRATION_TOKEN` env (the Vault `secret/matrix` token + +`matrix-secrets` ESO are kept for one-env-change revert to token-gated). tuwunel +has **no CAPTCHA** (only Synapse does) and a browser challenge would break native +clients, so bot defense is layered instead: + +- **Traefik rate-limit on `/register`** — a `register-ratelimit` Middleware + (`stacks/matrix`) on a path-scoped `ingress_register` carve-out (longer prefix + wins over the catch-all). Keyed on the **request Host (global `/register` cap), + not source IP** — because the host is reachable both via Cloudflare-IPv4 + (`CF-Connecting-IP`) and **IPv6-direct (HE tunnel → pfSense HAProxy → Traefik, + no CF header)**; a per-source key let IPv6 bots bypass entirely (found during + testing). 10/min, burst 20, **per Traefik replica (×3)**. +- **CrowdSec** (already on the ingress chain) is the hard backstop — bans abusive + IPs on both paths; covers the per-replica looseness of the soft rate-limit. +- **Notification:** Loki ruler rule `MatrixNewUserRegistered` (`stacks/monitoring`, + matches `... registered on this server`, never the rejection line) → `lane=security` + → existing `#security` Slack receiver. Also note tuwunel's admin bot + (`@conduit:matrix.viktorbarzin.me`) **natively posts every registration to the + server admin room**, so there's an in-Matrix notice too. +- **Verification:** open signup returns 200 (`@regtest1`, since deactivated via + `!admin users deactivate` in the admin room); Traefik access logs confirm + `/register` routes through the rate-limited carve-out router. A live 429 was not + force-tested (per-replica burst ~60 across 3 replicas; avoided hammering so as + not to trip CrowdSec on the test source IP). + +**Add a user:** anyone can self-register now. To provision manually instead: +`!admin users create-user ` in the admin room (first user `@viktor` is admin). +**Revert to token-gated:** drop the YES_I_AM... flag, re-add `TUWUNEL_REGISTRATION_TOKEN`. diff --git a/stacks/matrix/main.tf b/stacks/matrix/main.tf index b3c545c7..97b04057 100644 --- a/stacks/matrix/main.tf +++ b/stacks/matrix/main.tf @@ -156,20 +156,19 @@ resource "kubernetes_deployment" "matrix" { name = "TUWUNEL_TRUSTED_SERVERS" value = jsonencode(["matrix.org"]) } - # Registration disabled. To add a user later: set "true", apply, - # register with the Vault token (secret/matrix), then set back to "false". + # Registration OPEN (tokenless) — user-chosen 2026-06-08. tuwunel demands + # this explicit flag for tokenless open registration. Bot mitigations: + # the Traefik rate-limit on /register (register_ratelimit + ingress_register + # below) + CrowdSec + a Loki->#security alert on every signup (monitoring + # stack). To revert to token-gated: drop the YES_I_AM_VERY... flag and + # re-add the TUWUNEL_REGISTRATION_TOKEN env (secret/matrix still holds it). env { name = "TUWUNEL_ALLOW_REGISTRATION" - value = "false" + value = "true" } env { - name = "TUWUNEL_REGISTRATION_TOKEN" - value_from { - secret_key_ref { - name = "matrix-secrets" - key = "registration_token" - } - } + name = "TUWUNEL_YES_I_AM_VERY_VERY_SURE_I_WANT_AN_OPEN_REGISTRATION_SERVER_PRONE_TO_ABUSE" + value = "true" } # 50 MiB — kept under Cloudflare's 100 MB proxied-request ceiling. env { @@ -275,3 +274,54 @@ module "ingress" { "gethomepage.dev/pod-selector" = "" } } + +# Open registration is bot-exposed, so rate-limit the register endpoint. Keyed on +# the request HOST (a GLOBAL /register cap), NOT the source IP — this host is +# reachable BOTH via Cloudflare-IPv4 (CF-Connecting-IP header) AND IPv6-direct (HE +# tunnel → pfSense HAProxy → Traefik, no CF header), so a per-source-IP key would +# let IPv6 bots bypass entirely. 10/min sustained, burst 20, PER Traefik replica +# (×3 ≈ 30/min, burst ~60 effective). Legit signups are rare so this only bites +# mass-signup; CrowdSec is the hard backstop (bans abusive IPs on both paths). A +# Loki rule (stacks/monitoring) alerts #security on each successful signup. +resource "kubernetes_manifest" "register_ratelimit" { + manifest = { + apiVersion = "traefik.io/v1alpha1" + kind = "Middleware" + metadata = { + name = "register-ratelimit" + namespace = kubernetes_namespace.matrix.metadata[0].name + } + spec = { + rateLimit = { + average = 10 + period = "1m" + burst = 20 + sourceCriterion = { + requestHost = true + } + } + } + } +} + +# Path-scoped ingress for the register endpoints only (longer prefix → Traefik +# matches it ahead of the catch-all matrix ingress) with the rate-limit middleware +# attached. Same auth="none" as the main ingress (Matrix register is a UIA/bearer +# API, not a browser session); anti-AI off (API endpoint, native clients). +module "ingress_register" { + source = "../../modules/kubernetes/ingress_factory" + # auth = "none": Matrix client registration API (UIA), not a browser session. + auth = "none" + anti_ai_scraping = false + dns_type = "none" # main module.ingress owns the DNS record for this host + namespace = kubernetes_namespace.matrix.metadata[0].name + name = "matrix-register" + service_name = "matrix" + full_host = "matrix.viktorbarzin.me" + ingress_path = ["/_matrix/client/v3/register", "/_matrix/client/r0/register"] + port = 80 + tls_secret_name = var.tls_secret_name + homepage_enabled = false # path carve-out, not its own dashboard tile + + extra_middlewares = ["matrix-register-ratelimit@kubernetescrd"] +} diff --git a/stacks/monitoring/modules/monitoring/loki.tf b/stacks/monitoring/modules/monitoring/loki.tf index 78f779e5..bf333463 100644 --- a/stacks/monitoring/modules/monitoring/loki.tf +++ b/stacks/monitoring/modules/monitoring/loki.tf @@ -385,6 +385,26 @@ resource "kubernetes_config_map" "loki_alert_rules" { } }, ] + }, + { + # Matrix (tuwunel) — open registration is ON, so notify on every new + # signup. tuwunel logs `... New user "@x:..." registered on this server` + # only on SUCCESS (the disabled-path logs "Rejecting ... registration is + # disabled"), so this matcher never false-fires on rejected attempts. + # lane=security routes it to the existing #security Slack receiver. + name = "Matrix" + rules = [ + { + alert = "MatrixNewUserRegistered" + expr = "sum(count_over_time({namespace=\"matrix\",container=\"matrix\"} |= \"registered on this server\" [10m])) > 0" + for = "0m" + labels = { severity = "info", lane = "security" } + annotations = { + summary = "New user registered on Matrix (tuwunel) — open registration is ON" + description = "A new account was created on matrix.viktorbarzin.me. See who with: kubectl -n matrix logs deploy/matrix | grep 'New user'. If unexpected/abuse, revert to token-gated registration in stacks/matrix." + } + }, + ] } ] })