fix: check and enforce 0o600 on SFTP key files when mount fails, then retry

When sshfs fails, check permissions on the private key and known_hosts
files. If any have wrong permissions (not 0o600 — possible on Docker
bind-mounts with ACL inheritance such as Synology NAS), apply chmod 600
and retry the mount once with a warning log.

Add ensureFileMode helper to packages/core/src/node/fs.ts (exported from
node index) and three tests for it: correct mode (no-op), wrong mode
(fixes and returns true), non-existent file (no-op).

Agent-Logs-Url: https://github.com/mowdep/zerobyte/sessions/d5edc5d0-fa7d-424b-a763-97601f577c92

Co-authored-by: mowdep <10937987+mowdep@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-05-10 23:03:31 +00:00 committed by GitHub
parent 1b8d8e670d
commit de92e7499f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 83 additions and 4 deletions

View file

@ -4,7 +4,7 @@ import * as os from "node:os";
import * as path from "node:path";
import { spawn } from "node:child_process";
import type { BackendConfig } from "@zerobyte/contracts/volumes";
import { FILE_MODES, logger, writeFileWithMode } from "@zerobyte/core/node";
import { FILE_MODES, ensureFileMode, logger, writeFileWithMode } from "@zerobyte/core/node";
import { toMessage } from "@zerobyte/core/utils";
import { OPERATION_TIMEOUT, SSH_KEYS_DIR } from "../constants";
import { getMountForPath } from "../fs";
@ -142,7 +142,30 @@ const mount = async (config: BackendConfig, mountPath: string) => {
const args = [`${config.username}@${config.host}:${config.path || ""}`, mountPath, "-o", options.join(",")];
if (config.password) args.push("-o", "password_stdin");
logger.info(`Executing sshfs: sshfs ${args.join(" ")}`);
await runSshfs(args, config.password);
try {
await runSshfs(args, config.password);
} catch (sshfsError) {
// On some Docker bind-mount filesystems (e.g. Synology NAS) the chmod call
// inside writeFileWithMode may not have taken effect due to ACL inheritance.
// SSH refuses to use key files whose permissions are too open, so check and
// re-apply 0o600 on all sensitive files written above, then retry once.
const [keyFixed, knownHostsFixed] = await Promise.all([
config.privateKey
? ensureFileMode(getPrivateKeyPath(mountPath), FILE_MODES.ownerReadWrite)
: Promise.resolve(false),
config.knownHosts
? ensureFileMode(getKnownHostsPath(mountPath), FILE_MODES.ownerReadWrite)
: Promise.resolve(false),
]);
if (keyFixed || knownHostsFixed) {
logger.warn("SFTP mount failed and key file permissions were incorrect; permissions have been fixed. Retrying mount...");
await runSshfs(args, config.password);
} else {
throw sshfsError;
}
}
logger.info(`SFTP volume at ${mountPath} mounted successfully.`);
return { status: "mounted" as const };

View file

@ -2,7 +2,7 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, test } from "vitest";
import { FILE_MODES, writeFileWithMode } from "../fs";
import { FILE_MODES, ensureFileMode, writeFileWithMode } from "../fs";
const tempDirectories = new Set<string>();
@ -15,6 +15,44 @@ afterEach(async () => {
tempDirectories.clear();
});
describe("ensureFileMode", () => {
test("returns false and leaves the file unchanged when mode is already correct", async () => {
const tempDirectory = await fs.mkdtemp(path.join(os.tmpdir(), "zerobyte-ensure-file-mode-"));
tempDirectories.add(tempDirectory);
const filePath = path.join(tempDirectory, "identity");
await fs.writeFile(filePath, "content");
await fs.chmod(filePath, 0o600);
const fixed = await ensureFileMode(filePath, FILE_MODES.ownerReadWrite);
expect(fixed).toBe(false);
expect((await fs.stat(filePath)).mode & 0o777).toBe(0o600);
});
test("returns true and applies the correct mode when permissions are wrong", async () => {
const tempDirectory = await fs.mkdtemp(path.join(os.tmpdir(), "zerobyte-ensure-file-mode-"));
tempDirectories.add(tempDirectory);
const filePath = path.join(tempDirectory, "identity");
await fs.writeFile(filePath, "content");
await fs.chmod(filePath, 0o755);
const fixed = await ensureFileMode(filePath, FILE_MODES.ownerReadWrite);
expect(fixed).toBe(true);
expect((await fs.stat(filePath)).mode & 0o777).toBe(0o600);
});
test("returns false when the file does not exist", async () => {
const tempDirectory = await fs.mkdtemp(path.join(os.tmpdir(), "zerobyte-ensure-file-mode-"));
tempDirectories.add(tempDirectory);
const fixed = await ensureFileMode(path.join(tempDirectory, "nonexistent.key"), FILE_MODES.ownerReadWrite);
expect(fixed).toBe(false);
});
});
describe("writeFileWithMode", () => {
test("applies the requested mode when creating a new file", async () => {
const tempDirectory = await fs.mkdtemp(path.join(os.tmpdir(), "zerobyte-write-file-with-mode-"));

View file

@ -6,6 +6,24 @@ export const FILE_MODES = {
type FileMode = (typeof FILE_MODES)[keyof typeof FILE_MODES];
/**
* Checks whether an existing file has the expected mode and, if not, applies chmod.
* Returns true when the mode was corrected, false when the file already had the
* correct mode or does not exist.
*/
export const ensureFileMode = async (filePath: string, mode: FileMode): Promise<boolean> => {
try {
const stat = await fs.stat(filePath);
if ((stat.mode & 0o777) !== mode) {
await fs.chmod(filePath, mode);
return true;
}
} catch {
// File does not exist or cannot be stat'd — nothing to fix.
}
return false;
};
export const writeFileWithMode = async (filePath: string, data: string, mode: FileMode) => {
// Remove any existing file first so the mode option on writeFile is always applied
// on a fresh file creation (mode is ignored for existing files). This also avoids

View file

@ -2,4 +2,4 @@ export { safeSpawn, safeExec } from "./spawn.js";
export type { SafeSpawnParams, SafeSpawnParamsLines, SafeSpawnParamsRaw, SpawnResult } from "./spawn.js";
export { logger } from "./logger.js";
export { sanitizeSensitiveData } from "../utils/sanitize.js";
export { FILE_MODES, writeFileWithMode } from "./fs.js";
export { FILE_MODES, writeFileWithMode, ensureFileMode } from "./fs.js";