fix: decrypt volume config before trying to mount volume

This commit is contained in:
Nicolas Meienberger 2026-05-06 22:01:58 +02:00 committed by Nico
parent 89d347cbc7
commit deedf2a203
4 changed files with 25 additions and 21 deletions

View file

@ -32,3 +32,7 @@ export const mapVolumeConfigSecrets = async (
export const encryptVolumeConfig = async (config: BackendConfig): Promise<BackendConfig> => { export const encryptVolumeConfig = async (config: BackendConfig): Promise<BackendConfig> => {
return await mapVolumeConfigSecrets(config, cryptoUtils.sealSecret); return await mapVolumeConfigSecrets(config, cryptoUtils.sealSecret);
}; };
export const decryptVolumeConfig = async (config: BackendConfig): Promise<BackendConfig> => {
return await mapVolumeConfigSecrets(config, cryptoUtils.resolveSecret);
};

View file

@ -20,7 +20,7 @@ import { volumeConfigSchema, type BackendConfig } from "~/schemas/volumes";
import { getOrganizationId } from "~/server/core/request-context"; import { getOrganizationId } from "~/server/core/request-context";
import { isNodeJSErrnoException } from "~/server/utils/fs"; import { isNodeJSErrnoException } from "~/server/utils/fs";
import { asShortId, type ShortId } from "~/server/utils/branded"; import { asShortId, type ShortId } from "~/server/utils/branded";
import { encryptVolumeConfig } from "./volume-config-secrets"; import { decryptVolumeConfig, encryptVolumeConfig } from "./volume-config-secrets";
type EnsureHealthyVolumeResult = type EnsureHealthyVolumeResult =
| { | {
@ -80,7 +80,7 @@ const createVolume = async (name: string, backendConfig: BackendConfig) => {
throw new InternalServerError("Failed to create volume"); throw new InternalServerError("Failed to create volume");
} }
const backend = createVolumeBackend(created); const backend = createVolumeBackend({ ...created, config: await decryptVolumeConfig(created.config) });
const { error, status } = await backend.mount(); const { error, status } = await backend.mount();
await db await db
@ -114,7 +114,7 @@ const mountVolume = async (shortId: ShortId) => {
throw new NotFoundError("Volume not found"); throw new NotFoundError("Volume not found");
} }
const backend = createVolumeBackend(volume); const backend = createVolumeBackend({ ...volume, config: await decryptVolumeConfig(volume.config) });
await backend.unmount(); await backend.unmount();
const { error, status } = await backend.mount(); const { error, status } = await backend.mount();
@ -219,7 +219,7 @@ const updateVolume = async (shortId: ShortId, volumeData: UpdateVolumeBody) => {
} }
if (configChanged) { if (configChanged) {
const backend = createVolumeBackend(updated); const backend = createVolumeBackend({ ...updated, config: await decryptVolumeConfig(updated.config) });
const { error, status } = await backend.mount(); const { error, status } = await backend.mount();
await db await db
.update(volumesTable) .update(volumesTable)
@ -235,18 +235,16 @@ const updateVolume = async (shortId: ShortId, volumeData: UpdateVolumeBody) => {
const testConnection = async (backendConfig: BackendConfig) => { const testConnection = async (backendConfig: BackendConfig) => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "zerobyte-test-")); const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "zerobyte-test-"));
try { try {
const encryptedConfig = await encryptVolumeConfig(backendConfig);
const mockVolume = { const mockVolume = {
id: 0, id: 0,
shortId: asShortId("test"), shortId: asShortId("test"),
name: "test-connection", name: "test-connection",
path: tempDir, path: tempDir,
config: encryptedConfig, config: backendConfig,
createdAt: Date.now(), createdAt: Date.now(),
updatedAt: Date.now(), updatedAt: Date.now(),
lastHealthCheck: Date.now(), lastHealthCheck: Date.now(),
type: encryptedConfig.backend, type: backendConfig.backend,
status: "unmounted" as const, status: "unmounted" as const,
lastError: null, lastError: null,
provisioningId: null, provisioningId: null,

View file

@ -1,4 +1,5 @@
import * as fs from "node:fs/promises"; import * as fs from "node:fs/promises";
import { createHash } from "node:crypto";
import * as os from "node:os"; import * as os from "node:os";
import * as path from "node:path"; import * as path from "node:path";
import { spawn } from "node:child_process"; import { spawn } from "node:child_process";
@ -10,8 +11,9 @@ import { withTimeout } from "../timeout";
import type { BackendConfig, VolumeBackend } from "../types"; import type { BackendConfig, VolumeBackend } from "../types";
import { executeUnmount } from "./utils"; import { executeUnmount } from "./utils";
const getPrivateKeyPath = (mountPath: string) => path.join(SSH_KEYS_DIR, `${path.basename(mountPath)}.key`); const getMountPathHash = (mountPath: string) => createHash("sha256").update(mountPath).digest("hex").slice(0, 16);
const getKnownHostsPath = (mountPath: string) => path.join(SSH_KEYS_DIR, `${path.basename(mountPath)}.known_hosts`); 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 runSshfs = async (args: string[], password?: string) => const runSshfs = async (args: string[], password?: string) =>
new Promise<void>((resolve, reject) => { new Promise<void>((resolve, reject) => {
@ -138,7 +140,6 @@ const mount = async (config: BackendConfig, mountPath: string) => {
const args = [`${config.username}@${config.host}:${config.path || ""}`, mountPath, "-o", options.join(",")]; const args = [`${config.username}@${config.host}:${config.path || ""}`, mountPath, "-o", options.join(",")];
if (config.password) args.push("-o", "password_stdin"); if (config.password) args.push("-o", "password_stdin");
logger.info(`Executing sshfs: sshfs ${args.join(" ")}`); logger.info(`Executing sshfs: sshfs ${args.join(" ")}`);
await runSshfs(args, config.password); await runSshfs(args, config.password);

View file

@ -16,18 +16,19 @@ export const listVolumeFiles = async (
) => { ) => {
const volumePath = getVolumePath(volume); const volumePath = getVolumePath(volume);
const requestedPath = subPath ? path.join(volumePath, subPath) : volumePath; const requestedPath = subPath ? path.join(volumePath, subPath) : volumePath;
const normalizedPath = path.normalize(requestedPath);
const relative = path.relative(volumePath, normalizedPath);
if (relative.startsWith("..") || path.isAbsolute(relative)) {
throw new Error("Invalid path");
}
const pageSize = Math.min(Math.max(limit, 1), MAX_PAGE_SIZE); const pageSize = Math.min(Math.max(limit, 1), MAX_PAGE_SIZE);
const startOffset = Math.max(offset, 0); const startOffset = Math.max(offset, 0);
try { try {
const dirents = await fs.readdir(normalizedPath, { withFileTypes: true }); const realVolumeRoot = await fs.realpath(volumePath);
const realRequestedPath = await fs.realpath(requestedPath);
const relative = path.relative(realVolumeRoot, realRequestedPath);
if (relative.startsWith("..") || path.isAbsolute(relative)) {
throw new Error("Invalid path");
}
const dirents = await fs.readdir(realRequestedPath, { withFileTypes: true });
dirents.sort((a, b) => { dirents.sort((a, b) => {
const aIsDir = a.isDirectory(); const aIsDir = a.isDirectory();
@ -45,11 +46,11 @@ export const listVolumeFiles = async (
const entries = ( const entries = (
await Promise.all( await Promise.all(
paginatedDirents.map(async (dirent) => { paginatedDirents.map(async (dirent) => {
const fullPath = path.join(normalizedPath, dirent.name); const fullPath = path.join(realRequestedPath, dirent.name);
try { try {
const stats = await fs.stat(fullPath); const stats = await fs.stat(fullPath);
const relativePath = path.relative(volumePath, fullPath); const relativePath = path.relative(realVolumeRoot, fullPath);
return { return {
name: dirent.name, name: dirent.name,