feat(gateway): add trusted-proxy auth mode (#15940)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 279d4b304f
Co-authored-by: nickytonline <833231+nickytonline@users.noreply.github.com>
Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
Reviewed-by: @steipete
This commit is contained in:
Nick Taylor
2026-02-14 06:32:17 -05:00
committed by GitHub
parent 3a330e681b
commit 1fb52b4d7b
28 changed files with 1867 additions and 92 deletions

View File

@@ -12,7 +12,7 @@ import {
validateGatewayPasswordInput,
} from "./onboard-helpers.js";
type GatewayAuthChoice = "token" | "password";
type GatewayAuthChoice = "token" | "password" | "trusted-proxy";
export async function promptGatewayConfig(
cfg: OpenClawConfig,
@@ -103,13 +103,18 @@ export async function promptGatewayConfig(
options: [
{ value: "token", label: "Token", hint: "Recommended default" },
{ value: "password", label: "Password" },
{
value: "trusted-proxy",
label: "Trusted Proxy",
hint: "Behind reverse proxy (Pomerium, Caddy, Traefik, etc.)",
},
],
initialValue: "token",
}),
runtime,
) as GatewayAuthChoice;
const tailscaleMode = guardCancel(
let tailscaleMode = guardCancel(
await select({
message: "Tailscale exposure",
options: [
@@ -175,8 +180,25 @@ export async function promptGatewayConfig(
authMode = "password";
}
if (authMode === "trusted-proxy" && bind === "loopback") {
note("Trusted proxy auth requires network bind. Adjusting bind to lan.", "Note");
bind = "lan";
}
if (authMode === "trusted-proxy" && tailscaleMode !== "off") {
note(
"Trusted proxy auth is incompatible with Tailscale serve/funnel. Disabling Tailscale.",
"Note",
);
tailscaleMode = "off";
tailscaleResetOnExit = false;
}
let gatewayToken: string | undefined;
let gatewayPassword: string | undefined;
let trustedProxyConfig:
| { userHeader: string; requiredHeaders?: string[]; allowUsers?: string[] }
| undefined;
let trustedProxies: string[] | undefined;
let next = cfg;
if (authMode === "token") {
@@ -201,11 +223,88 @@ export async function promptGatewayConfig(
gatewayPassword = String(password ?? "").trim();
}
if (authMode === "trusted-proxy") {
note(
[
"Trusted proxy mode: OpenClaw trusts user identity from a reverse proxy.",
"The proxy must authenticate users and pass identity via headers.",
"Only requests from specified proxy IPs will be trusted.",
"",
"Common use cases: Pomerium, Caddy + OAuth, Traefik + forward auth",
"Docs: https://docs.openclaw.ai/gateway/trusted-proxy-auth",
].join("\n"),
"Trusted Proxy Auth",
);
const userHeader = guardCancel(
await text({
message: "Header containing user identity",
placeholder: "x-forwarded-user",
initialValue: "x-forwarded-user",
validate: (value) => (value?.trim() ? undefined : "User header is required"),
}),
runtime,
);
const requiredHeadersRaw = guardCancel(
await text({
message: "Required headers (comma-separated, optional)",
placeholder: "x-forwarded-proto,x-forwarded-host",
}),
runtime,
);
const requiredHeaders = requiredHeadersRaw
? String(requiredHeadersRaw)
.split(",")
.map((h) => h.trim())
.filter(Boolean)
: [];
const allowUsersRaw = guardCancel(
await text({
message: "Allowed users (comma-separated, blank = all authenticated users)",
placeholder: "nick@example.com,admin@company.com",
}),
runtime,
);
const allowUsers = allowUsersRaw
? String(allowUsersRaw)
.split(",")
.map((u) => u.trim())
.filter(Boolean)
: [];
const trustedProxiesRaw = guardCancel(
await text({
message: "Trusted proxy IPs (comma-separated)",
placeholder: "10.0.1.10,192.168.1.5",
validate: (value) => {
if (!value || String(value).trim() === "") {
return "At least one trusted proxy IP is required";
}
return undefined;
},
}),
runtime,
);
trustedProxies = String(trustedProxiesRaw)
.split(",")
.map((ip) => ip.trim())
.filter(Boolean);
trustedProxyConfig = {
userHeader: String(userHeader).trim(),
requiredHeaders: requiredHeaders.length > 0 ? requiredHeaders : undefined,
allowUsers: allowUsers.length > 0 ? allowUsers : undefined,
};
}
const authConfig = buildGatewayAuthConfig({
existing: next.gateway?.auth,
mode: authMode,
token: gatewayToken,
password: gatewayPassword,
trustedProxy: trustedProxyConfig,
});
next = {
@@ -217,6 +316,7 @@ export async function promptGatewayConfig(
bind,
auth: authConfig,
...(customBindHost && { customBindHost }),
...(trustedProxies && { trustedProxies }),
tailscale: {
...next.gateway?.tailscale,
mode: tailscaleMode,