refactor(agent): store SFTP volume keys in tmp (#888)

* refactor(agent): store SFTP volume keys in tmp

* refactor: store temp keys in /run/zerobyte subfolders

* fix(restic): clean temp secrets dir on env setup failure

* fix: one secrets temp dir per env building
This commit is contained in:
Nico 2026-05-17 18:19:09 +02:00 committed by GitHub
parent 19a0781667
commit 11dacd7c71
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 284 additions and 177 deletions

View file

@ -9,11 +9,10 @@ export const RESTIC_CACHE_DIR = process.env.RESTIC_CACHE_DIR || "/var/lib/zeroby
export const DATABASE_URL = process.env.ZEROBYTE_DATABASE_URL || "/var/lib/zerobyte/data/zerobyte.db";
export const RESTIC_PASS_FILE = process.env.RESTIC_PASS_FILE || "/var/lib/zerobyte/data/restic.pass";
export const SSH_KEYS_DIR = "/var/lib/zerobyte/ssh";
export const RCLONE_CONFIG_DIR = process.env.RCLONE_CONFIG_DIR || "/root/.config/rclone";
export const RCLONE_CONFIG_FILE = path.join(RCLONE_CONFIG_DIR, "rclone.conf");
export const RESTORE_BLOCKED_ROOTS = [REPOSITORY_BASE, RESTIC_CACHE_DIR, SSH_KEYS_DIR, RCLONE_CONFIG_DIR, "/app"];
export const RESTORE_BLOCKED_ROOTS = [REPOSITORY_BASE, RESTIC_CACHE_DIR, RCLONE_CONFIG_DIR, "/app"];
export const DEFAULT_EXCLUDES = [RESTIC_PASS_FILE, REPOSITORY_BASE];

View file

@ -16,6 +16,27 @@ const getMountPathHash = (mountPath: string) => createHash("sha256").update(moun
const getPrivateKeyPath = (mountPath: string) => path.join(SSH_KEYS_DIR, `${getMountPathHash(mountPath)}.key`);
const getKnownHostsPath = (mountPath: string) => path.join(SSH_KEYS_DIR, `${getMountPathHash(mountPath)}.known_hosts`);
const ensurePrivateSshKeysDir = async () => {
const { uid } = os.userInfo();
try {
await fs.mkdir(SSH_KEYS_DIR, { mode: 0o700 });
} catch (error) {
if (!isNodeErrnoException(error) || error.code !== "EEXIST") throw error;
const stats = await fs.lstat(SSH_KEYS_DIR);
if (!stats.isDirectory() || stats.isSymbolicLink() || stats.uid !== uid) {
throw new Error(`Refusing to use unsafe SSH keys directory: ${SSH_KEYS_DIR}`);
}
}
await fs.chmod(SSH_KEYS_DIR, 0o700);
};
const isNodeErrnoException = (error: unknown): error is NodeJS.ErrnoException => {
return error instanceof Error && "code" in error;
};
const runSshfs = async (args: string[], password?: string) =>
new Promise<void>((resolve, reject) => {
const child = spawn("sshfs", args, { stdio: ["pipe", "pipe", "pipe"] });
@ -110,7 +131,7 @@ const mount = async (config: BackendConfig, mountPath: string) => {
const run = async () => {
await fs.mkdir(mountPath, { recursive: true });
await fs.mkdir(SSH_KEYS_DIR, { recursive: true });
await ensurePrivateSshKeysDir();
const { uid, gid } = os.userInfo();
const options = [
"reconnect",

View file

@ -1,8 +1,9 @@
import * as path from "node:path";
import * as os from "node:os";
export const OPERATION_TIMEOUT = 5000;
export const VOLUME_MOUNT_BASE = process.env.ZEROBYTE_VOLUMES_DIR || "/var/lib/zerobyte/volumes";
export const SSH_KEYS_DIR = "/var/lib/zerobyte/ssh";
export const SSH_KEYS_DIR = path.join(os.tmpdir(), "zerobyte-ssh");
export const RCLONE_CONFIG_DIR = process.env.RCLONE_CONFIG_DIR || "/root/.config/rclone";
export const RCLONE_CONFIG_FILE = path.join(RCLONE_CONFIG_DIR, "rclone.conf");
const serverIdleTimeout = Number(process.env.SERVER_IDLE_TIMEOUT ?? 60);

View file

@ -1,4 +1,5 @@
import fs from "node:fs/promises";
import os from "node:os";
import { afterEach, describe, expect, test } from "vitest";
import { buildEnv } from "../build-env";
import type { ResticDeps } from "../../types";
@ -24,14 +25,7 @@ const withCustomPassword = <T extends object>(config: T) => ({
const PLAIN_PRIVATE_KEY =
"-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAA\n-----END OPENSSH PRIVATE KEY-----";
const tempFiles = new Set<string>();
const trackTempFile = (filePath: string | undefined) => {
if (filePath) {
tempFiles.add(filePath);
}
};
const tempDirs = new Set<string>();
const buildEnvForTest = async (
config: Parameters<typeof buildEnv>[0],
@ -39,36 +33,35 @@ const buildEnvForTest = async (
deps: ResticDeps = makeDeps(),
) => {
const env = await buildEnv(config, organizationId, deps);
// Automatically track all temp file paths created by buildEnv
trackTempFile(env.RESTIC_PASSWORD_FILE);
trackTempFile(env.GOOGLE_APPLICATION_CREDENTIALS);
trackTempFile(env._SFTP_KEY_PATH);
trackTempFile(env._SFTP_KNOWN_HOSTS_PATH);
trackTempFile(env.RESTIC_CACERT);
if (env._RUNTIME_SECRETS_DIR) tempDirs.add(env._RUNTIME_SECRETS_DIR);
return env;
};
const listResticTempDirs = async () => {
return new Set((await fs.readdir(os.tmpdir())).filter((name) => name.startsWith("zerobyte-restic-")));
};
afterEach(async () => {
await Promise.all(
[...tempFiles].map(async (filePath) => {
await fs.rm(filePath, { force: true });
}),
);
tempFiles.clear();
await Promise.all([...tempDirs].map((dir) => fs.rm(dir, { recursive: true, force: true })));
tempDirs.clear();
});
describe("buildEnv", () => {
describe("base environment", () => {
test("always sets RESTIC_CACHE_DIR", async () => {
const env = await buildEnvForTest(withCustomPassword({ backend: "local" as const, path: "/tmp/repo" }), "org-1");
const env = await buildEnvForTest(
withCustomPassword({ backend: "local" as const, path: "/tmp/repo" }),
"org-1",
);
expect(env.RESTIC_CACHE_DIR).toBe(RESTIC_CACHE_DIR);
});
test("always sets PATH", async () => {
const env = await buildEnvForTest(withCustomPassword({ backend: "local" as const, path: "/tmp/repo" }), "org-1");
const env = await buildEnvForTest(
withCustomPassword({ backend: "local" as const, path: "/tmp/repo" }),
"org-1",
);
expect(env.PATH).toBeTruthy();
});
@ -77,7 +70,12 @@ describe("buildEnv", () => {
describe("password resolution", () => {
test("writes a password file when using customPassword on an existing repository", async () => {
const env = await buildEnvForTest(
{ backend: "local" as const, path: "/tmp/repo", isExistingRepository: true, customPassword: "my-secret" },
{
backend: "local" as const,
path: "/tmp/repo",
isExistingRepository: true,
customPassword: "my-secret",
},
"org-1",
);
@ -114,10 +112,39 @@ describe("buildEnv", () => {
throw new Error("Organization non-existent-org not found");
},
});
const before = await listResticTempDirs();
await expect(
buildEnv({ backend: "local" as const, path: "/tmp/repo" }, "non-existent-org", deps),
).rejects.toThrow("Organization non-existent-org not found");
expect(await listResticTempDirs()).toEqual(before);
});
test("cleans the temp directory when setup fails after creating one", async () => {
const before = await listResticTempDirs();
const deps = makeDeps({
resolveSecret: async (secret) => {
if (secret === "my-access-key") throw new Error("failed to resolve access key");
return secret;
},
});
await expect(
buildEnv(
withCustomPassword({
backend: "s3" as const,
endpoint: "https://s3.amazonaws.com",
bucket: "my-bucket",
accessKeyId: "my-access-key",
secretAccessKey: "my-secret-key",
}),
"org-1",
deps,
),
).rejects.toThrow("failed to resolve access key");
expect(await listResticTempDirs()).toEqual(before);
});
test("throws when getOrganizationResticPassword throws (no password configured)", async () => {
@ -127,9 +154,9 @@ describe("buildEnv", () => {
},
});
await expect(buildEnv({ backend: "local" as const, path: "/tmp/repo" }, "org-no-pass", deps)).rejects.toThrow(
"Restic password not configured for organization org-no-pass",
);
await expect(
buildEnv({ backend: "local" as const, path: "/tmp/repo" }, "org-no-pass", deps),
).rejects.toThrow("Restic password not configured for organization org-no-pass");
});
});
@ -150,7 +177,10 @@ describe("buildEnv", () => {
});
test("sets AWS_S3_BUCKET_LOOKUP=dns for Huawei Cloud endpoints", async () => {
const env = await buildEnvForTest({ ...base, endpoint: "https://obs.ap-southeast-1.myhuaweicloud.com" }, "org-1");
const env = await buildEnvForTest(
{ ...base, endpoint: "https://obs.ap-southeast-1.myhuaweicloud.com" },
"org-1",
);
expect(env.AWS_S3_BUCKET_LOOKUP).toBe("dns");
});
@ -296,7 +326,8 @@ describe("buildEnv", () => {
buildEnv(
{
...baseSftpConfig,
privateKey: "-----BEGIN RSA PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\n-----END RSA PRIVATE KEY-----",
privateKey:
"-----BEGIN RSA PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\n-----END RSA PRIVATE KEY-----",
},
"org-1",
deps,
@ -319,7 +350,10 @@ describe("buildEnv", () => {
});
test("uses StrictHostKeyChecking=yes when knownHosts is absent", async () => {
const env = await buildEnvForTest({ ...baseSftpConfig, skipHostKeyCheck: false, knownHosts: undefined }, "org-1");
const env = await buildEnvForTest(
{ ...baseSftpConfig, skipHostKeyCheck: false, knownHosts: undefined },
"org-1",
);
expect(env._SFTP_SSH_ARGS).toContain("StrictHostKeyChecking=yes");
expect(env._SFTP_SSH_ARGS).not.toContain("UserKnownHostsFile=/dev/null");
@ -336,8 +370,8 @@ describe("buildEnv", () => {
);
expect(env._SFTP_SSH_ARGS).toContain("StrictHostKeyChecking=yes");
expect(env._SFTP_SSH_ARGS).toContain("UserKnownHostsFile=/tmp/zerobyte-known-hosts-");
expect(env._SFTP_KNOWN_HOSTS_PATH).toMatch(/^\/tmp\/zerobyte-known-hosts-/);
expect(env._SFTP_SSH_ARGS).toContain("/zerobyte-known-hosts-");
expect(env._SFTP_KNOWN_HOSTS_PATH).toMatch(/\/zerobyte-restic-[^/]+\/zerobyte-known-hosts-/);
});
test("adds -p flag for non-default ports", async () => {
@ -355,7 +389,7 @@ describe("buildEnv", () => {
test("sets the key path in both _SFTP_KEY_PATH and SSH args -i flag", async () => {
const env = await buildEnvForTest(baseSftpConfig, "org-1");
expect(env._SFTP_KEY_PATH).toMatch(/^\/tmp\/zerobyte-ssh-/);
expect(env._SFTP_KEY_PATH).toMatch(/\/zerobyte-restic-[^/]+\/zerobyte-ssh-/);
expect(env._SFTP_SSH_ARGS).toContain(`-i ${env._SFTP_KEY_PATH}`);
});
});
@ -382,7 +416,10 @@ describe("buildEnv", () => {
});
test("does not set RESTIC_CACERT when cacert is absent", async () => {
const env = await buildEnvForTest(withCustomPassword({ backend: "local" as const, path: "/tmp/repo" }), "org-1");
const env = await buildEnvForTest(
withCustomPassword({ backend: "local" as const, path: "/tmp/repo" }),
"org-1",
);
expect(env.RESTIC_CACERT).toBeUndefined();
});
@ -399,7 +436,10 @@ describe("buildEnv", () => {
});
test("does not set _INSECURE_TLS when insecureTls is absent", async () => {
const env = await buildEnvForTest(withCustomPassword({ backend: "local" as const, path: "/tmp/repo" }), "org-1");
const env = await buildEnvForTest(
withCustomPassword({ backend: "local" as const, path: "/tmp/repo" }),
"org-1",
);
expect(env._INSECURE_TLS).toBeUndefined();
});

View file

@ -1,10 +1,26 @@
import crypto from "node:crypto";
import fs from "node:fs/promises";
import os from "node:os";
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 "../../node/fs.js";
const createRuntimeSecretsDir = async () => {
const runtimeSecretsDir = await fs.mkdtemp(path.join(os.tmpdir(), "zerobyte-restic-"));
await fs.chmod(runtimeSecretsDir, 0o700);
return runtimeSecretsDir;
};
const runtimeSecretPath = (runtimeSecretsDir: string, filename: string) => {
return path.join(runtimeSecretsDir, filename);
};
const removeRuntimeSecretsDir = async (runtimeSecretsDir: string | undefined) => {
if (runtimeSecretsDir) await fs.rm(runtimeSecretsDir, { recursive: true, force: true }).catch(() => {});
};
export const buildEnv = async (
config: RepositoryConfig,
organizationId: string,
@ -14,144 +30,170 @@ export const buildEnv = async (
RESTIC_CACHE_DIR: deps.resticCacheDir,
PATH: process.env.PATH || "/usr/local/bin:/usr/bin:/bin",
};
let runtimeSecretsDir: string | undefined;
const getRuntimeSecretPath = async (filename: string) => {
if (!runtimeSecretsDir) {
runtimeSecretsDir = await createRuntimeSecretsDir();
env._RUNTIME_SECRETS_DIR = runtimeSecretsDir;
}
return runtimeSecretPath(runtimeSecretsDir, filename);
};
if (config.isExistingRepository && config.customPassword) {
const decryptedPassword = await deps.resolveSecret(config.customPassword);
const passwordFilePath = path.join("/tmp", `zerobyte-pass-${crypto.randomBytes(8).toString("hex")}.txt`);
try {
if (config.isExistingRepository && config.customPassword) {
const decryptedPassword = await deps.resolveSecret(config.customPassword);
const passwordFilePath = await getRuntimeSecretPath(
`zerobyte-pass-${crypto.randomBytes(8).toString("hex")}.txt`,
);
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 writeFileWithMode(passwordFilePath, decryptedPassword, FILE_MODES.ownerReadWrite);
env.RESTIC_PASSWORD_FILE = passwordFilePath;
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 = await getRuntimeSecretPath(
`zerobyte-pass-${crypto.randomBytes(8).toString("hex")}.txt`,
);
await writeFileWithMode(passwordFilePath, decryptedPassword, FILE_MODES.ownerReadWrite);
env.RESTIC_PASSWORD_FILE = passwordFilePath;
}
switch (config.backend) {
case "s3":
env.AWS_ACCESS_KEY_ID = await deps.resolveSecret(config.accessKeyId);
env.AWS_SECRET_ACCESS_KEY = await deps.resolveSecret(config.secretAccessKey);
if (config.endpoint.includes("myhuaweicloud")) {
env.AWS_S3_BUCKET_LOOKUP = "dns";
}
break;
case "r2":
env.AWS_ACCESS_KEY_ID = await deps.resolveSecret(config.accessKeyId);
env.AWS_SECRET_ACCESS_KEY = await deps.resolveSecret(config.secretAccessKey);
env.AWS_REGION = "auto";
env.AWS_S3_FORCE_PATH_STYLE = "true";
break;
case "gcs": {
const decryptedCredentials = await deps.resolveSecret(config.credentialsJson);
const credentialsPath = await getRuntimeSecretPath(
`zerobyte-gcs-${crypto.randomBytes(8).toString("hex")}.json`,
);
await writeFileWithMode(credentialsPath, decryptedCredentials, FILE_MODES.ownerReadWrite);
env.GOOGLE_PROJECT_ID = config.projectId;
env.GOOGLE_APPLICATION_CREDENTIALS = credentialsPath;
break;
}
case "azure": {
env.AZURE_ACCOUNT_NAME = config.accountName;
env.AZURE_ACCOUNT_KEY = await deps.resolveSecret(config.accountKey);
if (config.endpointSuffix) {
env.AZURE_ENDPOINT_SUFFIX = config.endpointSuffix;
}
break;
}
case "rest": {
if (config.username) {
env.RESTIC_REST_USERNAME = await deps.resolveSecret(config.username);
}
if (config.password) {
env.RESTIC_REST_PASSWORD = await deps.resolveSecret(config.password);
}
break;
}
case "rclone":
env.RCLONE_CONFIG = deps.rcloneConfigFile;
break;
case "sftp": {
const decryptedKey = await deps.resolveSecret(config.privateKey);
let normalizedKey = decryptedKey.replace(/\r\n/g, "\n");
if (!normalizedKey.endsWith("\n")) {
normalizedKey += "\n";
}
if (normalizedKey.includes("ENCRYPTED")) {
logger.error(
"SFTP: Private key appears to be passphrase-protected. Please use an unencrypted key.",
);
throw new Error(
"Passphrase-protected SSH keys are not supported. Please provide an unencrypted private key.",
);
}
const keyPath = await getRuntimeSecretPath(`zerobyte-ssh-${crypto.randomBytes(8).toString("hex")}`);
await writeFileWithMode(keyPath, normalizedKey, FILE_MODES.ownerReadWrite);
env._SFTP_KEY_PATH = keyPath;
const sshArgs = [
"-o",
"LogLevel=ERROR",
"-o",
"BatchMode=yes",
"-o",
"NumberOfPasswordPrompts=0",
"-o",
"PreferredAuthentications=publickey",
"-o",
"PasswordAuthentication=no",
"-o",
"KbdInteractiveAuthentication=no",
"-o",
"IdentitiesOnly=yes",
"-o",
"ConnectTimeout=10",
"-o",
"ConnectionAttempts=1",
"-o",
"ServerAliveInterval=60",
"-o",
"ServerAliveCountMax=240",
"-i",
keyPath,
];
if (config.skipHostKeyCheck) {
sshArgs.push("-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null");
} else if (config.knownHosts) {
const knownHostsPath = await getRuntimeSecretPath(
`zerobyte-known-hosts-${crypto.randomBytes(8).toString("hex")}`,
);
await writeFileWithMode(knownHostsPath, config.knownHosts, FILE_MODES.ownerReadWrite);
env._SFTP_KNOWN_HOSTS_PATH = knownHostsPath;
sshArgs.push("-o", "StrictHostKeyChecking=yes", "-o", `UserKnownHostsFile=${knownHostsPath}`);
} else {
sshArgs.push("-o", "StrictHostKeyChecking=yes");
}
if (config.port && config.port !== 22) {
sshArgs.push("-p", String(config.port));
}
env._SFTP_SSH_ARGS = sshArgs.join(" ");
logger.info(`SFTP: SSH args: ${env._SFTP_SSH_ARGS}`);
break;
}
}
if (config.cacert) {
const decryptedCert = await deps.resolveSecret(config.cacert);
const certPath = await getRuntimeSecretPath(`zerobyte-cacert-${crypto.randomBytes(8).toString("hex")}.pem`);
await writeFileWithMode(certPath, decryptedCert, FILE_MODES.ownerReadWrite);
env.RESTIC_CACERT = certPath;
}
if (config.insecureTls) {
env._INSECURE_TLS = "true";
if (config.backend === "rclone") {
env.RCLONE_NO_CHECK_CERTIFICATE = "true";
}
}
return env;
} catch (error) {
await removeRuntimeSecretsDir(runtimeSecretsDir);
throw error;
}
switch (config.backend) {
case "s3":
env.AWS_ACCESS_KEY_ID = await deps.resolveSecret(config.accessKeyId);
env.AWS_SECRET_ACCESS_KEY = await deps.resolveSecret(config.secretAccessKey);
if (config.endpoint.includes("myhuaweicloud")) {
env.AWS_S3_BUCKET_LOOKUP = "dns";
}
break;
case "r2":
env.AWS_ACCESS_KEY_ID = await deps.resolveSecret(config.accessKeyId);
env.AWS_SECRET_ACCESS_KEY = await deps.resolveSecret(config.secretAccessKey);
env.AWS_REGION = "auto";
env.AWS_S3_FORCE_PATH_STYLE = "true";
break;
case "gcs": {
const decryptedCredentials = await deps.resolveSecret(config.credentialsJson);
const credentialsPath = path.join("/tmp", `zerobyte-gcs-${crypto.randomBytes(8).toString("hex")}.json`);
await writeFileWithMode(credentialsPath, decryptedCredentials, FILE_MODES.ownerReadWrite);
env.GOOGLE_PROJECT_ID = config.projectId;
env.GOOGLE_APPLICATION_CREDENTIALS = credentialsPath;
break;
}
case "azure": {
env.AZURE_ACCOUNT_NAME = config.accountName;
env.AZURE_ACCOUNT_KEY = await deps.resolveSecret(config.accountKey);
if (config.endpointSuffix) {
env.AZURE_ENDPOINT_SUFFIX = config.endpointSuffix;
}
break;
}
case "rest": {
if (config.username) {
env.RESTIC_REST_USERNAME = await deps.resolveSecret(config.username);
}
if (config.password) {
env.RESTIC_REST_PASSWORD = await deps.resolveSecret(config.password);
}
break;
}
case "rclone":
env.RCLONE_CONFIG = deps.rcloneConfigFile;
break;
case "sftp": {
const decryptedKey = await deps.resolveSecret(config.privateKey);
const keyPath = path.join("/tmp", `zerobyte-ssh-${crypto.randomBytes(8).toString("hex")}`);
let normalizedKey = decryptedKey.replace(/\r\n/g, "\n");
if (!normalizedKey.endsWith("\n")) {
normalizedKey += "\n";
}
if (normalizedKey.includes("ENCRYPTED")) {
logger.error("SFTP: Private key appears to be passphrase-protected. Please use an unencrypted key.");
throw new Error("Passphrase-protected SSH keys are not supported. Please provide an unencrypted private key.");
}
await writeFileWithMode(keyPath, normalizedKey, FILE_MODES.ownerReadWrite);
env._SFTP_KEY_PATH = keyPath;
const sshArgs = [
"-o",
"LogLevel=ERROR",
"-o",
"BatchMode=yes",
"-o",
"NumberOfPasswordPrompts=0",
"-o",
"PreferredAuthentications=publickey",
"-o",
"PasswordAuthentication=no",
"-o",
"KbdInteractiveAuthentication=no",
"-o",
"IdentitiesOnly=yes",
"-o",
"ConnectTimeout=10",
"-o",
"ConnectionAttempts=1",
"-o",
"ServerAliveInterval=60",
"-o",
"ServerAliveCountMax=240",
"-i",
keyPath,
];
if (config.skipHostKeyCheck) {
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 writeFileWithMode(knownHostsPath, config.knownHosts, FILE_MODES.ownerReadWrite);
env._SFTP_KNOWN_HOSTS_PATH = knownHostsPath;
sshArgs.push("-o", "StrictHostKeyChecking=yes", "-o", `UserKnownHostsFile=${knownHostsPath}`);
} else {
sshArgs.push("-o", "StrictHostKeyChecking=yes");
}
if (config.port && config.port !== 22) {
sshArgs.push("-p", String(config.port));
}
env._SFTP_SSH_ARGS = sshArgs.join(" ");
logger.info(`SFTP: SSH args: ${env._SFTP_SSH_ARGS}`);
break;
}
}
if (config.cacert) {
const decryptedCert = await deps.resolveSecret(config.cacert);
const certPath = path.join("/tmp", `zerobyte-cacert-${crypto.randomBytes(8).toString("hex")}.pem`);
await writeFileWithMode(certPath, decryptedCert, FILE_MODES.ownerReadWrite);
env.RESTIC_CACERT = certPath;
}
if (config.insecureTls) {
env._INSECURE_TLS = "true";
if (config.backend === "rclone") {
env.RCLONE_NO_CHECK_CERTIFICATE = "true";
}
}
return env;
};

View file

@ -13,4 +13,8 @@ export const cleanupTemporaryKeys = async (env: ResticEnv, deps: ResticDeps) =>
if (env.RESTIC_PASSWORD_FILE && env.RESTIC_PASSWORD_FILE !== deps.resticPassFile) {
await fs.unlink(env.RESTIC_PASSWORD_FILE).catch(() => {});
}
if (env._RUNTIME_SECRETS_DIR) {
await fs.rm(env._RUNTIME_SECRETS_DIR, { recursive: true, force: true }).catch(() => {});
}
};