mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 11:21:23 +00:00
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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user