Skip to content

[Bug]: Linux node daemon install inlines gateway token into user systemd unit #78043

@coygeek

Description

@coygeek

Severity Assessment

CVSS Assessment

Metric v3.1 v4.0
Score 7.8 / 10.0 8.5 / 10.0
Severity High High
Vector CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H CVSS:4.0/AV:L/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N
Calculator CVSS v3.1 Calculator CVSS v4.0 Calculator

Threat Model Alignment

Classification: security-specific

OpenClaw's gateway security guide states that shared-secret bearer credentials are full operator secrets for the gateway, and authorizeTokenAuth() treats a matching OPENCLAW_GATEWAY_TOKEN as successful token authentication. The Linux node-daemon install path persists that token inside the generated user systemd unit, so any same-host principal that can read the unit crosses the gateway.auth boundary without pairing or another gateway credential. SECURITY.md excludes exposed third-party or user-controlled credentials, but this token is OpenClaw's own gateway secret. The issue is therefore in scope as a credential-disclosure bug, even under the one-user model, because the documented mitigation for shared hosts is separate OS-user boundaries and this install path weakens that boundary when the unit inherits default read permissions.

Impact

On Linux, openclaw node install copies the caller's OPENCLAW_GATEWAY_TOKEN into the generated openclaw-node.service user unit as an inline Environment= entry. A same-host user or process that can read that unit recovers a bearer secret that OpenClaw accepts as full gateway operator authentication.

Affected Component

Files: openclaw/src/daemon/service-env.ts:421-449, openclaw/src/commands/node-daemon-install-helpers.ts:19-68, openclaw/src/cli/node-cli/daemon.ts:131-167, openclaw/src/daemon/systemd.ts:560-622, openclaw/src/daemon/systemd-unit.ts:20-35, openclaw/src/gateway/auth.ts:350-363

const gatewayToken = normalizeOptionalString(env.OPENCLAW_GATEWAY_TOKEN);
return {
  ...buildCommonServiceEnvironment(env, sharedEnv),
  OPENCLAW_GATEWAY_TOKEN: gatewayToken,
  OPENCLAW_ALLOW_INSECURE_PRIVATE_WS: allowInsecurePrivateWs,
  OPENCLAW_SYSTEMD_UNIT: resolveNodeSystemdServiceName(),
  OPENCLAW_SERVICE_KIND: NODE_SERVICE_KIND,
};

return entries.map(([key, value]) => {
  const rawValue = value ?? "";
  return `Environment=${systemdEscapeArg(`${key}=${rawValue.trim()}`)}`;
});

const unit = buildSystemdUnit({
  programArguments,
  workingDirectory,
  environment: environmentSansDotEnvEntries,
  environmentFiles: environmentFileResult.environmentFiles,
});
await fs.writeFile(unitPath, unit, "utf8");

if (!safeEqualSecret(params.connectToken, params.authToken)) {
  return { ok: false, reason: "token_mismatch" };
}
return { ok: true, method: "token" };

Technical Reproduction

  1. On a Linux host with user systemd enabled, export a real gateway token into the shell that will run the installer: OPENCLAW_GATEWAY_TOKEN=<gateway-secret>.
  2. Run openclaw node install --host 127.0.0.1 --port 18789. runNodeDaemonInstall() passes process.env to buildNodeInstallPlan(), which calls buildNodeServiceEnvironment() and returns an environment object containing OPENCLAW_GATEWAY_TOKEN.
  3. resolveNodeService().install() forwards that environment into the shared systemd installer. writeSystemdUnit() keeps the node token in environmentSansDotEnvEntries, buildSystemdUnit() renders Environment=OPENCLAW_GATEWAY_TOKEN=<token>, and fs.writeFile(unitPath, unit, "utf8") writes the resulting unit to ~/.config/systemd/user/openclaw-node.service without an explicit restrictive mode.
  4. From another same-host account or process that can read the target user's systemd unit path, read ~/.config/systemd/user/openclaw-node.service and extract the inline token.
  5. Present that token to any gateway HTTP or WebSocket client. authorizeTokenAuth() compares it with the configured gateway token and returns { ok: true, method: "token" }, granting operator access.

Demonstrated Impact

The vulnerable path is specific to the Linux node-daemon installer. buildNodeServiceEnvironment() always copies OPENCLAW_GATEWAY_TOKEN into the generated service environment, buildNodeInstallPlan() returns only environment (not environmentValueSources), and runNodeDaemonInstall() passes that inline environment directly into service.install(). In the shared systemd writer, writeSystemdGatewayEnvironmentFile() only emits a protected 0600 env file for values sourced from the state-dir dotenv or an existing environment file; the node token never enters that path, so environmentFileResult.environmentFiles stays empty for the token and buildSystemdUnit() serializes it inline as Environment=OPENCLAW_GATEWAY_TOKEN=.... The resulting user unit is written with fs.writeFile(unitPath, unit, "utf8") and no follow-up chmod, so visibility falls back to the caller's directory and umask defaults instead of an owner-only policy. A reader that extracts the token bypasses pairing and any narrower device identity checks because the gateway auth layer accepts the shared bearer token as full operator access.

The same claim does not hold for the current macOS launchd path. prepareLaunchAgentProgramArguments() writes launchd environment values to ~/.openclaw/service-env/<label>.env with mode 0600, stores the wrapper script with mode 0700, and rewrites the plist to reference that wrapper instead of embedding the token inline. The verified remaining exposure is therefore the Linux user-systemd node install flow.

Environment

Verified against OpenClaw release v2026.5.4 (published 2026-05-05T08:24:01Z) and current upstream source commit 7188e4f4ad87a51a11d3dc3c7909fd79ea01d6e9 on the Linux node-daemon install path: openclaw/src/cli/node-cli/daemon.tsopenclaw/src/commands/node-daemon-install-helpers.tsopenclaw/src/daemon/service-env.tsopenclaw/src/daemon/systemd.ts / openclaw/src/daemon/systemd-unit.tsopenclaw/src/gateway/auth.ts. Scope was calibrated against the macOS launchd implementation in openclaw/src/daemon/launchd.ts, which already moves service environment variables into owner-only files.

Remediation Advice

Handle node-daemon gateway credentials with the same owner-only secret-file pattern already used by the launchd installer and by the Linux gateway installer for file-backed environment values. Carry environmentValueSources through the node install plan, mark OPENCLAW_GATEWAY_TOKEN as a managed secret that must not remain inline in the unit, and enforce owner-only permissions for any service artifact that still needs to reference credential material.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions