fix(security): grant private AI endpoints by exact origin, dialled pinned

ALLOW_LOCAL_AI_PRIVATE_HOSTS=true let any user of the instance point the
Local provider or the OpenAI-compatible gateway at any private address
the server could reach, the metadata range included, and a granted host
was dialled with no connect-time pin at all.

AI_PRIVATE_ORIGINS grants a private AI endpoint with the exact-origin
grammar NOTIFICATION_PRIVATE_ORIGINS and NIGHTSCOUT_PRIVATE_ORIGINS use.
A granted call goes through the pinned operator-approved dispatcher:
redirects forbidden, and metadata, link-local and unspecified answers
dropped at dial time even for a granted name. Everything else stays on
the public pin. The Local client (buffered and streaming) and the
gateway and admin-key tags of the OpenAI client all take their policy
from one function.

The host-list form of ALLOW_LOCAL_AI_PRIVATE_HOSTS keeps working with the
same pinning; a never-grantable literal or a host:port entry is refused
with a one-time warning. Its "true" form grants nothing any more and is
reported in the boot readiness summary with the replacement.

A failed lab-report scan no longer puts the endpoint's response body into
the wide event; status and model stay. The response to the caller never
carried it.
This commit is contained in:
Marc-André Bombeck committed 2026-09-26 19:46:11 +02:00
1 parent f7b3ce6ee0
commit 1a674cf4b0
20 files changed
+508 -181

No files matched your search

@@ -129,3 +129,23 @@ describe("formatReadiness + logReadinessSummary", () => {
expect(errors).toHaveLength(1);
});
});
describe("collectReadiness — retired AI grant", () => {
it("warns, without blocking, when ALLOW_LOCAL_AI_PRIVATE_HOSTS is still `true`", () => {
const env = { ...baseEnv(), ALLOW_LOCAL_AI_PRIVATE_HOSTS: " TRUE " };
const line = lineFor(env, "ALLOW_LOCAL_AI_PRIVATE_HOSTS");
expect(line?.status).toBe("warn");
expect(line?.detail).toContain("AI_PRIVATE_ORIGINS");
expect(collectReadiness(env).hasBlocker).toBe(false);
});
it("says nothing for a host list or an unset variable", () => {
expect(
lineFor(
{ ...baseEnv(), ALLOW_LOCAL_AI_PRIVATE_HOSTS: "ollama.lan" },
"ALLOW_LOCAL_AI_PRIVATE_HOSTS",
),
).toBeUndefined();
expect(lineFor(baseEnv(), "ALLOW_LOCAL_AI_PRIVATE_HOSTS")).toBeUndefined();
});
});
+18
View File
@@ -15,6 +15,11 @@
* signpost in front of them.
*/
import {
LEGACY_ANY_HOST_WARNING,
legacyAnyHostConfigured,
} from "@/lib/ai/local-host-allowlist";
export interface ReadinessLine {
/** Short label, e.g. "ENCRYPTION_KEY". */
label: string;
@@ -121,6 +126,19 @@ export function collectReadiness(env: Env = process.env): ReadinessReport {
: "Secure cookie flag OFF — fine for plain-HTTP LAN; do NOT expose plain HTTP to the internet",
});
// --- Retired AI grant (a warning, never a blocker) ----------------------
// `ALLOW_LOCAL_AI_PRIVATE_HOSTS=true` used to open every private host on
// the network to every user's AI base URL. It grants nothing since
// v1.39.3; an operator who still has it set has a local AI endpoint that
// just stopped answering, and this is the line that says why.
if (legacyAnyHostConfigured(env)) {
lines.push({
label: "ALLOW_LOCAL_AI_PRIVATE_HOSTS",
status: "warn",
detail: LEGACY_ANY_HOST_WARNING,
});
}
// --- Optional subsystems (informational) --------------------------------
const optional: Array<[string, boolean]> = [
["SMTP (email)", allSet(env, ["SMTP_HOST", "SMTP_PORT", "SMTP_FROM"])],