Unified · Split
| @@ -0,0 +1,6 @@ | |||
| 1 | --- | ||
| 2 | "@executor-js/host-selfhost": patch | ||
| 3 | "executor": patch | ||
| 4 | --- | ||
| 5 | |||
| 6 | Add `EXECUTOR_DISABLE_AUTH_RATE_LIMIT` to the self-host. Better Auth 1.6.17 and later enforce sign-in rate limits strictly in production, and with no trusted proxy header every caller shares one bucket of three sign-ins per ten seconds. The Docker release gate signs in from many test files at once and tripped it. The flag is off by default; the e2e harness sets it for the image it tests. | ||
| @@ -69,6 +69,7 @@ the container defaults. · 68 unmodified lines | |||
| 69 | | `EXECUTOR_ORG_NAME` | `Default` | Display name of the single org every user joins. | | 69 | | `EXECUTOR_ORG_NAME` | `Default` | Display name of the single org every user joins. | |
| 70 | | `EXECUTOR_ORG_SLUG` | `default` | URL slug for that org. | | 70 | | `EXECUTOR_ORG_SLUG` | `default` | URL slug for that org. | |
| 71 | | `EXECUTOR_ALLOW_LOCAL_NETWORK` | `false` | Allow sandboxed code to reach loopback / private addresses. Keep off unless you trust the code. | | 71 | | `EXECUTOR_ALLOW_LOCAL_NETWORK` | `false` | Allow sandboxed code to reach loopback / private addresses. Keep off unless you trust the code. | |
| 72 | | `EXECUTOR_DISABLE_AUTH_RATE_LIMIT` | `false` | Turn off sign-in rate limiting. Only when a proxy or WAF in front of Executor limits instead. | | ||
| 72 | 73 | ||
| 73 | Tracing is configured separately, and off unless you turn it on — see | 74 | Tracing is configured separately, and off unless you turn it on — see |
| 74 | [Tracing](/hosted/tracing). | 75 | [Tracing](/hosted/tracing). |
| @@ -36,6 +36,12 @@ · 35 unmodified lines | |||
| 36 | # default — adversarial generated code should not reach your internal network. | 36 | # default — adversarial generated code should not reach your internal network. |
| 37 | # EXECUTOR_ALLOW_LOCAL_NETWORK=false | 37 | # EXECUTOR_ALLOW_LOCAL_NETWORK=false |
| 38 | 38 | ||
| 39 | # --- Auth rate limiting ------------------------------------------------------- | ||
| 40 | # Sign-in attempts are rate-limited per client IP. Without a trusted proxy | ||
| 41 | # header every caller shares one bucket. Set the exact string "true" only when | ||
| 42 | # something in front of Executor rate-limits instead. | ||
| 43 | # EXECUTOR_DISABLE_AUTH_RATE_LIMIT=false | ||
| 44 | |||
| 39 | # --- Local stdio MCP (trusted deployments only) ------------------------------- | 45 | # --- Local stdio MCP (trusted deployments only) ------------------------------- |
| 40 | # Stdio MCP is disabled unless this is explicitly set to the exact string | 46 | # Stdio MCP is disabled unless this is explicitly set to the exact string |
| 41 | # "true". Enabling it lets users configure MCP servers whose commands execute | 47 | # "true". Enabling it lets users configure MCP servers whose commands execute |
| @@ -130,6 +130,10 @@ const makeAuthOptions = (client: Client, getOrganizationId: () => string, gate?: · 129 unmodified lines | |||
| 130 | baseURL: config.webBaseUrl, | 130 | baseURL: config.webBaseUrl, |
| 131 | trustedOrigins: [...config.trustedOrigins], | 131 | trustedOrigins: [...config.trustedOrigins], |
| 132 | advanced: { useSecureCookies: !hasInsecureTrustedOrigin }, | 132 | advanced: { useSecureCookies: !hasInsecureTrustedOrigin }, |
| 133 | // Better Auth's own limiter is on in production and off in development. | ||
| 134 | // Only an explicit opt-out is passed through, so that environment default | ||
| 135 | // stays in charge everywhere else. | ||
| 136 | ...(config.authRateLimit ? {} : { rateLimit: { enabled: false } }), | ||
| 133 | emailAndPassword: { enabled: true }, | 137 | emailAndPassword: { enabled: true }, |
| 134 | // `apiKey` issues long-lived personal keys (the API-keys page). With | 138 | // `apiKey` issues long-lived personal keys (the API-keys page). With |
| 135 | // `enableSessionForAPIKeys`, presenting a key resolves to its owner's | 139 | // `enableSessionForAPIKeys`, presenting a key resolves to its owner's |
| @@ -54,6 +54,15 @@ export interface SelfHostConfig { · 53 unmodified lines | |||
| 54 | * internal network unless an operator opts in. | 54 | * internal network unless an operator opts in. |
| 55 | */ | 55 | */ |
| 56 | readonly allowLocalNetwork: boolean; | 56 | readonly allowLocalNetwork: boolean; |
| 57 | /** | ||
| 58 | * Whether Better Auth rate-limits its own endpoints (sign-in and friends). | ||
| 59 | * Better Auth turns this on in production and keys the limit on the client | ||
| 60 | * IP it reads from a trusted proxy header. With no such header every caller | ||
| 61 | * shares one bucket, so an operator who rate-limits upstream, or an | ||
| 62 | * automated suite that signs in far faster than a person, turns it off with | ||
| 63 | * `EXECUTOR_DISABLE_AUTH_RATE_LIMIT=true`. | ||
| 64 | */ | ||
| 65 | readonly authRateLimit: boolean; | ||
| 57 | // Better Auth session secret. Always resolved (env, else generated + persisted | 66 | // Better Auth session secret. Always resolved (env, else generated + persisted |
| 58 | // under the data dir) so a single-container deploy boots with no env; the auth | 67 | // under the data dir) so a single-container deploy boots with no env; the auth |
| 59 | // layer still validates an explicitly-set env secret is long enough. | 68 | // layer still validates an explicitly-set env secret is long enough. |
| @@ -187,6 +196,7 @@ export const loadConfig = (): SelfHostConfig => { · 127 unmodified lines | |||
| 187 | webBaseUrl, | 196 | webBaseUrl, |
| 188 | trustedOrigins: resolveTrustedOrigins(webBaseUrl), | 197 | trustedOrigins: resolveTrustedOrigins(webBaseUrl), |
| 189 | allowLocalNetwork: process.env.EXECUTOR_ALLOW_LOCAL_NETWORK === "true", | 198 | allowLocalNetwork: process.env.EXECUTOR_ALLOW_LOCAL_NETWORK === "true", |
| 199 | authRateLimit: process.env.EXECUTOR_DISABLE_AUTH_RATE_LIMIT !== "true", | ||
| 190 | authSecret: resolveAuthSecret(), | 200 | authSecret: resolveAuthSecret(), |
| 191 | bootstrapAdminEmail: process.env.EXECUTOR_BOOTSTRAP_ADMIN_EMAIL, | 201 | bootstrapAdminEmail: process.env.EXECUTOR_BOOTSTRAP_ADMIN_EMAIL, |
| 192 | bootstrapAdminPassword: process.env.EXECUTOR_BOOTSTRAP_ADMIN_PASSWORD, | 202 | bootstrapAdminPassword: process.env.EXECUTOR_BOOTSTRAP_ADMIN_PASSWORD, |
| @@ -9,6 +9,8 @@ const TTL_ENV_NAME = "EXECUTOR_TOOLS_SYNC_TTL_MS"; · 8 unmodified lines | |||
| 9 | const originalValue = process.env[ENV_NAME]; | 9 | const originalValue = process.env[ENV_NAME]; |
| 10 | const originalSecret = process.env[SECRET_ENV_NAME]; | 10 | const originalSecret = process.env[SECRET_ENV_NAME]; |
| 11 | const originalTtl = process.env[TTL_ENV_NAME]; | 11 | const originalTtl = process.env[TTL_ENV_NAME]; |
| 12 | const RATE_LIMIT_ENV_NAME = "EXECUTOR_DISABLE_AUTH_RATE_LIMIT"; | ||
| 13 | const originalRateLimit = process.env[RATE_LIMIT_ENV_NAME]; | ||
| 12 | 14 | ||
| 13 | beforeEach(() => { | 15 | beforeEach(() => { |
| 14 | process.env[SECRET_ENV_NAME] = originalSecret ?? "executor-config-test-secret"; | 16 | process.env[SECRET_ENV_NAME] = originalSecret ?? "executor-config-test-secret"; |
| @@ -30,6 +32,11 @@ afterEach(() => { · 15 unmodified lines | |||
| 30 | } else { | 32 | } else { |
| 31 | process.env[TTL_ENV_NAME] = originalTtl; | 33 | process.env[TTL_ENV_NAME] = originalTtl; |
| 32 | } | 34 | } |
| 35 | if (originalRateLimit === undefined) { | ||
| 36 | delete process.env[RATE_LIMIT_ENV_NAME]; | ||
| 37 | } else { | ||
| 38 | process.env[RATE_LIMIT_ENV_NAME] = originalRateLimit; | ||
| 39 | } | ||
| 33 | }); | 40 | }); |
| 34 | 41 | ||
| 35 | const allowStdio = (): boolean => { | 42 | const allowStdio = (): boolean => { |
| @@ -112,3 +119,15 @@ test("a negative tools-sync TTL refuses to boot", () => { · 76 unmodified lines | |||
| 112 | process.env[TTL_ENV_NAME] = "-1"; | 119 | process.env[TTL_ENV_NAME] = "-1"; |
| 113 | expect(() => loadConfig()).toThrow(/must not be negative/); | 120 | expect(() => loadConfig()).toThrow(/must not be negative/); |
| 114 | }); | 121 | }); |
| 122 | |||
| 123 | test("auth rate limiting stays on unless the opt-out is exactly true", () => { | ||
| 124 | delete process.env[RATE_LIMIT_ENV_NAME]; | ||
| 125 | expect(loadConfig().authRateLimit).toBe(true); | ||
| 126 | process.env[RATE_LIMIT_ENV_NAME] = "TRUE"; | ||
| 127 | expect(loadConfig().authRateLimit).toBe(true); | ||
| 128 | }); | ||
| 129 | |||
| 130 | test("auth rate limiting is off when the opt-out is exactly true", () => { | ||
| 131 | process.env[RATE_LIMIT_ENV_NAME] = "true"; | ||
| 132 | expect(loadConfig().authRateLimit).toBe(false); | ||
| 133 | }); | ||
| @@ -108,6 +108,11 @@ export const runSelfhostContainer = async (options: RunContainerOptions): Promis · 107 unmodified lines | |||
| 108 | // test servers and points the instance at them. | 108 | // test servers and points the instance at them. |
| 109 | "-e", | 109 | "-e", |
| 110 | "EXECUTOR_ALLOW_LOCAL_NETWORK=true", | 110 | "EXECUTOR_ALLOW_LOCAL_NETWORK=true", |
| 111 | // The production image runs Better Auth's rate limiter. It sees no proxy | ||
| 112 | // header here, so it pools every caller into one bucket of three sign-ins | ||
| 113 | // per ten seconds, and this suite signs in from 100+ files at once. | ||
| 114 | "-e", | ||
| 115 | "EXECUTOR_DISABLE_AUTH_RATE_LIMIT=true", | ||
| 111 | options.image, | 116 | options.image, |
| 112 | ]; | 117 | ]; |
| 113 | log(options.logFile, `docker ${args.join(" ")}`); | 118 | log(options.logFile, `docker ${args.join(" ")}`); |