Skip to content

fix #95895: [Security]: GEMINI_API_KEY written as a plaintext literal in the generated systemd service unit#96149

Closed
mushuiyu886 wants to merge 2 commits into
openclaw:mainfrom
mushuiyu886:feat/issue-95895
Closed

fix #95895: [Security]: GEMINI_API_KEY written as a plaintext literal in the generated systemd service unit#96149
mushuiyu886 wants to merge 2 commits into
openclaw:mainfrom
mushuiyu886:feat/issue-95895

Conversation

@mushuiyu886

@mushuiyu886 mushuiyu886 commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Mark Google/Gemini auth-profile env refs as systemd file-backed service env values.
  • Preserve the current main config SecretRef behavior where GEMINI_API_KEY is managed through OPENCLAW_SERVICE_MANAGED_ENV_KEYS and delivered by the generated EnvironmentFile.
  • Add a regression test covering the combined config SecretRef plus auth-profile path on Linux.

What Problem This Solves

Current main already keeps Google/Gemini config SecretRefs out of inline systemd unit assignments by treating those config keys as managed service env keys. The remaining gap is auth-profile env refs: Google/Gemini auth-profile keys still used the default inline service env value source, so the generated systemd unit could render plaintext Environment=GOOGLE_API_KEY=... or Environment=GEMINI_API_KEY=... assignments for those auth-profile credentials.

The fix marks known Google/Gemini auth-profile env-ref keys as environmentValueSources: "file". The existing systemd staging contract then writes those values to the generated owner-only EnvironmentFile and omits plaintext assignments from the unit file.

User Impact

Users who install the gateway as a systemd service with Google/Gemini credentials supplied through auth-profile env refs no longer get those keys embedded in the generated unit file. The service still receives the credentials at runtime via the generated EnvironmentFile.

  • Why it matters / User impact: The generated unit is easier to inspect, back up, and diagnose without exposing Google/Gemini credential material in plaintext Environment= lines.

Origin / follow-up

Fixes #95895.

This is an existing PR updated after syncing with current main. The current main branch already covers the config SecretRef managed-key portion; this update keeps that behavior and narrows the remaining fix to Google/Gemini auth-profile env refs.

Competition / linked PR analysis

Linked PRs #95908, #96059, and #96065 also target #95895. After syncing with main, #96065's config SecretRef managed-key behavior is already present on the base branch.

This PR keeps the current main config SecretRef result: GEMINI_API_KEY remains represented in OPENCLAW_SERVICE_MANAGED_ENV_KEYS and is still delivered by the generated EnvironmentFile. It adds the missing auth-profile value-source metadata so Google/Gemini auth-profile keys also go through the generated EnvironmentFile instead of inline unit Environment= values.

Real behavior proof

  • Behavior or issue addressed: Google/Gemini env-ref secrets from auth profiles must not be written as plaintext Environment=GEMINI_API_KEY=... or Environment=GOOGLE_API_KEY=... entries in the generated systemd unit, while still being delivered to the service. The proof also verifies that current main's config SecretRef managed-key behavior remains intact.
  • Real environment tested: Linux worktree using the production buildGatewayInstallPlan and stageSystemdService paths with a temporary HOME/state dir. A fake systemctl binary returned success for the local availability gate; OpenClaw's production systemd staging code produced the unit file and EnvironmentFile.
  • Exact steps or command run after this patch:
cd /media/vdb/code/ai/aispace/openclaw-worktrees/pr-96149
PROOF_HEAD=$(git rev-parse HEAD) ./node_modules/.bin/tsx /media/vdb/code/ai/aispace/openclaw-issue-95895-evidence/gemini-managed-env-proof.mts
node scripts/run-vitest.mjs src/commands/daemon-install-helpers.test.ts
./node_modules/.bin/oxfmt --check src/commands/daemon-install-helpers.ts src/commands/daemon-install-helpers.test.ts
node scripts/run-oxlint.mjs --tsconfig config/tsconfig/oxlint.core.json src/commands/daemon-install-helpers.ts src/commands/daemon-install-helpers.test.ts
node scripts/run-tsgo.mjs -p test/tsconfig/tsconfig.core.test.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/core-test-after-sync.tsbuildinfo
node scripts/run-tsgo.mjs -p test/tsconfig/tsconfig.extensions.test.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/extensions-test-after-sync.tsbuildinfo
git diff --check
  • Evidence after fix:
