From 8c243a02803860b1c5445559f20f2e2881b5cafb Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Sun, 28 Dec 2025 13:08:09 +0100 Subject: [PATCH] feat: allow more secure SFTP connection with known hosts --- app/client/api-client/types.gen.ts | 38 ++++++++++++++++ .../components/create-repository-form.tsx | 2 +- .../repository-forms/sftp-repository-form.tsx | 45 ++++++++++++++++++- .../volumes/components/create-volume-form.tsx | 2 +- .../components/volume-forms/sftp-form.tsx | 44 +++++++++++++++++- app/schemas/restic.ts | 2 + app/schemas/volumes.ts | 2 + .../modules/backends/sftp/sftp-backend.ts | 19 +++++++- app/server/utils/restic.ts | 24 +++++++--- 9 files changed, 165 insertions(+), 13 deletions(-) diff --git a/app/client/api-client/types.gen.ts b/app/client/api-client/types.gen.ts index 80f3efc4..58f79eab 100644 --- a/app/client/api-client/types.gen.ts +++ b/app/client/api-client/types.gen.ts @@ -175,6 +175,8 @@ export type ListVolumesResponses = { path: string; username: string; port?: number; + skipHostKeyCheck?: boolean; + knownHosts?: string; password?: string; privateKey?: string; readOnly?: boolean; @@ -236,6 +238,8 @@ export type CreateVolumeData = { path: string; username: string; port?: number; + skipHostKeyCheck?: boolean; + knownHosts?: string; password?: string; privateKey?: string; readOnly?: boolean; @@ -294,6 +298,8 @@ export type CreateVolumeResponses = { path: string; username: string; port?: number; + skipHostKeyCheck?: boolean; + knownHosts?: string; password?: string; privateKey?: string; readOnly?: boolean; @@ -355,6 +361,8 @@ export type TestConnectionData = { path: string; username: string; port?: number; + skipHostKeyCheck?: boolean; + knownHosts?: string; password?: string; privateKey?: string; readOnly?: boolean; @@ -466,6 +474,8 @@ export type GetVolumeResponses = { path: string; username: string; port?: number; + skipHostKeyCheck?: boolean; + knownHosts?: string; password?: string; privateKey?: string; readOnly?: boolean; @@ -529,6 +539,8 @@ export type UpdateVolumeData = { path: string; username: string; port?: number; + skipHostKeyCheck?: boolean; + knownHosts?: string; password?: string; privateKey?: string; readOnly?: boolean; @@ -596,6 +608,8 @@ export type UpdateVolumeResponses = { path: string; username: string; port?: number; + skipHostKeyCheck?: boolean; + knownHosts?: string; password?: string; privateKey?: string; readOnly?: boolean; @@ -836,8 +850,10 @@ export type ListRepositoriesResponses = { privateKey: string; user: string; port?: number; + skipHostKeyCheck?: boolean; customPassword?: string; isExistingRepository?: boolean; + knownHosts?: string; }; createdAt: number; id: string; @@ -913,8 +929,10 @@ export type CreateRepositoryData = { privateKey: string; user: string; port?: number; + skipHostKeyCheck?: boolean; customPassword?: string; isExistingRepository?: boolean; + knownHosts?: string; }; name: string; compressionMode?: 'auto' | 'max' | 'off'; @@ -1052,8 +1070,10 @@ export type GetRepositoryResponses = { privateKey: string; user: string; port?: number; + skipHostKeyCheck?: boolean; customPassword?: string; isExistingRepository?: boolean; + knownHosts?: string; }; createdAt: number; id: string; @@ -1156,8 +1176,10 @@ export type UpdateRepositoryResponses = { privateKey: string; user: string; port?: number; + skipHostKeyCheck?: boolean; customPassword?: string; isExistingRepository?: boolean; + knownHosts?: string; }; createdAt: number; id: string; @@ -1430,8 +1452,10 @@ export type ListBackupSchedulesResponses = { privateKey: string; user: string; port?: number; + skipHostKeyCheck?: boolean; customPassword?: string; isExistingRepository?: boolean; + knownHosts?: string; }; createdAt: number; id: string; @@ -1479,6 +1503,8 @@ export type ListBackupSchedulesResponses = { path: string; username: string; port?: number; + skipHostKeyCheck?: boolean; + knownHosts?: string; password?: string; privateKey?: string; readOnly?: boolean; @@ -1688,8 +1714,10 @@ export type GetBackupScheduleResponses = { privateKey: string; user: string; port?: number; + skipHostKeyCheck?: boolean; customPassword?: string; isExistingRepository?: boolean; + knownHosts?: string; }; createdAt: number; id: string; @@ -1737,6 +1765,8 @@ export type GetBackupScheduleResponses = { path: string; username: string; port?: number; + skipHostKeyCheck?: boolean; + knownHosts?: string; password?: string; privateKey?: string; readOnly?: boolean; @@ -1927,8 +1957,10 @@ export type GetBackupScheduleForVolumeResponses = { privateKey: string; user: string; port?: number; + skipHostKeyCheck?: boolean; customPassword?: string; isExistingRepository?: boolean; + knownHosts?: string; }; createdAt: number; id: string; @@ -1976,6 +2008,8 @@ export type GetBackupScheduleForVolumeResponses = { path: string; username: string; port?: number; + skipHostKeyCheck?: boolean; + knownHosts?: string; password?: string; privateKey?: string; readOnly?: boolean; @@ -2333,8 +2367,10 @@ export type GetScheduleMirrorsResponses = { privateKey: string; user: string; port?: number; + skipHostKeyCheck?: boolean; customPassword?: string; isExistingRepository?: boolean; + knownHosts?: string; }; createdAt: number; id: string; @@ -2437,8 +2473,10 @@ export type UpdateScheduleMirrorsResponses = { privateKey: string; user: string; port?: number; + skipHostKeyCheck?: boolean; customPassword?: string; isExistingRepository?: boolean; + knownHosts?: string; }; createdAt: number; id: string; diff --git a/app/client/modules/repositories/components/create-repository-form.tsx b/app/client/modules/repositories/components/create-repository-form.tsx index dce3ff52..79760ca2 100644 --- a/app/client/modules/repositories/components/create-repository-form.tsx +++ b/app/client/modules/repositories/components/create-repository-form.tsx @@ -58,7 +58,7 @@ const defaultValuesForType = { azure: { backend: "azure" as const, compressionMode: "auto" as const }, rclone: { backend: "rclone" 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 = ({ diff --git a/app/client/modules/repositories/components/repository-forms/sftp-repository-form.tsx b/app/client/modules/repositories/components/repository-forms/sftp-repository-form.tsx index 321d3ba5..8ed55217 100644 --- a/app/client/modules/repositories/components/repository-forms/sftp-repository-form.tsx +++ b/app/client/modules/repositories/components/repository-forms/sftp-repository-form.tsx @@ -9,6 +9,7 @@ import { } from "../../../../components/ui/form"; import { Input } from "../../../../components/ui/input"; import { Textarea } from "../../../../components/ui/textarea"; +import { Switch } from "../../../../components/ui/switch"; import type { RepositoryFormValues } from "../create-repository-form"; type Props = { @@ -72,7 +73,7 @@ export const SftpRepositoryForm = ({ form }: Props) => { Path - + Repository path on the SFTP server. @@ -96,6 +97,48 @@ export const SftpRepositoryForm = ({ form }: Props) => { )} /> + ( + +
+ Skip Host Key Verification + + Disable SSH host key checking. Useful for servers with dynamic IPs or self-signed keys. + +
+ + + +
+ )} + /> + {!form.watch("skipHostKeyCheck") && ( + ( + + Known Hosts + +