feat: allow more secure SFTP connection with known hosts
This commit is contained in:
parent
d94653b2fd
commit
8c243a0280
9 changed files with 165 additions and 13 deletions
|
|
@ -175,6 +175,8 @@ export type ListVolumesResponses = {
|
||||||
path: string;
|
path: string;
|
||||||
username: string;
|
username: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
|
skipHostKeyCheck?: boolean;
|
||||||
|
knownHosts?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
privateKey?: string;
|
privateKey?: string;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
|
|
@ -236,6 +238,8 @@ export type CreateVolumeData = {
|
||||||
path: string;
|
path: string;
|
||||||
username: string;
|
username: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
|
skipHostKeyCheck?: boolean;
|
||||||
|
knownHosts?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
privateKey?: string;
|
privateKey?: string;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
|
|
@ -294,6 +298,8 @@ export type CreateVolumeResponses = {
|
||||||
path: string;
|
path: string;
|
||||||
username: string;
|
username: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
|
skipHostKeyCheck?: boolean;
|
||||||
|
knownHosts?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
privateKey?: string;
|
privateKey?: string;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
|
|
@ -355,6 +361,8 @@ export type TestConnectionData = {
|
||||||
path: string;
|
path: string;
|
||||||
username: string;
|
username: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
|
skipHostKeyCheck?: boolean;
|
||||||
|
knownHosts?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
privateKey?: string;
|
privateKey?: string;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
|
|
@ -466,6 +474,8 @@ export type GetVolumeResponses = {
|
||||||
path: string;
|
path: string;
|
||||||
username: string;
|
username: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
|
skipHostKeyCheck?: boolean;
|
||||||
|
knownHosts?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
privateKey?: string;
|
privateKey?: string;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
|
|
@ -529,6 +539,8 @@ export type UpdateVolumeData = {
|
||||||
path: string;
|
path: string;
|
||||||
username: string;
|
username: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
|
skipHostKeyCheck?: boolean;
|
||||||
|
knownHosts?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
privateKey?: string;
|
privateKey?: string;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
|
|
@ -596,6 +608,8 @@ export type UpdateVolumeResponses = {
|
||||||
path: string;
|
path: string;
|
||||||
username: string;
|
username: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
|
skipHostKeyCheck?: boolean;
|
||||||
|
knownHosts?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
privateKey?: string;
|
privateKey?: string;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
|
|
@ -836,8 +850,10 @@ export type ListRepositoriesResponses = {
|
||||||
privateKey: string;
|
privateKey: string;
|
||||||
user: string;
|
user: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
|
skipHostKeyCheck?: boolean;
|
||||||
customPassword?: string;
|
customPassword?: string;
|
||||||
isExistingRepository?: boolean;
|
isExistingRepository?: boolean;
|
||||||
|
knownHosts?: string;
|
||||||
};
|
};
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -913,8 +929,10 @@ export type CreateRepositoryData = {
|
||||||
privateKey: string;
|
privateKey: string;
|
||||||
user: string;
|
user: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
|
skipHostKeyCheck?: boolean;
|
||||||
customPassword?: string;
|
customPassword?: string;
|
||||||
isExistingRepository?: boolean;
|
isExistingRepository?: boolean;
|
||||||
|
knownHosts?: string;
|
||||||
};
|
};
|
||||||
name: string;
|
name: string;
|
||||||
compressionMode?: 'auto' | 'max' | 'off';
|
compressionMode?: 'auto' | 'max' | 'off';
|
||||||
|
|
@ -1052,8 +1070,10 @@ export type GetRepositoryResponses = {
|
||||||
privateKey: string;
|
privateKey: string;
|
||||||
user: string;
|
user: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
|
skipHostKeyCheck?: boolean;
|
||||||
customPassword?: string;
|
customPassword?: string;
|
||||||
isExistingRepository?: boolean;
|
isExistingRepository?: boolean;
|
||||||
|
knownHosts?: string;
|
||||||
};
|
};
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -1156,8 +1176,10 @@ export type UpdateRepositoryResponses = {
|
||||||
privateKey: string;
|
privateKey: string;
|
||||||
user: string;
|
user: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
|
skipHostKeyCheck?: boolean;
|
||||||
customPassword?: string;
|
customPassword?: string;
|
||||||
isExistingRepository?: boolean;
|
isExistingRepository?: boolean;
|
||||||
|
knownHosts?: string;
|
||||||
};
|
};
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -1430,8 +1452,10 @@ export type ListBackupSchedulesResponses = {
|
||||||
privateKey: string;
|
privateKey: string;
|
||||||
user: string;
|
user: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
|
skipHostKeyCheck?: boolean;
|
||||||
customPassword?: string;
|
customPassword?: string;
|
||||||
isExistingRepository?: boolean;
|
isExistingRepository?: boolean;
|
||||||
|
knownHosts?: string;
|
||||||
};
|
};
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -1479,6 +1503,8 @@ export type ListBackupSchedulesResponses = {
|
||||||
path: string;
|
path: string;
|
||||||
username: string;
|
username: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
|
skipHostKeyCheck?: boolean;
|
||||||
|
knownHosts?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
privateKey?: string;
|
privateKey?: string;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
|
|
@ -1688,8 +1714,10 @@ export type GetBackupScheduleResponses = {
|
||||||
privateKey: string;
|
privateKey: string;
|
||||||
user: string;
|
user: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
|
skipHostKeyCheck?: boolean;
|
||||||
customPassword?: string;
|
customPassword?: string;
|
||||||
isExistingRepository?: boolean;
|
isExistingRepository?: boolean;
|
||||||
|
knownHosts?: string;
|
||||||
};
|
};
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -1737,6 +1765,8 @@ export type GetBackupScheduleResponses = {
|
||||||
path: string;
|
path: string;
|
||||||
username: string;
|
username: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
|
skipHostKeyCheck?: boolean;
|
||||||
|
knownHosts?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
privateKey?: string;
|
privateKey?: string;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
|
|
@ -1927,8 +1957,10 @@ export type GetBackupScheduleForVolumeResponses = {
|
||||||
privateKey: string;
|
privateKey: string;
|
||||||
user: string;
|
user: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
|
skipHostKeyCheck?: boolean;
|
||||||
customPassword?: string;
|
customPassword?: string;
|
||||||
isExistingRepository?: boolean;
|
isExistingRepository?: boolean;
|
||||||
|
knownHosts?: string;
|
||||||
};
|
};
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -1976,6 +2008,8 @@ export type GetBackupScheduleForVolumeResponses = {
|
||||||
path: string;
|
path: string;
|
||||||
username: string;
|
username: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
|
skipHostKeyCheck?: boolean;
|
||||||
|
knownHosts?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
privateKey?: string;
|
privateKey?: string;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
|
|
@ -2333,8 +2367,10 @@ export type GetScheduleMirrorsResponses = {
|
||||||
privateKey: string;
|
privateKey: string;
|
||||||
user: string;
|
user: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
|
skipHostKeyCheck?: boolean;
|
||||||
customPassword?: string;
|
customPassword?: string;
|
||||||
isExistingRepository?: boolean;
|
isExistingRepository?: boolean;
|
||||||
|
knownHosts?: string;
|
||||||
};
|
};
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -2437,8 +2473,10 @@ export type UpdateScheduleMirrorsResponses = {
|
||||||
privateKey: string;
|
privateKey: string;
|
||||||
user: string;
|
user: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
|
skipHostKeyCheck?: boolean;
|
||||||
customPassword?: string;
|
customPassword?: string;
|
||||||
isExistingRepository?: boolean;
|
isExistingRepository?: boolean;
|
||||||
|
knownHosts?: string;
|
||||||
};
|
};
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ const defaultValuesForType = {
|
||||||
azure: { backend: "azure" as const, compressionMode: "auto" as const },
|
azure: { backend: "azure" as const, compressionMode: "auto" as const },
|
||||||
rclone: { backend: "rclone" as const, compressionMode: "auto" as const },
|
rclone: { backend: "rclone" as const, compressionMode: "auto" as const },
|
||||||
rest: { backend: "rest" as const, compressionMode: "auto" as const },
|
rest: { backend: "rest" as const, compressionMode: "auto" as const },
|
||||||
sftp: { backend: "sftp" as const, compressionMode: "auto" as const, port: 22 },
|
sftp: { backend: "sftp" as const, compressionMode: "auto" as const, port: 22, skipHostKeyCheck: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CreateRepositoryForm = ({
|
export const CreateRepositoryForm = ({
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import {
|
||||||
} from "../../../../components/ui/form";
|
} from "../../../../components/ui/form";
|
||||||
import { Input } from "../../../../components/ui/input";
|
import { Input } from "../../../../components/ui/input";
|
||||||
import { Textarea } from "../../../../components/ui/textarea";
|
import { Textarea } from "../../../../components/ui/textarea";
|
||||||
|
import { Switch } from "../../../../components/ui/switch";
|
||||||
import type { RepositoryFormValues } from "../create-repository-form";
|
import type { RepositoryFormValues } from "../create-repository-form";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
@ -72,7 +73,7 @@ export const SftpRepositoryForm = ({ form }: Props) => {
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Path</FormLabel>
|
<FormLabel>Path</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="backups/ironmount" {...field} />
|
<Input placeholder="backups/zerobyte" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>Repository path on the SFTP server. </FormDescription>
|
<FormDescription>Repository path on the SFTP server. </FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|
@ -96,6 +97,48 @@ export const SftpRepositoryForm = ({ form }: Props) => {
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="skipHostKeyCheck"
|
||||||
|
defaultValue={true}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Skip Host Key Verification</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Disable SSH host key checking. Useful for servers with dynamic IPs or self-signed keys.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{!form.watch("skipHostKeyCheck") && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="knownHosts"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Known Hosts</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
placeholder="example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ..."
|
||||||
|
className="font-mono text-xs"
|
||||||
|
rows={3}
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
The contents of the <code>known_hosts</code> file for this server.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ const defaultValuesForType = {
|
||||||
smb: { backend: "smb" as const, port: 445, vers: "3.0" as const },
|
smb: { backend: "smb" as const, port: 445, vers: "3.0" as const },
|
||||||
webdav: { backend: "webdav" as const, port: 80, ssl: false, path: "/webdav" },
|
webdav: { backend: "webdav" as const, port: 80, ssl: false, path: "/webdav" },
|
||||||
rclone: { backend: "rclone" as const, path: "/" },
|
rclone: { backend: "rclone" as const, path: "/" },
|
||||||
sftp: { backend: "sftp" as const, port: 22, path: "/" },
|
sftp: { backend: "sftp" as const, port: 22, path: "/", skipHostKeyCheck: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, formId, loading, className }: Props) => {
|
export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, formId, loading, className }: Props) => {
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import {
|
||||||
import { Input } from "../../../../components/ui/input";
|
import { Input } from "../../../../components/ui/input";
|
||||||
import { SecretInput } from "../../../../components/ui/secret-input";
|
import { SecretInput } from "../../../../components/ui/secret-input";
|
||||||
import { Textarea } from "../../../../components/ui/textarea";
|
import { Textarea } from "../../../../components/ui/textarea";
|
||||||
|
import { Switch } from "../../../../components/ui/switch";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
form: UseFormReturn<FormValues>;
|
form: UseFormReturn<FormValues>;
|
||||||
|
|
@ -36,7 +37,6 @@ export const SFTPForm = ({ form }: Props) => {
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="port"
|
name="port"
|
||||||
defaultValue={22}
|
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Port</FormLabel>
|
<FormLabel>Port</FormLabel>
|
||||||
|
|
@ -115,6 +115,48 @@ export const SFTPForm = ({ form }: Props) => {
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="skipHostKeyCheck"
|
||||||
|
defaultValue={true}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Skip Host Key Verification</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Disable SSH host key checking. Useful for servers with dynamic IPs or self-signed keys.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{!form.watch("skipHostKeyCheck") && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="knownHosts"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Known Hosts</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
placeholder="example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ..."
|
||||||
|
className="font-mono text-xs"
|
||||||
|
rows={3}
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
The contents of the <code>known_hosts</code> file for this server.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,8 @@ export const sftpRepositoryConfigSchema = type({
|
||||||
user: "string",
|
user: "string",
|
||||||
path: "string",
|
path: "string",
|
||||||
privateKey: "string",
|
privateKey: "string",
|
||||||
|
skipHostKeyCheck: "boolean = true",
|
||||||
|
knownHosts: "string?",
|
||||||
}).and(baseRepositoryConfigSchema);
|
}).and(baseRepositoryConfigSchema);
|
||||||
|
|
||||||
export const repositoryConfigSchemaBase = s3RepositoryConfigSchema
|
export const repositoryConfigSchemaBase = s3RepositoryConfigSchema
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,8 @@ export const sftpConfigSchema = type({
|
||||||
privateKey: "string?",
|
privateKey: "string?",
|
||||||
path: "string",
|
path: "string",
|
||||||
readOnly: "boolean?",
|
readOnly: "boolean?",
|
||||||
|
skipHostKeyCheck: "boolean = true",
|
||||||
|
knownHosts: "string?",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const volumeConfigSchemaBase = nfsConfigSchema
|
export const volumeConfigSchemaBase = nfsConfigSchema
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,11 @@ const getPrivateKeyPath = (mountPath: string) => {
|
||||||
return path.join(SSH_KEYS_DIR, `${name}.key`);
|
return path.join(SSH_KEYS_DIR, `${name}.key`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getKnownHostsPath = (mountPath: string) => {
|
||||||
|
const name = path.basename(mountPath);
|
||||||
|
return path.join(SSH_KEYS_DIR, `${name}.known_hosts`);
|
||||||
|
};
|
||||||
|
|
||||||
const mount = async (config: BackendConfig, mountPath: string) => {
|
const mount = async (config: BackendConfig, mountPath: string) => {
|
||||||
logger.debug(`Mounting SFTP volume ${mountPath}...`);
|
logger.debug(`Mounting SFTP volume ${mountPath}...`);
|
||||||
|
|
||||||
|
|
@ -51,12 +56,18 @@ const mount = async (config: BackendConfig, mountPath: string) => {
|
||||||
"ServerAliveInterval=15",
|
"ServerAliveInterval=15",
|
||||||
"ServerAliveCountMax=3",
|
"ServerAliveCountMax=3",
|
||||||
"allow_other",
|
"allow_other",
|
||||||
"StrictHostKeyChecking=no",
|
|
||||||
"UserKnownHostsFile=/dev/null",
|
|
||||||
"uid=1000",
|
"uid=1000",
|
||||||
"gid=1000",
|
"gid=1000",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (config.skipHostKeyCheck) {
|
||||||
|
options.push("StrictHostKeyChecking=no", "UserKnownHostsFile=/dev/null");
|
||||||
|
} else if (config.knownHosts) {
|
||||||
|
const knownHostsPath = getKnownHostsPath(mountPath);
|
||||||
|
await fs.writeFile(knownHostsPath, config.knownHosts, { mode: 0o600 });
|
||||||
|
options.push(`UserKnownHostsFile=${knownHostsPath}`, "StrictHostKeyChecking=yes");
|
||||||
|
}
|
||||||
|
|
||||||
if (config.readOnly) {
|
if (config.readOnly) {
|
||||||
options.push("ro");
|
options.push("ro");
|
||||||
}
|
}
|
||||||
|
|
@ -126,6 +137,10 @@ const unmount = async (mountPath: string) => {
|
||||||
|
|
||||||
const keyPath = getPrivateKeyPath(mountPath);
|
const keyPath = getPrivateKeyPath(mountPath);
|
||||||
await fs.unlink(keyPath).catch(() => {});
|
await fs.unlink(keyPath).catch(() => {});
|
||||||
|
|
||||||
|
const knownHostsPath = getKnownHostsPath(mountPath);
|
||||||
|
await fs.unlink(knownHostsPath).catch(() => {});
|
||||||
|
|
||||||
await fs.rmdir(mountPath).catch(() => {});
|
await fs.rmdir(mountPath).catch(() => {});
|
||||||
|
|
||||||
logger.info(`SFTP volume at ${mountPath} unmounted successfully.`);
|
logger.info(`SFTP volume at ${mountPath} unmounted successfully.`);
|
||||||
|
|
|
||||||
|
|
@ -159,7 +159,7 @@ export const buildEnv = async (config: RepositoryConfig) => {
|
||||||
}
|
}
|
||||||
case "sftp": {
|
case "sftp": {
|
||||||
const decryptedKey = await cryptoUtils.resolveSecret(config.privateKey);
|
const decryptedKey = await cryptoUtils.resolveSecret(config.privateKey);
|
||||||
const keyPath = path.join("/tmp", `ironmount-ssh-${crypto.randomBytes(8).toString("hex")}`);
|
const keyPath = path.join("/tmp", `zerobyte-ssh-${crypto.randomBytes(8).toString("hex")}`);
|
||||||
|
|
||||||
let normalizedKey = decryptedKey.replace(/\r\n/g, "\n");
|
let normalizedKey = decryptedKey.replace(/\r\n/g, "\n");
|
||||||
if (!normalizedKey.endsWith("\n")) {
|
if (!normalizedKey.endsWith("\n")) {
|
||||||
|
|
@ -176,10 +176,6 @@ export const buildEnv = async (config: RepositoryConfig) => {
|
||||||
env._SFTP_KEY_PATH = keyPath;
|
env._SFTP_KEY_PATH = keyPath;
|
||||||
|
|
||||||
const sshArgs = [
|
const sshArgs = [
|
||||||
"-o",
|
|
||||||
"StrictHostKeyChecking=no",
|
|
||||||
"-o",
|
|
||||||
"UserKnownHostsFile=/dev/null",
|
|
||||||
"-o",
|
"-o",
|
||||||
"LogLevel=VERBOSE",
|
"LogLevel=VERBOSE",
|
||||||
"-o",
|
"-o",
|
||||||
|
|
@ -190,6 +186,15 @@ export const buildEnv = async (config: RepositoryConfig) => {
|
||||||
keyPath,
|
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 fs.writeFile(knownHostsPath, config.knownHosts, { mode: 0o600 });
|
||||||
|
env._SFTP_KNOWN_HOSTS_PATH = knownHostsPath;
|
||||||
|
sshArgs.push("-o", "StrictHostKeyChecking=yes", "-o", `UserKnownHostsFile=${knownHostsPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
if (config.port && config.port !== 22) {
|
if (config.port && config.port !== 22) {
|
||||||
sshArgs.push("-p", String(config.port));
|
sshArgs.push("-p", String(config.port));
|
||||||
}
|
}
|
||||||
|
|
@ -806,8 +811,13 @@ const copy = async (
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cleanupTemporaryKeys = async (config: RepositoryConfig, env: Record<string, string>) => {
|
export const cleanupTemporaryKeys = async (config: RepositoryConfig, env: Record<string, string>) => {
|
||||||
if (config.backend === "sftp" && env._SFTP_KEY_PATH) {
|
if (config.backend === "sftp") {
|
||||||
await fs.unlink(env._SFTP_KEY_PATH).catch(() => {});
|
if (env._SFTP_KEY_PATH) {
|
||||||
|
await fs.unlink(env._SFTP_KEY_PATH).catch(() => {});
|
||||||
|
}
|
||||||
|
if (env._SFTP_KNOWN_HOSTS_PATH) {
|
||||||
|
await fs.unlink(env._SFTP_KNOWN_HOSTS_PATH).catch(() => {});
|
||||||
|
}
|
||||||
} else if (config.isExistingRepository && config.customPassword && env.RESTIC_PASSWORD_FILE) {
|
} else if (config.isExistingRepository && config.customPassword && env.RESTIC_PASSWORD_FILE) {
|
||||||
await fs.unlink(env.RESTIC_PASSWORD_FILE).catch(() => {});
|
await fs.unlink(env.RESTIC_PASSWORD_FILE).catch(() => {});
|
||||||
} else if (config.backend === "gcs" && env.GOOGLE_APPLICATION_CREDENTIALS) {
|
} else if (config.backend === "gcs" && env.GOOGLE_APPLICATION_CREDENTIALS) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue