mirror of https://github.com/openclaw/openclaw.git
Gateway: fail closed unresolved local auth SecretRefs (#42672)
* Gateway: fail closed unresolved local auth SecretRefs * Docs: align node-host gateway auth precedence * CI: resolve rebase breakages in checks lanes * Tests: isolate LOCAL_REMOTE_FALLBACK_TOKEN env state * Gateway: remove stale remote.enabled auth-surface semantics * Changelog: note gateway SecretRef fail-closed fix
This commit is contained in:
parent
a52104c235
commit
0125ce1f44
|
|
@ -84,7 +84,7 @@ Docs: https://docs.openclaw.ai
|
|||
- Browser/Browserbase 429 handling: surface stable no-retry rate-limit guidance without buffering discarded HTTP 429 response bodies from remote browser services. (#40491) thanks @mvanhorn.
|
||||
- Gateway/auth: allow one trusted device-token retry on shared-token mismatch with recovery hints to prevent reconnect churn during token drift. (#42507) Thanks @joshavant.
|
||||
- Channels/allowlists: remove stale matcher caching so same-array allowlist edits and wildcard replacements take effect immediately, with regression coverage for in-place mutation cases.
|
||||
- Gateway/auth: fail closed when local `gateway.auth.*` SecretRefs are configured but unavailable, instead of silently falling back to `gateway.remote.*` credentials in local mode. Thanks @tdjackey.
|
||||
- Gateway/auth: fail closed when local `gateway.auth.*` SecretRefs are configured but unavailable, instead of silently falling back to `gateway.remote.*` credentials in local mode. (#42672) Thanks @joshavant.
|
||||
- Sandbox/fs bridge: pin staged writes to verified parent directories so temporary write files cannot materialize outside the allowed mount before atomic replace. Thanks @tdjackey.
|
||||
- Commands/config writes: enforce `configWrites` against both the originating account and the targeted account scope for `/config` and config-backed `/allowlist` edits, blocking sibling-account mutations while preserving gateway `operator.admin` flows. Thanks @tdjackey for reporting.
|
||||
|
||||
|
|
|
|||
|
|
@ -1337,6 +1337,8 @@ public struct SessionsPatchParams: Codable, Sendable {
|
|||
public let model: AnyCodable?
|
||||
public let spawnedby: AnyCodable?
|
||||
public let spawndepth: AnyCodable?
|
||||
public let subagentrole: AnyCodable?
|
||||
public let subagentcontrolscope: AnyCodable?
|
||||
public let sendpolicy: AnyCodable?
|
||||
public let groupactivation: AnyCodable?
|
||||
|
||||
|
|
@ -1355,6 +1357,8 @@ public struct SessionsPatchParams: Codable, Sendable {
|
|||
model: AnyCodable?,
|
||||
spawnedby: AnyCodable?,
|
||||
spawndepth: AnyCodable?,
|
||||
subagentrole: AnyCodable?,
|
||||
subagentcontrolscope: AnyCodable?,
|
||||
sendpolicy: AnyCodable?,
|
||||
groupactivation: AnyCodable?)
|
||||
{
|
||||
|
|
@ -1372,6 +1376,8 @@ public struct SessionsPatchParams: Codable, Sendable {
|
|||
self.model = model
|
||||
self.spawnedby = spawnedby
|
||||
self.spawndepth = spawndepth
|
||||
self.subagentrole = subagentrole
|
||||
self.subagentcontrolscope = subagentcontrolscope
|
||||
self.sendpolicy = sendpolicy
|
||||
self.groupactivation = groupactivation
|
||||
}
|
||||
|
|
@ -1391,6 +1397,8 @@ public struct SessionsPatchParams: Codable, Sendable {
|
|||
case model
|
||||
case spawnedby = "spawnedBy"
|
||||
case spawndepth = "spawnDepth"
|
||||
case subagentrole = "subagentRole"
|
||||
case subagentcontrolscope = "subagentControlScope"
|
||||
case sendpolicy = "sendPolicy"
|
||||
case groupactivation = "groupActivation"
|
||||
}
|
||||
|
|
@ -3046,7 +3054,7 @@ public struct ExecApprovalsSnapshot: Codable, Sendable {
|
|||
|
||||
public struct ExecApprovalRequestParams: Codable, Sendable {
|
||||
public let id: String?
|
||||
public let command: String
|
||||
public let command: String?
|
||||
public let commandargv: [String]?
|
||||
public let systemrunplan: [String: AnyCodable]?
|
||||
public let env: [String: AnyCodable]?
|
||||
|
|
@ -3067,7 +3075,7 @@ public struct ExecApprovalRequestParams: Codable, Sendable {
|
|||
|
||||
public init(
|
||||
id: String?,
|
||||
command: String,
|
||||
command: String?,
|
||||
commandargv: [String]?,
|
||||
systemrunplan: [String: AnyCodable]?,
|
||||
env: [String: AnyCodable]?,
|
||||
|
|
|
|||
|
|
@ -1337,6 +1337,8 @@ public struct SessionsPatchParams: Codable, Sendable {
|
|||
public let model: AnyCodable?
|
||||
public let spawnedby: AnyCodable?
|
||||
public let spawndepth: AnyCodable?
|
||||
public let subagentrole: AnyCodable?
|
||||
public let subagentcontrolscope: AnyCodable?
|
||||
public let sendpolicy: AnyCodable?
|
||||
public let groupactivation: AnyCodable?
|
||||
|
||||
|
|
@ -1355,6 +1357,8 @@ public struct SessionsPatchParams: Codable, Sendable {
|
|||
model: AnyCodable?,
|
||||
spawnedby: AnyCodable?,
|
||||
spawndepth: AnyCodable?,
|
||||
subagentrole: AnyCodable?,
|
||||
subagentcontrolscope: AnyCodable?,
|
||||
sendpolicy: AnyCodable?,
|
||||
groupactivation: AnyCodable?)
|
||||
{
|
||||
|
|
@ -1372,6 +1376,8 @@ public struct SessionsPatchParams: Codable, Sendable {
|
|||
self.model = model
|
||||
self.spawnedby = spawnedby
|
||||
self.spawndepth = spawndepth
|
||||
self.subagentrole = subagentrole
|
||||
self.subagentcontrolscope = subagentcontrolscope
|
||||
self.sendpolicy = sendpolicy
|
||||
self.groupactivation = groupactivation
|
||||
}
|
||||
|
|
@ -1391,6 +1397,8 @@ public struct SessionsPatchParams: Codable, Sendable {
|
|||
case model
|
||||
case spawnedby = "spawnedBy"
|
||||
case spawndepth = "spawnDepth"
|
||||
case subagentrole = "subagentRole"
|
||||
case subagentcontrolscope = "subagentControlScope"
|
||||
case sendpolicy = "sendPolicy"
|
||||
case groupactivation = "groupActivation"
|
||||
}
|
||||
|
|
@ -3046,7 +3054,7 @@ public struct ExecApprovalsSnapshot: Codable, Sendable {
|
|||
|
||||
public struct ExecApprovalRequestParams: Codable, Sendable {
|
||||
public let id: String?
|
||||
public let command: String
|
||||
public let command: String?
|
||||
public let commandargv: [String]?
|
||||
public let systemrunplan: [String: AnyCodable]?
|
||||
public let env: [String: AnyCodable]?
|
||||
|
|
@ -3067,7 +3075,7 @@ public struct ExecApprovalRequestParams: Codable, Sendable {
|
|||
|
||||
public init(
|
||||
id: String?,
|
||||
command: String,
|
||||
command: String?,
|
||||
commandargv: [String]?,
|
||||
systemrunplan: [String: AnyCodable]?,
|
||||
env: [String: AnyCodable]?,
|
||||
|
|
|
|||
|
|
@ -946,7 +946,7 @@ Default slash command settings:
|
|||
Gateway auth for this handler uses the same shared credential resolution contract as other Gateway clients:
|
||||
|
||||
- env-first local auth (`OPENCLAW_GATEWAY_TOKEN` / `OPENCLAW_GATEWAY_PASSWORD` then `gateway.auth.*`)
|
||||
- in local mode, `gateway.remote.*` can be used as fallback when `gateway.auth.*` is unset
|
||||
- in local mode, `gateway.remote.*` can be used as fallback only when `gateway.auth.*` is unset; configured-but-unresolved local SecretRefs fail closed
|
||||
- remote-mode support via `gateway.remote.*` when applicable
|
||||
- URL overrides are override-safe: CLI overrides do not reuse implicit credentials, and env overrides use env credentials only
|
||||
|
||||
|
|
|
|||
|
|
@ -273,7 +273,7 @@ Security note:
|
|||
- `--token` and `--password` can be visible in local process listings on some systems.
|
||||
- Prefer `--token-file`/`--password-file` or environment variables (`OPENCLAW_GATEWAY_TOKEN`, `OPENCLAW_GATEWAY_PASSWORD`).
|
||||
- Gateway auth resolution follows the shared contract used by other Gateway clients:
|
||||
- local mode: env (`OPENCLAW_GATEWAY_*`) -> `gateway.auth.*` -> `gateway.remote.*` fallback when `gateway.auth.*` is unset
|
||||
- local mode: env (`OPENCLAW_GATEWAY_*`) -> `gateway.auth.*` -> `gateway.remote.*` fallback only when `gateway.auth.*` is unset (configured-but-unresolved local SecretRefs fail closed)
|
||||
- remote mode: `gateway.remote.*` with env/config fallback per remote precedence rules
|
||||
- `--url` is override-safe and does not reuse implicit config/env credentials; pass explicit `--token`/`--password` (or file variants)
|
||||
- ACP runtime backend child processes receive `OPENCLAW_SHELL=acp`, which can be used for context-specific shell/profile rules.
|
||||
|
|
|
|||
|
|
@ -1018,7 +1018,7 @@ Subcommands:
|
|||
|
||||
Auth notes:
|
||||
|
||||
- `node` resolves gateway auth from env/config (no `--token`/`--password` flags): `OPENCLAW_GATEWAY_TOKEN` / `OPENCLAW_GATEWAY_PASSWORD`, then `gateway.auth.*`, with remote-mode support via `gateway.remote.*`.
|
||||
- `node` resolves gateway auth from env/config (no `--token`/`--password` flags): `OPENCLAW_GATEWAY_TOKEN` / `OPENCLAW_GATEWAY_PASSWORD`, then `gateway.auth.*`. In local mode, node host intentionally ignores `gateway.remote.*`; in `gateway.mode=remote`, `gateway.remote.*` participates per remote precedence rules.
|
||||
- Legacy `CLAWDBOT_GATEWAY_*` env vars are intentionally ignored for node-host auth resolution.
|
||||
|
||||
## Nodes
|
||||
|
|
|
|||
|
|
@ -64,7 +64,8 @@ Options:
|
|||
|
||||
- `OPENCLAW_GATEWAY_TOKEN` / `OPENCLAW_GATEWAY_PASSWORD` are checked first.
|
||||
- Then local config fallback: `gateway.auth.token` / `gateway.auth.password`.
|
||||
- In local mode, `gateway.remote.token` / `gateway.remote.password` are also eligible as fallback when `gateway.auth.*` is unset.
|
||||
- In local mode, node host intentionally does not inherit `gateway.remote.token` / `gateway.remote.password`.
|
||||
- If `gateway.auth.token` / `gateway.auth.password` is explicitly configured via SecretRef and unresolved, node auth resolution fails closed (no remote fallback masking).
|
||||
- In `gateway.mode=remote`, remote client fields (`gateway.remote.token` / `gateway.remote.password`) are also eligible per remote precedence rules.
|
||||
- Legacy `CLAWDBOT_GATEWAY_*` env vars are ignored for node host auth resolution.
|
||||
|
||||
|
|
|
|||
|
|
@ -2470,7 +2470,8 @@ See [Plugins](/tools/plugin).
|
|||
- `remote.transport`: `ssh` (default) or `direct` (ws/wss). For `direct`, `remote.url` must be `ws://` or `wss://`.
|
||||
- `OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1`: client-side break-glass override that allows plaintext `ws://` to trusted private-network IPs; default remains loopback-only for plaintext.
|
||||
- `gateway.remote.token` / `.password` are remote-client credential fields. They do not configure gateway auth by themselves.
|
||||
- Local gateway call paths can use `gateway.remote.*` as fallback when `gateway.auth.*` is unset.
|
||||
- Local gateway call paths can use `gateway.remote.*` as fallback only when `gateway.auth.*` is unset.
|
||||
- If `gateway.auth.token` / `gateway.auth.password` is explicitly configured via SecretRef and unresolved, resolution fails closed (no remote fallback masking).
|
||||
- `trustedProxies`: reverse proxy IPs that terminate TLS. Only list proxies you control.
|
||||
- `allowRealIpFallback`: when `true`, the gateway accepts `X-Real-IP` if `X-Forwarded-For` is missing. Default `false` for fail-closed behavior.
|
||||
- `gateway.tools.deny`: extra tool names blocked for HTTP `POST /tools/invoke` (extends default deny list).
|
||||
|
|
|
|||
|
|
@ -103,18 +103,19 @@ When the gateway is loopback-only, keep the URL at `ws://127.0.0.1:18789` and op
|
|||
|
||||
## Credential precedence
|
||||
|
||||
Gateway credential resolution follows one shared contract across call/probe/status paths, Discord exec-approval monitoring, and node-host connections:
|
||||
Gateway credential resolution follows one shared contract across call/probe/status paths and Discord exec-approval monitoring. Node-host uses the same base contract with one local-mode exception (it intentionally ignores `gateway.remote.*`):
|
||||
|
||||
- Explicit credentials (`--token`, `--password`, or tool `gatewayToken`) always win on call paths that accept explicit auth.
|
||||
- URL override safety:
|
||||
- CLI URL overrides (`--url`) never reuse implicit config/env credentials.
|
||||
- Env URL overrides (`OPENCLAW_GATEWAY_URL`) may use env credentials only (`OPENCLAW_GATEWAY_TOKEN` / `OPENCLAW_GATEWAY_PASSWORD`).
|
||||
- Local mode defaults:
|
||||
- token: `OPENCLAW_GATEWAY_TOKEN` -> `gateway.auth.token` -> `gateway.remote.token`
|
||||
- password: `OPENCLAW_GATEWAY_PASSWORD` -> `gateway.auth.password` -> `gateway.remote.password`
|
||||
- token: `OPENCLAW_GATEWAY_TOKEN` -> `gateway.auth.token` -> `gateway.remote.token` (remote fallback applies only when local auth token input is unset)
|
||||
- password: `OPENCLAW_GATEWAY_PASSWORD` -> `gateway.auth.password` -> `gateway.remote.password` (remote fallback applies only when local auth password input is unset)
|
||||
- Remote mode defaults:
|
||||
- token: `gateway.remote.token` -> `OPENCLAW_GATEWAY_TOKEN` -> `gateway.auth.token`
|
||||
- password: `OPENCLAW_GATEWAY_PASSWORD` -> `gateway.remote.password` -> `gateway.auth.password`
|
||||
- Node-host local-mode exception: `gateway.remote.token` / `gateway.remote.password` are ignored.
|
||||
- Remote probe/status token checks are strict by default: they use `gateway.remote.token` only (no local token fallback) when targeting remote mode.
|
||||
- Legacy `CLAWDBOT_GATEWAY_*` env vars are only used by compatibility call paths; probe/status/auth resolution uses `OPENCLAW_GATEWAY_*` only.
|
||||
|
||||
|
|
@ -140,7 +141,8 @@ Short version: **keep the Gateway loopback-only** unless you’re sure you need
|
|||
set `OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1` on the client process as break-glass.
|
||||
- **Non-loopback binds** (`lan`/`tailnet`/`custom`, or `auto` when loopback is unavailable) must use auth tokens/passwords.
|
||||
- `gateway.remote.token` / `.password` are client credential sources. They do **not** configure server auth by themselves.
|
||||
- Local call paths can use `gateway.remote.*` as fallback when `gateway.auth.*` is unset.
|
||||
- Local call paths can use `gateway.remote.*` as fallback only when `gateway.auth.*` is unset.
|
||||
- If `gateway.auth.token` / `gateway.auth.password` is explicitly configured via SecretRef and unresolved, resolution fails closed (no remote fallback masking).
|
||||
- `gateway.remote.tlsFingerprint` pins the remote TLS cert when using `wss://`.
|
||||
- **Tailscale Serve** can authenticate Control UI/WebSocket traffic via identity
|
||||
headers when `gateway.auth.allowTailscale: true`; HTTP API endpoints still
|
||||
|
|
|
|||
|
|
@ -41,13 +41,13 @@ Examples of inactive surfaces:
|
|||
- Web search provider-specific keys that are not selected by `tools.web.search.provider`.
|
||||
In auto mode (provider unset), keys are consulted by precedence for provider auto-detection until one resolves.
|
||||
After selection, non-selected provider keys are treated as inactive until selected.
|
||||
- `gateway.remote.token` / `gateway.remote.password` SecretRefs are active (when `gateway.remote.enabled` is not `false`) if one of these is true:
|
||||
- `gateway.remote.token` / `gateway.remote.password` SecretRefs are active if one of these is true:
|
||||
- `gateway.mode=remote`
|
||||
- `gateway.remote.url` is configured
|
||||
- `gateway.tailscale.mode` is `serve` or `funnel`
|
||||
In local mode without those remote surfaces:
|
||||
- `gateway.remote.token` is active when token auth can win and no env/auth token is configured.
|
||||
- `gateway.remote.password` is active only when password auth can win and no env/auth password is configured.
|
||||
- In local mode without those remote surfaces:
|
||||
- `gateway.remote.token` is active when token auth can win and no env/auth token is configured.
|
||||
- `gateway.remote.password` is active only when password auth can win and no env/auth password is configured.
|
||||
- `gateway.auth.token` SecretRef is inactive for startup auth resolution when `OPENCLAW_GATEWAY_TOKEN` (or `CLAWDBOT_GATEWAY_TOKEN`) is set, because env token input wins for that runtime.
|
||||
|
||||
## Gateway auth surface diagnostics
|
||||
|
|
|
|||
|
|
@ -754,8 +754,10 @@ Doctor can generate one for you: `openclaw doctor --generate-gateway-token`.
|
|||
|
||||
Note: `gateway.remote.token` / `.password` are client credential sources. They
|
||||
do **not** protect local WS access by themselves.
|
||||
Local call paths can use `gateway.remote.*` as fallback when `gateway.auth.*`
|
||||
Local call paths can use `gateway.remote.*` as fallback only when `gateway.auth.*`
|
||||
is unset.
|
||||
If `gateway.auth.token` / `gateway.auth.password` is explicitly configured via
|
||||
SecretRef and unresolved, resolution fails closed (no remote fallback masking).
|
||||
Optional: pin remote TLS with `gateway.remote.tlsFingerprint` when using `wss://`.
|
||||
Plaintext `ws://` is loopback-only by default. For trusted private-network
|
||||
paths, set `OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1` on the client process as break-glass.
|
||||
|
|
|
|||
|
|
@ -1452,7 +1452,8 @@ Non-loopback binds **require auth**. Configure `gateway.auth.mode` + `gateway.au
|
|||
Notes:
|
||||
|
||||
- `gateway.remote.token` / `.password` do **not** enable local gateway auth by themselves.
|
||||
- Local call paths can use `gateway.remote.*` as fallback when `gateway.auth.*` is unset.
|
||||
- Local call paths can use `gateway.remote.*` as fallback only when `gateway.auth.*` is unset.
|
||||
- If `gateway.auth.token` / `gateway.auth.password` is explicitly configured via SecretRef and unresolved, resolution fails closed (no remote fallback masking).
|
||||
- The Control UI authenticates via `connect.params.auth.token` (stored in app/UI settings). Avoid putting tokens in URLs.
|
||||
|
||||
### Why do I need a token on localhost now
|
||||
|
|
|
|||
|
|
@ -92,7 +92,10 @@ Notes:
|
|||
|
||||
- `openclaw node run` supports token or password auth.
|
||||
- Env vars are preferred: `OPENCLAW_GATEWAY_TOKEN` / `OPENCLAW_GATEWAY_PASSWORD`.
|
||||
- Config fallback is `gateway.auth.token` / `gateway.auth.password`; in remote mode, `gateway.remote.token` / `gateway.remote.password` are also eligible.
|
||||
- Config fallback is `gateway.auth.token` / `gateway.auth.password`.
|
||||
- In local mode, node host intentionally ignores `gateway.remote.token` / `gateway.remote.password`.
|
||||
- In remote mode, `gateway.remote.token` / `gateway.remote.password` are eligible per remote precedence rules.
|
||||
- If active local `gateway.auth.*` SecretRefs are configured but unresolved, node-host auth fails closed.
|
||||
- Legacy `CLAWDBOT_GATEWAY_*` env vars are intentionally ignored by node-host auth resolution.
|
||||
|
||||
### Start a node host (service)
|
||||
|
|
|
|||
|
|
@ -198,16 +198,6 @@ function appendCronDeliveryInstruction(params: {
|
|||
return `${params.commandBody}\n\nReturn your summary as plain text; it will be delivered automatically. If the task explicitly calls for messaging a specific external recipient, note who/where it should go instead of sending it yourself.`.trim();
|
||||
}
|
||||
|
||||
function resolveCronEmbeddedAgentLane(lane?: string) {
|
||||
const trimmed = lane?.trim();
|
||||
// Cron jobs already execute inside the cron command lane. Reusing that same
|
||||
// lane for the nested embedded-agent run deadlocks: the outer cron task holds
|
||||
// the lane while the inner run waits to reacquire it.
|
||||
if (!trimmed || trimmed === "cron") {
|
||||
return CommandLane.Nested;
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
export async function runCronIsolatedAgentTurn(params: {
|
||||
cfg: OpenClawConfig;
|
||||
deps: CliDeps;
|
||||
|
|
|
|||
|
|
@ -655,6 +655,7 @@ describe("callGateway password resolution", () => {
|
|||
envSnapshot = captureEnv([
|
||||
"OPENCLAW_GATEWAY_PASSWORD",
|
||||
"OPENCLAW_GATEWAY_TOKEN",
|
||||
"LOCAL_REMOTE_FALLBACK_TOKEN",
|
||||
"LOCAL_REF_PASSWORD",
|
||||
"REMOTE_REF_TOKEN",
|
||||
"REMOTE_REF_PASSWORD",
|
||||
|
|
@ -662,6 +663,7 @@ describe("callGateway password resolution", () => {
|
|||
resetGatewayCallMocks();
|
||||
delete process.env.OPENCLAW_GATEWAY_PASSWORD;
|
||||
delete process.env.OPENCLAW_GATEWAY_TOKEN;
|
||||
delete process.env.LOCAL_REMOTE_FALLBACK_TOKEN;
|
||||
delete process.env.LOCAL_REF_PASSWORD;
|
||||
delete process.env.REMOTE_REF_TOKEN;
|
||||
delete process.env.REMOTE_REF_PASSWORD;
|
||||
|
|
@ -813,6 +815,30 @@ describe("callGateway password resolution", () => {
|
|||
expect(lastClientOptions?.password).toBe("resolved-local-fallback-password"); // pragma: allowlist secret
|
||||
});
|
||||
|
||||
it("fails closed when unresolved local token SecretRef would otherwise fall back to remote token", async () => {
|
||||
process.env.LOCAL_REMOTE_FALLBACK_TOKEN = "resolved-local-remote-fallback-token";
|
||||
loadConfig.mockReturnValue({
|
||||
gateway: {
|
||||
mode: "local",
|
||||
bind: "loopback",
|
||||
auth: {
|
||||
mode: "token",
|
||||
token: { source: "env", provider: "default", id: "MISSING_LOCAL_REF_TOKEN" },
|
||||
},
|
||||
remote: {
|
||||
token: { source: "env", provider: "default", id: "LOCAL_REMOTE_FALLBACK_TOKEN" },
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
providers: {
|
||||
default: { source: "env" },
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig);
|
||||
|
||||
await expect(callGateway({ method: "health" })).rejects.toThrow("gateway.auth.token");
|
||||
});
|
||||
|
||||
it.each(["none", "trusted-proxy"] as const)(
|
||||
"ignores unresolved local password ref when auth mode is %s",
|
||||
async (mode) => {
|
||||
|
|
|
|||
|
|
@ -416,4 +416,74 @@ describe("resolveGatewayConnectionAuth", () => {
|
|||
}),
|
||||
).toThrow("gateway.auth.password");
|
||||
});
|
||||
|
||||
it("fails closed when local token SecretRef is unresolved and remote token fallback exists", async () => {
|
||||
const config = cfg({
|
||||
gateway: {
|
||||
mode: "local",
|
||||
auth: {
|
||||
mode: "token",
|
||||
token: { source: "env", provider: "default", id: "MISSING_LOCAL_TOKEN" },
|
||||
},
|
||||
remote: {
|
||||
token: "remote-token",
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
providers: {
|
||||
default: { source: "env" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
resolveGatewayConnectionAuth({
|
||||
config,
|
||||
env: {} as NodeJS.ProcessEnv,
|
||||
includeLegacyEnv: false,
|
||||
}),
|
||||
).rejects.toThrow("gateway.auth.token");
|
||||
expect(() =>
|
||||
resolveGatewayConnectionAuthFromConfig({
|
||||
cfg: config,
|
||||
env: {} as NodeJS.ProcessEnv,
|
||||
includeLegacyEnv: false,
|
||||
}),
|
||||
).toThrow("gateway.auth.token");
|
||||
});
|
||||
|
||||
it("fails closed when local password SecretRef is unresolved and remote password fallback exists", async () => {
|
||||
const config = cfg({
|
||||
gateway: {
|
||||
mode: "local",
|
||||
auth: {
|
||||
mode: "password",
|
||||
password: { source: "env", provider: "default", id: "MISSING_LOCAL_PASSWORD" },
|
||||
},
|
||||
remote: {
|
||||
password: "remote-password", // pragma: allowlist secret
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
providers: {
|
||||
default: { source: "env" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
resolveGatewayConnectionAuth({
|
||||
config,
|
||||
env: {} as NodeJS.ProcessEnv,
|
||||
includeLegacyEnv: false,
|
||||
}),
|
||||
).rejects.toThrow("gateway.auth.password");
|
||||
expect(() =>
|
||||
resolveGatewayConnectionAuthFromConfig({
|
||||
cfg: config,
|
||||
env: {} as NodeJS.ProcessEnv,
|
||||
includeLegacyEnv: false,
|
||||
}),
|
||||
).toThrow("gateway.auth.password");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ export type GatewayCredentialPlan = {
|
|||
remoteMode: boolean;
|
||||
remoteUrlConfigured: boolean;
|
||||
tailscaleRemoteExposure: boolean;
|
||||
remoteEnabled: boolean;
|
||||
remoteConfiguredSurface: boolean;
|
||||
remoteTokenFallbackActive: boolean;
|
||||
remoteTokenActive: boolean;
|
||||
|
|
@ -187,7 +186,6 @@ export function createGatewayCredentialPlan(params: {
|
|||
const remoteUrlConfigured = Boolean(trimToUndefined(remote?.url));
|
||||
const tailscaleRemoteExposure =
|
||||
gateway?.tailscale?.mode === "serve" || gateway?.tailscale?.mode === "funnel";
|
||||
const remoteEnabled = remote?.enabled !== false;
|
||||
const remoteConfiguredSurface = remoteMode || remoteUrlConfigured || tailscaleRemoteExposure;
|
||||
const remoteTokenFallbackActive = localTokenCanWin && !envToken && !localToken.configured;
|
||||
const remotePasswordFallbackActive = !envPassword && !localPassword.configured && passwordCanWin;
|
||||
|
|
@ -209,12 +207,10 @@ export function createGatewayCredentialPlan(params: {
|
|||
remoteMode,
|
||||
remoteUrlConfigured,
|
||||
tailscaleRemoteExposure,
|
||||
remoteEnabled,
|
||||
remoteConfiguredSurface,
|
||||
remoteTokenFallbackActive,
|
||||
remoteTokenActive: remoteEnabled && (remoteConfiguredSurface || remoteTokenFallbackActive),
|
||||
remoteTokenActive: remoteConfiguredSurface || remoteTokenFallbackActive,
|
||||
remotePasswordFallbackActive,
|
||||
remotePasswordActive:
|
||||
remoteEnabled && (remoteConfiguredSurface || remotePasswordFallbackActive),
|
||||
remotePasswordActive: remoteConfiguredSurface || remotePasswordFallbackActive,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -158,6 +158,58 @@ describe("resolveGatewayCredentialsFromConfig", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("fails closed when local token SecretRef is unresolved and remote token fallback exists", () => {
|
||||
expect(() =>
|
||||
resolveGatewayCredentialsFromConfig({
|
||||
cfg: {
|
||||
gateway: {
|
||||
mode: "local",
|
||||
auth: {
|
||||
mode: "token",
|
||||
token: { source: "env", provider: "default", id: "MISSING_LOCAL_TOKEN" },
|
||||
},
|
||||
remote: {
|
||||
token: "remote-token",
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
providers: {
|
||||
default: { source: "env" },
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig,
|
||||
env: {} as NodeJS.ProcessEnv,
|
||||
includeLegacyEnv: false,
|
||||
}),
|
||||
).toThrow("gateway.auth.token");
|
||||
});
|
||||
|
||||
it("fails closed when local password SecretRef is unresolved and remote password fallback exists", () => {
|
||||
expect(() =>
|
||||
resolveGatewayCredentialsFromConfig({
|
||||
cfg: {
|
||||
gateway: {
|
||||
mode: "local",
|
||||
auth: {
|
||||
mode: "password",
|
||||
password: { source: "env", provider: "default", id: "MISSING_LOCAL_PASSWORD" },
|
||||
},
|
||||
remote: {
|
||||
password: "remote-password", // pragma: allowlist secret
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
providers: {
|
||||
default: { source: "env" },
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig,
|
||||
env: {} as NodeJS.ProcessEnv,
|
||||
includeLegacyEnv: false,
|
||||
}),
|
||||
).toThrow("gateway.auth.password");
|
||||
});
|
||||
|
||||
it("throws when local password auth relies on an unresolved SecretRef", () => {
|
||||
expect(() =>
|
||||
resolveGatewayCredentialsFromConfig({
|
||||
|
|
|
|||
|
|
@ -111,7 +111,6 @@ describe("evaluateGatewayAuthSurfaceStates", () => {
|
|||
gateway: {
|
||||
mode: "local",
|
||||
remote: {
|
||||
enabled: true,
|
||||
token: envRef("GW_REMOTE_TOKEN"),
|
||||
},
|
||||
},
|
||||
|
|
@ -131,7 +130,6 @@ describe("evaluateGatewayAuthSurfaceStates", () => {
|
|||
mode: "password",
|
||||
},
|
||||
remote: {
|
||||
enabled: true,
|
||||
token: envRef("GW_REMOTE_TOKEN"),
|
||||
},
|
||||
},
|
||||
|
|
@ -153,7 +151,6 @@ describe("evaluateGatewayAuthSurfaceStates", () => {
|
|||
token: envRef("GW_AUTH_TOKEN"),
|
||||
},
|
||||
remote: {
|
||||
enabled: true,
|
||||
token: envRef("GW_REMOTE_TOKEN"),
|
||||
},
|
||||
},
|
||||
|
|
@ -170,7 +167,6 @@ describe("evaluateGatewayAuthSurfaceStates", () => {
|
|||
const states = evaluate({
|
||||
gateway: {
|
||||
remote: {
|
||||
enabled: true,
|
||||
url: "wss://gateway.example.com",
|
||||
password: envRef("GW_REMOTE_PASSWORD"),
|
||||
},
|
||||
|
|
@ -190,7 +186,6 @@ describe("evaluateGatewayAuthSurfaceStates", () => {
|
|||
mode: "token",
|
||||
},
|
||||
remote: {
|
||||
enabled: true,
|
||||
password: envRef("GW_REMOTE_PASSWORD"),
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -166,9 +166,6 @@ export function evaluateGatewayAuthSurfaceStates(params: {
|
|||
if (!remote) {
|
||||
return "gateway.remote is not configured.";
|
||||
}
|
||||
if (!plan.remoteEnabled) {
|
||||
return "gateway.remote.enabled is false.";
|
||||
}
|
||||
if (plan.remoteConfiguredSurface) {
|
||||
return `remote surface is active: ${remoteSurfaceReason}.`;
|
||||
}
|
||||
|
|
@ -191,9 +188,6 @@ export function evaluateGatewayAuthSurfaceStates(params: {
|
|||
if (!remote) {
|
||||
return "gateway.remote is not configured.";
|
||||
}
|
||||
if (!plan.remoteEnabled) {
|
||||
return "gateway.remote.enabled is false.";
|
||||
}
|
||||
if (plan.remoteConfiguredSurface) {
|
||||
return `remote surface is active: ${remoteSurfaceReason}.`;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue