openclaw - ✅(Solved) Fix [Bug]: Linux node daemon install inlines gateway token into user systemd unit [1 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#78043Fetched 2026-05-06 06:17:36
View on GitHub
Comments
1
Participants
2
Timeline
6
Reactions
2
Author
Timeline (top)
mentioned ×2subscribed ×2commented ×1cross-referenced ×1

Root Cause

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

Fix Action

Fix / Workaround

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

PR fix notes

PR #78044: fix: keep node systemd gateway tokens out of units

Description (problem / solution / changelog)

Summary

  • Keep the Linux node daemon gateway token out of generated user systemd unit Environment= lines.
  • Carry node install environment source metadata through the install path so OPENCLAW_GATEWAY_TOKEN is written to the existing owner-only service env file.
  • Sanitize existing vulnerable inline token entries out of .bak unit backups during re-stage or upgrade.

Linked Issue

  • Closes #78043

Security Impact

  • Secrets/tokens handling changed: Yes.
  • New permissions/capabilities: No.
  • Data access scope changed: No.

The change moves the node daemon gateway token from a readable unit-file inline environment entry into the existing 0600 service env file path. Non-secret service environment values remain inline, and stale managed token values are removed when the current install omits them.

Tests

  • OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test src/commands/node-daemon-install-helpers.test.ts src/daemon/systemd.test.ts
  • pnpm exec oxfmt --check --threads=1 src/commands/node-daemon-install-helpers.ts src/commands/node-daemon-install-helpers.test.ts src/cli/node-cli/daemon.ts src/daemon/node-service.ts src/daemon/systemd.ts src/daemon/systemd.test.ts docs/cli/node.md CHANGELOG.md
  • Stage 4 gate: pnpm exec oxlint src/ && pnpm build && pnpm check && pnpm test

Stage 4 completed with passed_with_baseline_failures: pnpm build and pnpm check passed; lint and test failures were classified as pre-existing baseline failures with 0 new failures.

Notes

  • The branch was rebased and tested against current upstream source commit 7188e4f4ad87a51a11d3dc3c7909fd79ea01d6e9.
  • The submitted issue was refreshed against release v2026.5.4 before publish.

Changed files

  • CHANGELOG.md (modified, +1/-0)
  • docs/cli/node.md (modified, +2/-0)
  • src/cli/node-cli/daemon.ts (modified, +2/-1)
  • src/commands/node-daemon-install-helpers.test.ts (modified, +31/-0)
  • src/commands/node-daemon-install-helpers.ts (modified, +19/-1)
  • src/daemon/node-service.ts (modified, +3/-0)
  • src/daemon/systemd.test.ts (modified, +102/-0)
  • src/daemon/systemd.ts (modified, +77/-4)

Code Example

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" };
RAW_BUFFERClick to expand / collapse

Severity Assessment

CVSS Assessment

Metricv3.1v4.0
Score7.8 / 10.08.5 / 10.0
SeverityHighHigh
VectorCVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:HCVSS:4.0/AV:L/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N
CalculatorCVSS v3.1 CalculatorCVSS 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.

<!-- submission-marker:CR-csb-node-daemon-service-token-leak -->

extent analysis

TL;DR

To fix the credential-disclosure bug, handle node-daemon gateway credentials with an owner-only secret-file pattern and enforce owner-only permissions for service artifacts referencing credential material.

Guidance

  1. Modify the Linux node-daemon installer to store the OPENCLAW_GATEWAY_TOKEN in a separate, owner-only file instead of embedding it inline in the systemd unit.
  2. Carry environmentValueSources through the node install plan to track the origin of environment variables and mark sensitive values like OPENCLAW_GATEWAY_TOKEN as managed secrets.
  3. Enforce owner-only permissions for the generated systemd unit and any other service artifacts that reference credential material.
  4. Update the buildSystemdUnit function to exclude sensitive environment variables from the inline Environment entries and instead reference the owner-only files containing these secrets.

Example

// Instead of directly embedding the token in the unit
const unit = buildSystemdUnit({
  // ...
  environment: environmentSansDotEnvEntries,
  // ...
});

// Use an owner-only file for the token
const tokenFile = '/path/to/token/file';
fs.writeFile(tokenFile, gatewayToken, { mode: 0o600 });
const unit = buildSystemdUnit({
  // ...
  environmentFiles: [tokenFile],
  // ...
});

Notes

This fix assumes that the buildSystemdUnit function can be modified to support referencing environment files instead of inline environment variables. Additionally, the owner-only file pattern should be consistent with the existing launchd implementation.

Recommendation

Apply the workaround by modifying the Linux node-daemon installer to use an owner-only secret-file pattern for handling node-daemon gateway credentials. This approach aligns with the existing security practices in the launchd implementation and mitigates the credential-disclosure bug.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix [Bug]: Linux node daemon install inlines gateway token into user systemd unit [1 pull requests, 1 comments, 2 participants]