From efd2792197e3ea1c01e393721c70192bdb7e5754 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 3 Aug 2026 17:57:36 -0400 Subject: [PATCH 1/3] bump default rate limit --- server/lib/readConfigFile.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/lib/readConfigFile.ts b/server/lib/readConfigFile.ts index cb4b83ccf..5774456a7 100644 --- a/server/lib/readConfigFile.ts +++ b/server/lib/readConfigFile.ts @@ -266,13 +266,13 @@ export const configSchema = z .positive() .gt(0) .optional() - .default(10), + .default(30), burst: z .number() .positive() .gt(0) .optional() - .default(16) + .default(50) }) .optional() .prefault({}) From f079714cafb4f2307a1fac8d9c6105a005cea58f Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 4 Aug 2026 10:07:52 -0400 Subject: [PATCH 2/3] Dont redirect when the browser agent is not real --- server/routers/badger/verifySession.ts | 54 ++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/server/routers/badger/verifySession.ts b/server/routers/badger/verifySession.ts index 33c75b101..99e0d9812 100644 --- a/server/routers/badger/verifySession.ts +++ b/server/routers/badger/verifySession.ts @@ -127,6 +127,9 @@ export async function verifyResourceSession( // Extract HTTP Basic Auth credentials if present const clientHeaderAuth = extractBasicAuth(headers); + const clientUserAgent = headers?.["user-agent"] || headers?.["User-Agent"]; + const clientIsBrowser = isBrowserUserAgent(clientUserAgent); + const clientIp = requestIp ? stripPortFromHost(requestIp, badgerVersion) : undefined; @@ -313,9 +316,14 @@ export async function verifyResourceSession( return allowed(res, undefined, dontStripSession); } - const redirectPath = `/auth/resource/${encodeURIComponent( - resource.resourceGuid - )}?redirect=${encodeURIComponent(originalRequestURL)}`; + // Only offer a browser redirect to clients that can actually follow one and log in + // (an interactive browser). Non-browser clients (curl, scripts, bots, etc.) just get + // an unauthorized response from Badger instead of a login redirect URL. + const redirectPath = clientIsBrowser + ? `/auth/resource/${encodeURIComponent( + resource.resourceGuid + )}?redirect=${encodeURIComponent(originalRequestURL)}` + : undefined; // check for access token in headers if ( @@ -1476,6 +1484,46 @@ async function getCountryCodeFromIp(ip: string): Promise { return cachedCountryCode; } +// Permissive by default: only reject known non-browser clients or a missing +// User-Agent (real browsers always send one). This avoids blocking real +// browsers whose UA string doesn't match a hardcoded allow-list. +const NON_BROWSER_USER_AGENT_PATTERNS = [ + /curl/, + /wget/, + /python-requests/, + /python-urllib/, + /go-http-client/, + /okhttp/, + /axios/, + /node-fetch/, + /postmanruntime/, + /insomnia/, + /libwww-perl/, + /java\//, + /ruby/, + /php/, + /bot/, + /spider/, + /crawler/, + /headlesschrome/, + /phantomjs/, + /httpclient/, + /prometheus/, + /go-resty/, + /apache-httpclient/, + /scrapy/ +]; + +function isBrowserUserAgent(userAgent: string | undefined): boolean { + if (!userAgent) { + return false; + } + + const ua = userAgent.toLowerCase(); + + return !NON_BROWSER_USER_AGENT_PATTERNS.some((pattern) => pattern.test(ua)); +} + function extractBasicAuth( headers: Record | undefined ): string | undefined { From 835a30cffe95ff29b9b41bf27b19bc9b74017fb7 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 4 Aug 2026 17:44:34 -0400 Subject: [PATCH 3/3] Show the cert status of the namespace domains properly --- .../certificates/getBatchedCertificates.ts | 32 ++++++++++++++++--- src/hooks/useCertificate.ts | 2 +- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/server/private/routers/certificates/getBatchedCertificates.ts b/server/private/routers/certificates/getBatchedCertificates.ts index 1c390b7dd..2ab5fd288 100644 --- a/server/private/routers/certificates/getBatchedCertificates.ts +++ b/server/private/routers/certificates/getBatchedCertificates.ts @@ -10,12 +10,12 @@ * * This file is not licensed under the AGPLv3. */ -import { certificates, db, domains, orgDomains } from "@server/db"; +import { certificates, db, domainNamespaces, domains, orgDomains } from "@server/db"; import response from "@server/lib/response"; import logger from "@server/logger"; import { type GetBatchedCertificateResponse } from "@server/routers/certificates/types"; import HttpCode from "@server/types/HttpCode"; -import { and, eq, inArray, or } from "drizzle-orm"; +import { and, eq, inArray, isNotNull, or } from "drizzle-orm"; import { NextFunction, Request, Response } from "express"; import createHttpError from "http-errors"; import { z } from "zod"; @@ -63,14 +63,28 @@ async function query(orgId: string, domainList: string[]) { }) .from(certificates) .innerJoin(domains, eq(certificates.domainId, domains.domainId)) - .innerJoin( + .leftJoin( orgDomains, and( eq(domains.domainId, orgDomains.domainId), eq(orgDomains.orgId, orgId) ) ) - .where(and(inArray(certificates.domain, domainList))); + .leftJoin( + domainNamespaces, + eq(domains.domainId, domainNamespaces.domainId) + ) + .where( + and( + inArray(certificates.domain, domainList), + // Namespace domains are shared across all orgs, so they skip + // the org-ownership check (mirrors verifyCertificateAccess). + or( + isNotNull(orgDomains.orgId), + isNotNull(domainNamespaces.domainNamespaceId) + ) + ) + ); // All non resolved domain certificates might be `ns` or `wildcard`, // which means exact domain certificates do not exist @@ -110,19 +124,27 @@ async function query(orgId: string, domainList: string[]) { }) .from(certificates) .innerJoin(domains, eq(certificates.domainId, domains.domainId)) - .innerJoin( + .leftJoin( orgDomains, and( eq(domains.domainId, orgDomains.domainId), eq(orgDomains.orgId, orgId) ) ) + .leftJoin( + domainNamespaces, + eq(domains.domainId, domainNamespaces.domainId) + ) .where( and( eq(certificates.wildcard, true), or( inArray(certificates.domain, [...domainLevelDownSet]), inArray(certificates.domain, [...wildcardDomainSet]) + ), + or( + isNotNull(orgDomains.orgId), + isNotNull(domainNamespaces.domainNamespaceId) ) ) ); diff --git a/src/hooks/useCertificate.ts b/src/hooks/useCertificate.ts index 1a4ff46f4..9afbb766e 100644 --- a/src/hooks/useCertificate.ts +++ b/src/hooks/useCertificate.ts @@ -111,7 +111,7 @@ export function useCertificate({ let certError: string | null = null; if (restartCert.isError) { certError = "Failed to restart"; - } else if (isError || initialCertValue === null) { + } else if (isError || (!isLoading && data === null)) { // Null value means failed to get the certificate certError = "Failed"; }