HEAD=fe757649357653498548269ec709ccfa3e551656
managed=GEMINI_API_KEY
plan source GEMINI_API_KEY=file
plan source GOOGLE_API_KEY=file
unit contains proof-gemini-key=false
unit contains proof-google-key=false
unit has Environment=GEMINI_API_KEY=false
unit has Environment=GOOGLE_API_KEY=false
unit has generated EnvironmentFile=true
env file contains GEMINI_API_KEY=true
env file contains GOOGLE_API_KEY=true
env file mode=600
PASS: Gemini/Google env refs are delivered through the generated 0600 systemd EnvironmentFile, not inline unit Environment values.
Test Files  1 passed (1)
     Tests  49 passed (49)
[test] passed 1 Vitest shard in 52.57s
Checking formatting...

All matched files use the correct format.
Finished in 86ms on 2 files using 8 threads.
node scripts/run-oxlint.mjs --tsconfig config/tsconfig/oxlint.core.json src/commands/daemon-install-helpers.ts src/commands/daemon-install-helpers.test.ts
# exit 0, no diagnostics
node scripts/run-tsgo.mjs -p test/tsconfig/tsconfig.core.test.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/core-test-after-sync.tsbuildinfo
# exit 0, no diagnostics
node scripts/run-tsgo.mjs -p test/tsconfig/tsconfig.extensions.test.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/extensions-test-after-sync.tsbuildinfo
# exit 0, no diagnostics
git diff --check
# exit 0, no whitespace errors
  • Observed result after fix: The staged systemd unit has no inline Google/Gemini secret assignments or secret literals; the generated EnvironmentFile is referenced by the unit, contains both runtime service env values, and has mode 0600. The install plan keeps current main's OPENCLAW_SERVICE_MANAGED_ENV_KEYS=GEMINI_API_KEY result for the config SecretRef path, and marks both GEMINI_API_KEY and GOOGLE_API_KEY as file in environmentValueSources.
  • What was not tested: Out of scope for this patch: restarting an already installed user service and calling the external Google/Gemini API. The tested boundary is the local systemd service staging/file-generation path that writes the unit and EnvironmentFile.
  • Fix classification: Root cause fix

Merge risk

  • Risk labels considered: security-boundary, auth-provider, public-config, compatibility
  • Risk explanation: The changed contract surface is generated daemon service environment metadata for Google/Gemini auth-profile env-ref keys. The source of truth remains the install-plan environmentValueSources metadata that systemd staging already uses for file-backed service env values.
  • Why acceptable: The diff only changes value-source selection for known Google/Gemini auth-profile env key names. Existing current-main behavior for config SecretRefs, exec passEnv, durable .env, generated service env defaults, and systemd EnvironmentFile rendering is preserved.

Review findings addressed

  • RF-001: Maintainer/security decision boundary documented. This PR intentionally moves known Google/Gemini auth-profile credentials from inline systemd Environment= assignments into the existing generated 0600 EnvironmentFile path; current-head proof stages the unit and EnvironmentFile to show runtime delivery is preserved without inline plaintext.
  • RF-002: Scope boundary documented. The file-backed auth-profile policy is limited to the known Google/Gemini credential names GEMINI_API_KEY, GEMINI_API_KEYS, GEMINI_API_KEY_0, and GOOGLE_API_KEY; a generic all-provider auth-profile service-env policy remains out of scope unless maintainers request that broader direction.
  • RF-003: Manual-review boundary documented. The remaining auth-profile credential-rendering choice is security and compatibility sensitive, so the PR body calls out the canonical EnvironmentFile contract, unchanged paths, and exact current-head real behavior proof for maintainer/security review.
  • Sync note: The branch has been synced onto current main, preserving fix(install): manage config-secretref env refs via OPENCLAW_SERVICE_MANAGED_ENV_KEYS #96065's config SecretRef managed-key behavior and resolving the conflict in src/commands/daemon-install-helpers.ts. The current diff only adds the remaining auth-profile value-source behavior plus its regression test.
  • CI/type note: The Google provider fixture includes the required baseUrl; direct current-head run-tsgo.mjs core and extensions test type checks pass locally.

