Skip to content

Commit 3949a9f

Browse files
committed
fix(node): sanitize systemd backup secrets
Remove file-backed managed systemd environment keys from .bak units during restage so upgrades from inline-token units do not preserve leaked gateway tokens. Add regression coverage for restaging over a vulnerable unit while preserving unrelated environment entries.
1 parent 59aabf6 commit 3949a9f

2 files changed

Lines changed: 71 additions & 5 deletions

File tree

src/daemon/systemd.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -877,6 +877,47 @@ describe("stageSystemdService", () => {
877877
});
878878
});
879879

880+
it("sanitizes file-backed managed secrets out of the backup unit on re-stage", async () => {
881+
await withStageFixture(async ({ env, unitPath }) => {
882+
await fs.mkdir(path.dirname(unitPath), { recursive: true });
883+
await fs.writeFile(
884+
unitPath,
885+
[
886+
"[Service]",
887+
"ExecStart=/usr/bin/openclaw node run",
888+
"Environment=OPENCLAW_GATEWAY_TOKEN=inline-token",
889+
"Environment=OPENCLAW_GATEWAY_PORT=18789",
890+
].join("\n"),
891+
"utf8",
892+
);
893+
894+
mockSystemctlStatusOk();
895+
896+
await stageSystemdService({
897+
env,
898+
stdout: { write: vi.fn() } as unknown as NodeJS.WritableStream,
899+
programArguments: ["/usr/bin/openclaw", "node", "run"],
900+
workingDirectory: "/tmp",
901+
environment: {
902+
OPENCLAW_GATEWAY_TOKEN: "fresh-token",
903+
OPENCLAW_GATEWAY_PORT: "18789",
904+
},
905+
environmentValueSources: {
906+
OPENCLAW_GATEWAY_TOKEN: "file",
907+
},
908+
});
909+
910+
const [unit, backupUnit] = await Promise.all([
911+
fs.readFile(unitPath, "utf8"),
912+
fs.readFile(`${unitPath}.bak`, "utf8"),
913+
]);
914+
915+
expect(unit).not.toContain("Environment=OPENCLAW_GATEWAY_TOKEN=fresh-token");
916+
expect(backupUnit).not.toContain("Environment=OPENCLAW_GATEWAY_TOKEN=inline-token");
917+
expect(backupUnit).toContain("Environment=OPENCLAW_GATEWAY_PORT=18789");
918+
});
919+
});
920+
880921
it("clears stale inline-managed keys from env file on re-stage (#76860)", async () => {
881922
await withStageFixture(async ({ env, stateDir, unitPath, envFilePath }) => {
882923
// Existing env file carries a stale OPENCLAW_GATEWAY_TOKEN that the

src/daemon/systemd.ts

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,27 @@ function collectSystemdFileBackedEnvironment(params: {
231231
return environment;
232232
}
233233

234+
function sanitizeSystemdUnitBackupContent(params: {
235+
content: string;
236+
fileManagedKeys: ReadonlySet<string>;
237+
}): string {
238+
if (params.fileManagedKeys.size === 0) {
239+
return params.content;
240+
}
241+
return params.content
242+
.split("\n")
243+
.filter((rawLine) => {
244+
const line = rawLine.trim();
245+
if (!line.startsWith("Environment=")) {
246+
return true;
247+
}
248+
const parsed = parseSystemdEnvAssignment(line.slice("Environment=".length).trim());
249+
const key = parsed ? normalizeSystemdEnvironmentKey(parsed.key) : null;
250+
return !key || !params.fileManagedKeys.has(key);
251+
})
252+
.join("\n");
253+
}
254+
234255
function expandSystemdSpecifier(input: string, env: GatewayServiceEnv): string {
235256
// Support the common unit-specifier used in user services.
236257
return input.replaceAll("%h", toPosixPath(resolveHomeDir(env)));
@@ -593,13 +614,20 @@ async function writeSystemdUnit({
593614

594615
const unitPath = resolveSystemdUnitPath(env);
595616
await fs.mkdir(path.dirname(unitPath), { recursive: true });
617+
const fileManagedKeys = collectSystemdFileManagedKeys({
618+
environmentValueSources,
619+
});
596620

597621
// Preserve user customizations: back up existing unit file before overwriting.
598622
let backedUp = false;
599623
try {
600-
await fs.access(unitPath);
601624
const backupPath = `${unitPath}.bak`;
602-
await fs.copyFile(unitPath, backupPath);
625+
const existingUnit = await fs.readFile(unitPath, "utf8");
626+
const backupUnit = sanitizeSystemdUnitBackupContent({
627+
content: existingUnit,
628+
fileManagedKeys,
629+
});
630+
await fs.writeFile(backupPath, backupUnit, "utf8");
603631
backedUp = true;
604632
} catch {
605633
// File does not exist yet — nothing to back up.
@@ -620,9 +648,6 @@ async function writeSystemdUnit({
620648
environment,
621649
environmentValueSources,
622650
});
623-
const fileManagedKeys = collectSystemdFileManagedKeys({
624-
environmentValueSources,
625-
});
626651
const environmentFileResult = await writeSystemdGatewayEnvironmentFile({
627652
stateDir,
628653
dotenvVars: stateDirDotEnvVars,

0 commit comments

Comments
 (0)