fix: force user to provide known hosts values in sftp volume / repository creation
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled

This commit is contained in:
Nicolas Meienberger 2026-03-31 20:23:45 +02:00
parent aa7eaa0929
commit 3acf565ccd
7 changed files with 49 additions and 25 deletions

View file

@ -52,16 +52,26 @@ const formBaseFields = {
compressionMode: z.enum(COMPRESSION_MODES).optional(),
};
export const formSchema = z.discriminatedUnion("backend", [
localRepositoryConfigSchema.extend(formBaseFields),
s3RepositoryConfigSchema.extend(formBaseFields),
r2RepositoryConfigSchema.extend(formBaseFields),
gcsRepositoryConfigSchema.extend(formBaseFields),
azureRepositoryConfigSchema.extend(formBaseFields),
rcloneRepositoryConfigSchema.extend(formBaseFields),
restRepositoryConfigSchema.extend(formBaseFields),
sftpRepositoryConfigSchema.extend(formBaseFields),
]);
export const formSchema = z
.discriminatedUnion("backend", [
localRepositoryConfigSchema.extend(formBaseFields),
s3RepositoryConfigSchema.extend(formBaseFields),
r2RepositoryConfigSchema.extend(formBaseFields),
gcsRepositoryConfigSchema.extend(formBaseFields),
azureRepositoryConfigSchema.extend(formBaseFields),
rcloneRepositoryConfigSchema.extend(formBaseFields),
restRepositoryConfigSchema.extend(formBaseFields),
sftpRepositoryConfigSchema.extend(formBaseFields),
])
.superRefine((value, ctx) => {
if (value.backend === "sftp" && !value.skipHostKeyCheck && !value.knownHosts?.trim()) {
ctx.addIssue({
code: "custom",
message: "Known hosts are required unless host key verification is skipped",
path: ["knownHosts"],
});
}
});
export type RepositoryFormValues = z.input<typeof formSchema>;

View file

@ -32,14 +32,24 @@ import { useSystemInfo } from "~/client/hooks/use-system-info";
import { useScrollToFormError } from "~/client/hooks/use-scroll-to-form-error";
import { DirectoryForm, NFSForm, SMBForm, WebDAVForm, RcloneForm, SFTPForm } from "./volume-forms";
export const formSchema = z.discriminatedUnion("backend", [
directoryConfigSchema.extend({ name: z.string().min(2).max(32) }),
nfsConfigSchema.extend({ name: z.string().min(2).max(32) }),
smbConfigSchema.extend({ name: z.string().min(2).max(32) }),
webdavConfigSchema.extend({ name: z.string().min(2).max(32) }),
rcloneConfigSchema.extend({ name: z.string().min(2).max(32) }),
sftpConfigSchema.extend({ name: z.string().min(2).max(32) }),
]);
export const formSchema = z
.discriminatedUnion("backend", [
directoryConfigSchema.extend({ name: z.string().min(2).max(32) }),
nfsConfigSchema.extend({ name: z.string().min(2).max(32) }),
smbConfigSchema.extend({ name: z.string().min(2).max(32) }),
webdavConfigSchema.extend({ name: z.string().min(2).max(32) }),
rcloneConfigSchema.extend({ name: z.string().min(2).max(32) }),
sftpConfigSchema.extend({ name: z.string().min(2).max(32) }),
])
.superRefine((value, ctx) => {
if (value.backend === "sftp" && !value.skipHostKeyCheck && !value.knownHosts?.trim()) {
ctx.addIssue({
code: "custom",
message: "Known hosts are required unless host key verification is skipped",
path: ["knownHosts"],
});
}
});
export type FormValues = z.input<typeof formSchema>;

View file

@ -82,7 +82,7 @@ export const sftpConfigSchema = z.object({
privateKey: z.string().optional(),
path: z.string().min(1),
readOnly: z.boolean().optional(),
skipHostKeyCheck: z.boolean().default(true),
skipHostKeyCheck: z.boolean().default(false),
knownHosts: z.string().optional(),
});

View file

@ -60,12 +60,14 @@ const mount = async (config: BackendConfig, mountPath: string) => {
`gid=${gid}`,
];
if (config.skipHostKeyCheck || !config.knownHosts) {
if (config.skipHostKeyCheck) {
options.push("StrictHostKeyChecking=no", "UserKnownHostsFile=/dev/null");
} else if (config.knownHosts) {
const knownHostsPath = getKnownHostsPath(mountPath);
await writeFileWithMode(knownHostsPath, config.knownHosts, FILE_MODES.ownerReadWrite);
options.push(`UserKnownHostsFile=${knownHostsPath}`, "StrictHostKeyChecking=yes");
} else {
options.push("StrictHostKeyChecking=yes");
}
if (config.readOnly) {

View file

@ -317,11 +317,11 @@ describe("buildEnv", () => {
expect(env._SFTP_SSH_ARGS).toContain("UserKnownHostsFile=/dev/null");
});
test("uses StrictHostKeyChecking=no when knownHosts is absent", async () => {
test("uses StrictHostKeyChecking=yes when knownHosts is absent", async () => {
const env = await buildEnvForTest({ ...baseSftpConfig, skipHostKeyCheck: false, knownHosts: undefined }, "org-1");
expect(env._SFTP_SSH_ARGS).toContain("StrictHostKeyChecking=no");
expect(env._SFTP_SSH_ARGS).toContain("UserKnownHostsFile=/dev/null");
expect(env._SFTP_SSH_ARGS).toContain("StrictHostKeyChecking=yes");
expect(env._SFTP_SSH_ARGS).not.toContain("UserKnownHostsFile=/dev/null");
});
test("uses StrictHostKeyChecking=yes and a known hosts file when knownHosts is provided", async () => {

View file

@ -114,13 +114,15 @@ export const buildEnv = async (
keyPath,
];
if (config.skipHostKeyCheck || !config.knownHosts) {
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) {

View file

@ -116,7 +116,7 @@ export const sftpRepositoryConfigSchema = z
user: z.string().min(1),
path: z.string().min(1),
privateKey: z.string().min(1),
skipHostKeyCheck: z.boolean().default(true),
skipHostKeyCheck: z.boolean().default(false),
knownHosts: z.string().optional(),
})
.extend(baseRepositoryConfigSchema.shape);