Regression Test Plan

  • Target test file: src/commands/daemon-install-helpers.test.ts
  • Scenario locked in: A gateway install plan receives GEMINI_API_KEY from a config SecretRef and GOOGLE_API_KEY from an auth-profile env ref; both must keep their values in the plan and be marked environmentValueSources: "file", while current main's OPENCLAW_SERVICE_MANAGED_ENV_KEYS=GEMINI_API_KEY behavior remains intact.
  • Why this is the smallest reliable guardrail: The regression locks the install-plan source metadata because systemd staging uses that metadata to decide whether a value is rendered inline or through the generated EnvironmentFile.
  • node scripts/run-vitest.mjs src/commands/daemon-install-helpers.test.ts
  • ./node_modules/.bin/oxfmt --check src/commands/daemon-install-helpers.ts src/commands/daemon-install-helpers.test.ts
  • node scripts/run-oxlint.mjs --tsconfig config/tsconfig/oxlint.core.json src/commands/daemon-install-helpers.ts src/commands/daemon-install-helpers.test.ts
  • node scripts/run-tsgo.mjs -p test/tsconfig/tsconfig.core.test.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/core-test-after-sync.tsbuildinfo
  • node scripts/run-tsgo.mjs -p test/tsconfig/tsconfig.extensions.test.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/extensions-test-after-sync.tsbuildinfo
  • git diff --check
  • PROOF_HEAD=$(git rev-parse HEAD) ./node_modules/.bin/tsx /media/vdb/code/ai/aispace/openclaw-issue-95895-evidence/gemini-managed-env-proof.mts

Risk / Compatibility

Risk is limited to Google/Gemini auth-profile env-ref service rendering. Other provider auth-profile env refs keep the existing inline behavior, and current-main config SecretRef managed-key behavior is unchanged.

  • What did NOT change: The patch does not change generic config SecretRef behavior, non-Google/Gemini auth-profile env-ref behavior, exec passEnv, generated service env defaults, or renderer behavior outside these Google/Gemini env key names.
  • Architecture / source-of-truth check: buildGatewayInstallPlan is the source-of-truth contract for service env values and their environmentValueSources; systemd staging already treats file sources as generated EnvironmentFile values.
  • Patch quality notes: This is not a fallback or string scrubber in the renderer. The change updates the owner metadata before rendering, so every downstream systemd staging path gets the same file-backed contract.
  • Maintainer-ready confidence: High for the file-generation boundary tested here: the proof runs the production install-plan and systemd staging code, verifies no unit plaintext assignment, verifies EnvironmentFile delivery, checks mode 0600, and includes the synced target tests, formatting check, lint check, type-check shard, and whitespace check.

The generated systemd service still receives the Google/Gemini values because the change uses the existing file-backed EnvironmentFile path rather than dropping the values from the plan.

What was not changed

  • Did not change current main's config SecretRef managed-key behavior.
  • Did not change non-Google/Gemini auth-profile env-ref behavior.
  • Did not change exec passEnv handling.
  • Did not change the systemd renderer, EnvironmentFile writer, launchd renderer, or scheduled-task renderer.
  • Did not add repro/proof files to the repository.

Root Cause

  • Root cause: Google/Gemini auth-profile env refs used the default inline service env value source, so systemd rendering treated them as unit Environment= assignments.
  • Why this is root-cause fix: The service env plan is the source of truth for how systemd should render each service env value. Marking these Google/Gemini auth-profile env-ref values as file-backed routes them through the existing generated EnvironmentFile path, which removes plaintext unit assignments while preserving runtime delivery.

@clawsweeper

clawsweeper Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed July 6, 2026, 2:39 AM ET / 06:39 UTC.

Summary
The PR marks selected Google/Gemini auth-profile environment refs as file-backed service environment values and adds a daemon install regression test for the combined config SecretRef plus auth-profile path.

PR surface: Source +17, Tests +47. Total +64 across 2 files.

Reproducibility: yes. Source inspection on current main shows auth-profile env refs enter the install plan with inline value-source metadata, while systemd renders file-only value sources through the generated EnvironmentFile.

Review metrics: 1 noteworthy metric.

  • Credential Delivery Surface: 1 auth-profile source path changed; 4 Google/Gemini key names allowlisted. The PR changes how selected provider credentials are rendered into installed service environments, which is security and upgrade sensitive.

