From d70330f3942003e333cbb35e6ae2e955935d19b3 Mon Sep 17 00:00:00 2001 From: Jiang Bohan Date: Mon, 18 May 2026 13:03:58 +0800 Subject: [PATCH] docs(self-host): document auth rate-limit env keys Adds REDIS_URL, RATE_LIMIT_AUTH, RATE_LIMIT_AUTH_VERIFY, and RATE_LIMIT_TRUSTED_PROXIES to the environment-variables page (EN + ZH) and to .env.example, with the reverse-proxy caveat that without RATE_LIMIT_TRUSTED_PROXIES every user shares the proxy IP and the whole deployment ends up in one bucket. Follow-up to #2636. MUL-2251. Co-authored-by: multica-agent --- .env.example | 22 +++++++++++++++++++ .../content/docs/environment-variables.mdx | 19 ++++++++++++++++ .../content/docs/environment-variables.zh.mdx | 19 ++++++++++++++++ 3 files changed, 60 insertions(+) diff --git a/.env.example b/.env.example index fb361c4f8..d3da90d0a 100644 --- a/.env.example +++ b/.env.example @@ -122,6 +122,28 @@ LOCAL_UPLOAD_BASE_URL=http://localhost:8080 # Example: CORS_ALLOWED_ORIGINS=https://app.multica.ai,https://staging.multica.ai CORS_ALLOWED_ORIGINS= +# ==================== Rate limiting (optional Redis) ==================== +# Per-IP fixed-window rate limiter on the public auth endpoints +# (/auth/send-code, /auth/verify-code, /auth/google). Backed by Redis. +# When REDIS_URL is unset the limiter is a no-op (fail-open) and the +# backend logs "rate limiting disabled: REDIS_URL not configured" at +# startup. The same REDIS_URL is reused by the realtime fan-out hub, +# the PAT cache, and the daemon-token cache. +# REDIS_URL=redis://localhost:6379/0 +# Max requests per IP per minute. Defaults are 5 for send-code/google +# and 20 for verify-code. +# RATE_LIMIT_AUTH=5 +# RATE_LIMIT_AUTH_VERIFY=20 +# Comma-separated CIDRs whose X-Forwarded-For the auth limiter is +# allowed to trust. Empty (default) = never trust XFF, only RemoteAddr. +# REQUIRED behind a reverse proxy — otherwise every real user shares +# the proxy IP and the whole deployment lands in one bucket, turning +# /auth/send-code into 5 req/min site-wide. Use e.g. "127.0.0.1/32,::1/128" +# for same-host Caddy/Nginx, or the CDN's published ranges for ALB/CF. +# This is a separate list from MULTICA_TRUSTED_PROXIES above (which +# governs the autopilot webhook limiter). +# RATE_LIMIT_TRUSTED_PROXIES= + # Realtime metrics endpoint (/health/realtime) access control. See MUL-1342. # When unset, the endpoint only serves direct loopback (127.0.0.1 / ::1) # callers with no forwarding headers and returns 404 to everything else — diff --git a/apps/docs/content/docs/environment-variables.mdx b/apps/docs/content/docs/environment-variables.mdx index 83eb02605..c3b4f5d4c 100644 --- a/apps/docs/content/docs/environment-variables.mdx +++ b/apps/docs/content/docs/environment-variables.mdx @@ -128,6 +128,25 @@ Three allowlist layers combine by priority. **If any layer is set to a non-empty **Invite flows themselves do not check the signup allowlist** — but the invitee must still be able to **sign in** before accepting the invite. If they already have a Multica account (for example from another workspace), they can accept directly, unaffected by the allowlist; **if they have never signed up**, the first step of sign-in (requesting a verification code) still passes through the allowlist check, and an email rejected by `ALLOW_SIGNUP=false` or by `ALLOWED_EMAILS` / `ALLOWED_EMAIL_DOMAINS` **cannot finish signup, and therefore cannot accept the invite**. +## Rate limiting (optional Redis) + +Public auth endpoints — `/auth/send-code`, `/auth/verify-code`, `/auth/google` — have per-IP fixed-window rate limiting in front of them. The limiter is backed by Redis. When `REDIS_URL` is unset the middleware is a **no-op** (fail-open) and the backend logs `rate limiting disabled: REDIS_URL not configured` at startup. + +| Variable | Default | Description | +|---|---|---| +| `REDIS_URL` | empty | Redis connection URL (for example `redis://localhost:6379/0`). When unset, rate limiting on auth endpoints is disabled. The same Redis is also used by the realtime hub fan-out, the PAT cache, and the daemon-token cache — they all fall back to in-memory / direct-DB mode when unset | +| `RATE_LIMIT_AUTH` | `5` | Max requests per IP per minute against `/auth/send-code` and `/auth/google` | +| `RATE_LIMIT_AUTH_VERIFY` | `20` | Max requests per IP per minute against `/auth/verify-code` | +| `RATE_LIMIT_TRUSTED_PROXIES` | empty | Comma-separated CIDRs whose `X-Forwarded-For` header the limiter is allowed to trust. Empty (the default) means **never trust XFF** — the limiter only uses the direct connection's `RemoteAddr` | + +When a request is over the limit, the server replies with `429 Too Many Requests`, `Retry-After: 60`, and body `{"error":"too many requests"}`. + + +**Behind a reverse proxy you must set `RATE_LIMIT_TRUSTED_PROXIES`.** Otherwise every real user shares the proxy's IP from the backend's point of view, the whole deployment ends up in one bucket, and `/auth/send-code` becomes 5 req/min for the entire site. Typical values: `127.0.0.1/32,::1/128` for a same-host Caddy / Nginx; the CDN's published ranges for Cloudflare / ALB / CloudFront. Only IPs whose `RemoteAddr` falls inside one of these CIDRs may use `X-Forwarded-For` to identify the client. + + +This separate `RATE_LIMIT_TRUSTED_PROXIES` is **not** the same as `MULTICA_TRUSTED_PROXIES`, which controls the autopilot-webhook limiter (`/api/webhooks/autopilots/{token}`). Each limiter parses its own list, so a deployment behind a proxy should set both. + ## Daemon tuning parameters The daemon runs on the user's local machine, and its config is read from local environment variables too. The common ones: diff --git a/apps/docs/content/docs/environment-variables.zh.mdx b/apps/docs/content/docs/environment-variables.zh.mdx index 57dc22f4d..b65d3a8a1 100644 --- a/apps/docs/content/docs/environment-variables.zh.mdx +++ b/apps/docs/content/docs/environment-variables.zh.mdx @@ -128,6 +128,25 @@ Multica 存储用户上传的附件(评论里的图片、文件等)。**优 **邀请流程本身不检查 signup 白名单**——但被邀请人必须先能**登录**才能接受邀请。如果对方已经有 Multica 账号(比如在其他工作区注册过),可以直接接受,不受白名单影响;**如果对方还没注册过**,他们登录的第一步(发送验证码)仍然会过白名单检查,被 `ALLOW_SIGNUP=false` 或 `ALLOWED_EMAILS` / `ALLOWED_EMAIL_DOMAINS` 拒绝的邮箱**无法完成注册,也就没法接受邀请**。 +## 速率限制(可选 Redis) + +公开认证端点——`/auth/send-code`、`/auth/verify-code`、`/auth/google`——前面挂了按 IP 的固定窗口限流。限流器后端是 Redis。`REDIS_URL` 不设时中间件**直通**(fail-open),后端启动会打日志 `rate limiting disabled: REDIS_URL not configured`。 + +| 环境变量 | 默认值 | 说明 | +|---|---|---| +| `REDIS_URL` | 空 | Redis 连接 URL(例如 `redis://localhost:6379/0`)。不设时认证端点的限流功能直接关闭。同一个 Redis 也被实时事件 fan-out、PAT 缓存、守护进程 token 缓存复用;不设时这些组件分别回落到内存模式 / 直查 DB | +| `RATE_LIMIT_AUTH` | `5` | 单 IP 每分钟对 `/auth/send-code` 和 `/auth/google` 的最大请求数 | +| `RATE_LIMIT_AUTH_VERIFY` | `20` | 单 IP 每分钟对 `/auth/verify-code` 的最大请求数 | +| `RATE_LIMIT_TRUSTED_PROXIES` | 空 | 逗号分隔的 CIDR 列表,列在内的来源 IP 才允许通过 `X-Forwarded-For` 标识客户端。默认空 = **永不信任 XFF**,限流器只看直连的 `RemoteAddr` | + +被限流的请求会返回 `429 Too Many Requests`,带 `Retry-After: 60` 头和 `{"error":"too many requests"}` 响应体。 + + +**部署在反向代理后面时必须设 `RATE_LIMIT_TRUSTED_PROXIES`。** 否则在后端看来所有真实用户都共用代理那个 IP,整个部署落到同一个桶里,`/auth/send-code` 会变成全站每分钟只能发 5 次。常见值:本机 Caddy / Nginx 用 `127.0.0.1/32,::1/128`;Cloudflare / ALB / CloudFront 用各家公开的 CDN IP 段。只有 `RemoteAddr` 落在这些 CIDR 内的请求才被允许通过 `X-Forwarded-For` 改写客户端 IP。 + + +这里的 `RATE_LIMIT_TRUSTED_PROXIES` 和 `MULTICA_TRUSTED_PROXIES` **不是同一个**变量——后者控制的是 autopilot webhook 端点(`/api/webhooks/autopilots/{token}`)的限流器。两个限流器各自读各自的列表,部署在代理后面的实例需要两个都配上。 + ## 守护进程的调节参数 守护进程跑在用户本地机器上,配置也是读本地环境变量。常用的几个: