fix(gateway): prevent auth bypass when behind unconfigured reverse proxy (#1795)

* fix(gateway): prevent auth bypass when behind unconfigured reverse proxy

When proxy headers (X-Forwarded-For, X-Real-IP) are present but
gateway.trustedProxies is not configured, the gateway now treats
connections as non-local. This prevents a scenario where all proxied
requests appear to come from localhost and receive automatic trust.

Previously, running behind nginx/Caddy without configuring trustedProxies
would cause isLocalClient=true for all external connections, potentially
bypassing authentication and auto-approving device pairing.

The gateway now logs a warning when this condition is detected, guiding
operators to configure trustedProxies for proper client IP detection.

Also adds documentation for reverse proxy security configuration.

* fix: harden reverse proxy auth (#1795) (thanks @orlyjamie)

---------

Co-authored-by: orlyjamie <orlyjamie@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
Jamieson O'Reilly
2026-01-26 02:08:03 +11:00
committed by GitHub
parent 1c606fdb57
commit 6aec34bc60
6 changed files with 133 additions and 6 deletions

View File

@@ -53,6 +53,30 @@ describe("security audit", () => {
).toBe(true);
});
it("warns when loopback control UI lacks trusted proxies", async () => {
const cfg: ClawdbotConfig = {
gateway: {
bind: "loopback",
controlUi: { enabled: true },
},
};
const res = await runSecurityAudit({
config: cfg,
includeFilesystem: false,
includeChannelSecurity: false,
});
expect(res.findings).toEqual(
expect.arrayContaining([
expect.objectContaining({
checkId: "gateway.trusted_proxies_missing",
severity: "warn",
}),
]),
);
});
it("flags logging.redactSensitive=off", async () => {
const cfg: ClawdbotConfig = {
logging: { redactSensitive: "off" },

View File

@@ -207,6 +207,10 @@ function collectGatewayConfigFindings(cfg: ClawdbotConfig): SecurityAuditFinding
const bind = typeof cfg.gateway?.bind === "string" ? cfg.gateway.bind : "loopback";
const tailscaleMode = cfg.gateway?.tailscale?.mode ?? "off";
const auth = resolveGatewayAuth({ authConfig: cfg.gateway?.auth, tailscaleMode });
const controlUiEnabled = cfg.gateway?.controlUi?.enabled !== false;
const trustedProxies = Array.isArray(cfg.gateway?.trustedProxies)
? cfg.gateway.trustedProxies
: [];
if (bind !== "loopback" && auth.mode === "none") {
findings.push({
@@ -218,6 +222,20 @@ function collectGatewayConfigFindings(cfg: ClawdbotConfig): SecurityAuditFinding
});
}
if (bind === "loopback" && controlUiEnabled && trustedProxies.length === 0) {
findings.push({
checkId: "gateway.trusted_proxies_missing",
severity: "warn",
title: "Reverse proxy headers are not trusted",
detail:
"gateway.bind is loopback and gateway.trustedProxies is empty. " +
"If you expose the Control UI through a reverse proxy, configure trusted proxies " +
"so local-client checks cannot be spoofed.",
remediation:
"Set gateway.trustedProxies to your proxy IPs or keep the Control UI local-only.",
});
}
if (tailscaleMode === "funnel") {
findings.push({
checkId: "gateway.tailscale_funnel",