Root-cause cluster
Relationship: fixed_by_candidate
Canonical: #95895
Summary: The canonical issue is the plaintext Google/Gemini generated service secret exposure; this PR is the current open candidate for the remaining auth-profile path after the config SecretRef side merged.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Merge readiness
Overall: 🐚 platinum hermit
Proof: 🦞 diamond lobster
Patch quality: 🐚 platinum hermit
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • none.

Risk before merge

  • [P1] Merging intentionally changes how selected Google/Gemini auth-profile credentials are delivered to generated service environments, so maintainers should accept the EnvironmentFile credential boundary explicitly.
  • [P1] The patch is a provider-specific allowlist for four Google/Gemini env names; if maintainers want all auth-profile env refs handled by a generic policy, this is intentionally narrower than that direction.

Maintainer options:

  1. Accept The EnvironmentFile Credential Boundary (recommended)
    Approve the narrowed Google/Gemini auth-profile policy and merge after exact-head checks remain green.
  2. Broaden The Policy Before Merge
    Request a patch that moves the auth-profile value-source decision into a generic provider/auth-profile policy if the allowlist is too narrow.
  3. Pause For Separate Security Work
    Close or pause this PR only if maintainers decide the remaining auth-profile path should be owned by a different canonical security change.

Next step before merge

  • [P2] Manual review is the right lane because the patch has no narrow repair blocker, but the auth-provider credential delivery change needs maintainer/security acceptance before merge.

Maintainer decision needed

  • Question: Should OpenClaw accept the narrowed policy that makes Google/Gemini auth-profile env refs file-backed in generated service environments while leaving other auth-profile env refs unchanged?
  • Rationale: The mechanics are source-backed and proven, but CI cannot decide whether the provider-specific credential-delivery boundary is the intended security and compatibility policy.
  • Likely owner: obviyus — obviyus recently authored and landed the current-main config SecretRef half of this exact service-env security cluster.
  • Options:
    • Accept Narrow File-Backed Policy (recommended): Merge this PR once maintainers agree the existing EnvironmentFile contract is the right boundary for Google/Gemini auth-profile keys.
    • Request Generic Auth-Profile Policy: Ask for a revised PR if all provider auth-profile env refs should share one common file-backed policy instead of this allowlist.
    • Keep Auth-Profile Rendering Separate: Pause or close this PR if maintainers want the linked issue limited to config SecretRefs and separate auth-profile work later.

Security
Cleared: No supply-chain or new secret-exposure defect was found in the diff; the remaining security concern is maintainer acceptance of the credential-delivery boundary.

Review details

Best possible solution:

Land this narrowed PR after maintainer/security review accepts the Google/Gemini auth-profile EnvironmentFile policy and exact-head gates remain green.

Do we have a high-confidence way to reproduce the issue?

Yes. Source inspection on current main shows auth-profile env refs enter the install plan with inline value-source metadata, while systemd renders file-only value sources through the generated EnvironmentFile.

Is this the best way to solve the issue?

Yes, if maintainers accept the narrow policy. The patch uses the existing service env value-source seam instead of scrubbing renderer output, and the obvious broader alternative is a maintainer policy choice rather than a mechanical fix.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 806397f0d92c.

Label changes

Label justifications:

  • P1: The PR targets plaintext provider credential exposure in a current gateway install workflow.
  • merge-risk: 🚨 compatibility: Existing generated service installs can see different credential rendering and backup sanitization behavior for selected Google/Gemini auth-profile env refs.
  • merge-risk: 🚨 auth-provider: The affected values are Google/Gemini provider authentication secrets used by auth profiles.
  • merge-risk: 🚨 security-boundary: The diff changes whether selected credentials are written inline or only through the generated owner-only EnvironmentFile.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (live_output): The PR body includes current-head live terminal proof from production install-plan and systemd staging code showing no inline Google/Gemini unit assignments, generated EnvironmentFile delivery, and mode 0600.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes current-head live terminal proof from production install-plan and systemd staging code showing no inline Google/Gemini unit assignments, generated EnvironmentFile delivery, and mode 0600.
Evidence reviewed

PR surface:

Source +17, Tests +47. Total +64 across 2 files.

