fix: add additional chmod after file write
Closes #478 The mode option of fs.writeFile does not reliably apply permissions on all filesystems. On Synology NAS Docker volumes (btrfs), the file ends up with 0755 permissions instead of 0600, causing SSH to refuse the key.
This commit is contained in:
parent
a1b2d97dbc
commit
b132ad6df5
5 changed files with 55 additions and 17 deletions
|
|
@ -6,6 +6,7 @@ import { OPERATION_TIMEOUT } from "../../../core/constants";
|
|||
import { cryptoUtils } from "../../../utils/crypto";
|
||||
import { toMessage } from "../../../utils/errors";
|
||||
import { logger } from "@zerobyte/core/node";
|
||||
import { FILE_MODES, writeFileWithMode } from "@zerobyte/core/utils";
|
||||
import { getMountForPath } from "../../../utils/mountinfo";
|
||||
import { withTimeout } from "../../../utils/timeout";
|
||||
import type { VolumeBackend } from "../backend";
|
||||
|
|
@ -65,7 +66,7 @@ const mount = async (config: BackendConfig, mountPath: string) => {
|
|||
options.push("StrictHostKeyChecking=no", "UserKnownHostsFile=/dev/null");
|
||||
} else if (config.knownHosts) {
|
||||
const knownHostsPath = getKnownHostsPath(mountPath);
|
||||
await fs.writeFile(knownHostsPath, config.knownHosts, { mode: 0o600 });
|
||||
await writeFileWithMode(knownHostsPath, config.knownHosts, FILE_MODES.ownerReadWrite);
|
||||
options.push(`UserKnownHostsFile=${knownHostsPath}`, "StrictHostKeyChecking=yes");
|
||||
}
|
||||
|
||||
|
|
@ -84,7 +85,7 @@ const mount = async (config: BackendConfig, mountPath: string) => {
|
|||
if (!normalizedKey.endsWith("\n")) {
|
||||
normalizedKey += "\n";
|
||||
}
|
||||
await fs.writeFile(keyPath, normalizedKey, { mode: 0o600 });
|
||||
await writeFileWithMode(keyPath, normalizedKey, FILE_MODES.ownerReadWrite);
|
||||
options.push(`IdentityFile=${keyPath}`);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import crypto from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { ResticDeps, ResticEnv } from "../types";
|
||||
import type { RepositoryConfig } from "../schemas";
|
||||
import { logger } from "../../node";
|
||||
import { FILE_MODES, writeFileWithMode } from "../../utils/fs.js";
|
||||
|
||||
export const buildEnv = async (
|
||||
config: RepositoryConfig,
|
||||
|
|
@ -19,17 +19,13 @@ export const buildEnv = async (
|
|||
const decryptedPassword = await deps.resolveSecret(config.customPassword);
|
||||
const passwordFilePath = path.join("/tmp", `zerobyte-pass-${crypto.randomBytes(8).toString("hex")}.txt`);
|
||||
|
||||
await fs.writeFile(passwordFilePath, decryptedPassword, {
|
||||
mode: 0o600,
|
||||
});
|
||||
await writeFileWithMode(passwordFilePath, decryptedPassword, FILE_MODES.ownerReadWrite);
|
||||
env.RESTIC_PASSWORD_FILE = passwordFilePath;
|
||||
} else {
|
||||
const encryptedPassword = await deps.getOrganizationResticPassword(organizationId);
|
||||
const decryptedPassword = await deps.resolveSecret(encryptedPassword);
|
||||
const passwordFilePath = path.join("/tmp", `zerobyte-pass-${crypto.randomBytes(8).toString("hex")}.txt`);
|
||||
await fs.writeFile(passwordFilePath, decryptedPassword, {
|
||||
mode: 0o600,
|
||||
});
|
||||
await writeFileWithMode(passwordFilePath, decryptedPassword, FILE_MODES.ownerReadWrite);
|
||||
env.RESTIC_PASSWORD_FILE = passwordFilePath;
|
||||
}
|
||||
|
||||
|
|
@ -51,9 +47,7 @@ export const buildEnv = async (
|
|||
case "gcs": {
|
||||
const decryptedCredentials = await deps.resolveSecret(config.credentialsJson);
|
||||
const credentialsPath = path.join("/tmp", `zerobyte-gcs-${crypto.randomBytes(8).toString("hex")}.json`);
|
||||
await fs.writeFile(credentialsPath, decryptedCredentials, {
|
||||
mode: 0o600,
|
||||
});
|
||||
await writeFileWithMode(credentialsPath, decryptedCredentials, FILE_MODES.ownerReadWrite);
|
||||
env.GOOGLE_PROJECT_ID = config.projectId;
|
||||
env.GOOGLE_APPLICATION_CREDENTIALS = credentialsPath;
|
||||
break;
|
||||
|
|
@ -89,7 +83,7 @@ export const buildEnv = async (
|
|||
throw new Error("Passphrase-protected SSH keys are not supported. Please provide an unencrypted private key.");
|
||||
}
|
||||
|
||||
await fs.writeFile(keyPath, normalizedKey, { mode: 0o600 });
|
||||
await writeFileWithMode(keyPath, normalizedKey, FILE_MODES.ownerReadWrite);
|
||||
|
||||
env._SFTP_KEY_PATH = keyPath;
|
||||
|
||||
|
|
@ -124,9 +118,7 @@ export const buildEnv = async (
|
|||
sshArgs.push("-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null");
|
||||
} else if (config.knownHosts) {
|
||||
const knownHostsPath = path.join("/tmp", `zerobyte-known-hosts-${crypto.randomBytes(8).toString("hex")}`);
|
||||
await fs.writeFile(knownHostsPath, config.knownHosts, {
|
||||
mode: 0o600,
|
||||
});
|
||||
await writeFileWithMode(knownHostsPath, config.knownHosts, FILE_MODES.ownerReadWrite);
|
||||
env._SFTP_KNOWN_HOSTS_PATH = knownHostsPath;
|
||||
sshArgs.push("-o", "StrictHostKeyChecking=yes", "-o", `UserKnownHostsFile=${knownHostsPath}`);
|
||||
}
|
||||
|
|
@ -144,7 +136,7 @@ export const buildEnv = async (
|
|||
if (config.cacert) {
|
||||
const decryptedCert = await deps.resolveSecret(config.cacert);
|
||||
const certPath = path.join("/tmp", `zerobyte-cacert-${crypto.randomBytes(8).toString("hex")}.pem`);
|
||||
await fs.writeFile(certPath, decryptedCert, { mode: 0o600 });
|
||||
await writeFileWithMode(certPath, decryptedCert, FILE_MODES.ownerReadWrite);
|
||||
env.RESTIC_CACERT = certPath;
|
||||
}
|
||||
|
||||
|
|
|
|||
32
packages/core/src/utils/__tests__/fs.test.ts
Normal file
32
packages/core/src/utils/__tests__/fs.test.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, test } from "bun:test";
|
||||
import { FILE_MODES, writeFileWithMode } from "../fs";
|
||||
|
||||
const tempDirectories = new Set<string>();
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
[...tempDirectories].map(async (directoryPath) => {
|
||||
await fs.rm(directoryPath, { recursive: true, force: true });
|
||||
}),
|
||||
);
|
||||
tempDirectories.clear();
|
||||
});
|
||||
|
||||
describe("writeFileWithMode", () => {
|
||||
test("applies the requested mode even when rewriting an existing file", async () => {
|
||||
const tempDirectory = await fs.mkdtemp(path.join(os.tmpdir(), "zerobyte-write-file-with-mode-"));
|
||||
tempDirectories.add(tempDirectory);
|
||||
|
||||
const filePath = path.join(tempDirectory, "identity");
|
||||
await fs.writeFile(filePath, "old-content");
|
||||
await fs.chmod(filePath, 0o755);
|
||||
|
||||
await writeFileWithMode(filePath, "new-content", FILE_MODES.ownerReadWrite);
|
||||
|
||||
expect(await fs.readFile(filePath, "utf8")).toBe("new-content");
|
||||
expect((await fs.stat(filePath)).mode & 0o777).toBe(0o600);
|
||||
});
|
||||
});
|
||||
12
packages/core/src/utils/fs.ts
Normal file
12
packages/core/src/utils/fs.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import fs from "node:fs/promises";
|
||||
|
||||
export const FILE_MODES = {
|
||||
ownerReadWrite: 0o600,
|
||||
} as const;
|
||||
|
||||
export type FileMode = (typeof FILE_MODES)[keyof typeof FILE_MODES];
|
||||
|
||||
export const writeFileWithMode = async (filePath: string, data: string, mode: FileMode) => {
|
||||
await fs.writeFile(filePath, data, { mode });
|
||||
await fs.chmod(filePath, mode);
|
||||
};
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
export { safeJsonParse } from "./json.js";
|
||||
export { normalizeAbsolutePath } from "./path.js";
|
||||
export { findCommonAncestor } from "./common-ancestor.js";
|
||||
export { FILE_MODES, writeFileWithMode } from "./fs.js";
|
||||
|
|
|
|||
Loading…
Reference in a new issue