View PR surface stats
Area Files Added Removed Net
Source 1 18 1 +17
Tests 1 47 0 +47
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 2 65 1 +64

What I checked:

  • Repository policy applied: Root AGENTS.md was read fully; its compatibility-sensitive guidance for provider/auth, config/env, setup, startup, and security-boundary changes applies to this daemon install review. (AGENTS.md:35, 806397f0d92c)
  • Current main auth-profile env refs are still inline by default: Current main collects auth-profile env refs into authProfileEnvironment and adds them to the service env plan without a valueSource override, so addServiceEnvPlanEntries defaults them to inline. (src/commands/daemon-install-helpers.ts:665, 806397f0d92c)
  • Inline is the service-env-plan default: addServiceEnvPlanEntries records valueSource ?? "inline", which is why the existing auth-profile path can render as inline unless this PR supplies file metadata. (src/daemon/service-env-plan.ts:54, 806397f0d92c)
  • Systemd file-backed contract is already present: Systemd staging collects file-only value sources, writes those values to the generated EnvironmentFile, and omits file-provided keys from inline Environment= entries. (src/daemon/systemd.ts:864, 806397f0d92c)
  • PR head uses the existing value-source seam: The diff adds a four-name Google/Gemini allowlist and passes a valueSource callback for authProfileEnvironment instead of changing the systemd renderer. (src/commands/daemon-install-helpers.ts:677, fe7576493576)
  • Regression coverage targets the remaining path: The added test covers a Linux plan with GEMINI_API_KEY from config SecretRef and GOOGLE_API_KEY from an auth profile, expecting both values to be file-backed while preserving the managed-key config SecretRef behavior. (src/commands/daemon-install-helpers.test.ts:1770, fe7576493576)

Likely related people:

  • steipete: Peter Steinberger history includes the auth-profile daemon install env-ref persistence and envfile provenance work that this PR builds on. (role: recent area contributor; confidence: high; commits: ba9fb4d994d6, a2cb80b9c4be, 2b01bcf6c805; files: src/commands/daemon-install-helpers.ts, src/daemon/service-env-render-policy.ts, src/daemon/service-managed-env.ts)
  • obviyus: obviyus authored the current-main config SecretRef render-source follow-up and merged the related PR that fixed the other half of the same issue cluster. (role: recent adjacent owner; confidence: high; commits: af0cf4ee653a, 01a607bfc102, e79865569c50; files: src/commands/daemon-install-helpers.ts, src/daemon/service-env-render-policy.ts, src/commands/daemon-install-helpers.test.ts)
  • vincentkoc: Recent daemon and systemd service history includes Vincent Koc work on the same service install/runtime surface. (role: daemon/systemd adjacent contributor; confidence: medium; commits: 64785823d093, b258c3fc6578, d7c173b69456; files: src/daemon/systemd.ts, src/commands/daemon-install-helpers.ts)
  • hclsys: HCL authored the systemd env-file preservation work directly adjacent to the EnvironmentFile contract used by this PR. (role: systemd env-file contributor; confidence: medium; commits: f8f881f63fab; files: src/commands/daemon-install-helpers.ts, src/daemon/systemd.ts, src/daemon/service-managed-env.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.
Review history (3 earlier review cycles)
  • reviewed June 23, 2026, 9:23 PM ET / 01:23 UTC sha 80033c6 :: needs maintainer review before merge. :: none
  • reviewed 2026-07-05T14:43:04.052Z sha 80033c6 :: needs maintainer review before merge. :: none
  • reviewed 2026-07-05T15:28:35.418Z sha a04b1fa :: needs maintainer review before merge. :: none

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. P1 High-priority user-facing bug, regression, or broken workflow. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. merge-risk: 🚨 auth-provider 🚨 May break OAuth, tokens, provider routing, model choice, or credentials. merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. labels Jun 24, 2026
@mushuiyu886

Copy link
Copy Markdown
Contributor Author

merge risk

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

commands Command implementations merge-risk: 🚨 auth-provider 🚨 May break OAuth, tokens, provider routing, model choice, or credentials. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. P1 High-priority user-facing bug, regression, or broken workflow. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. size: S status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Security]: GEMINI_API_KEY written as a plaintext literal in the generated systemd service unit

1 participant