diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 438a2575b..b3aa6467c 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -634,7 +634,7 @@ "option": { "dockerHost": { "label": "Docker host", - "placeholder": "unix:///var/run/docker.sock or ssh://user@host" + "placeholder": "unix:///var/run/docker.sock" }, "host": { "label": "Host", @@ -716,7 +716,7 @@ "http": "Enter the URL or IP to monitor (e.g., https://example.com/ or 192.168.1.100) and add a clear display name that appears on the dashboard. Set an optional proxy server to connect through.", "ping": "Enter the IP address or hostname to ping (e.g., 192.168.1.100 or example.com) and add a clear display name that appears on the dashboard.", "port": "Enter the URL or IP of the server, the port number and a clear display name that appears on the dashboard.", - "docker": "Enter the Docker host to monitor: a local socket (unix:///var/run/docker.sock or /var/run/docker.sock) or a remote host over SSH (ssh://user@host, with an optional :port). Every container on the host is monitored.", + "docker": "Enter the Docker socket to monitor, as unix:///var/run/docker.sock or an absolute path such as /var/run/docker.sock. Every container on the host is monitored.", "game": "Enter the IP address or hostname and the port number to ping (e.g., 192.168.1.100 or example.com) and choose game type. Some games require enabling server queries in their config file — check your game server's documentation. See the supported games list for protocol details.", "pagespeed": "Track page load performance, Core Web Vitals, and optimization scores for your website.", "grpc": "Enter the hostname and port of the gRPC server, optionally specify a service name for the health check, and add a display name.", diff --git a/server/src/api/validation/monitorValidation.ts b/server/src/api/validation/monitorValidation.ts index c11df8f80..ddea832ef 100644 --- a/server/src/api/validation/monitorValidation.ts +++ b/server/src/api/validation/monitorValidation.ts @@ -120,22 +120,15 @@ const refineProxySelection = (body: { proxyMode?: string; proxyId?: string }, ct } }; -const dockerUrlRegex = /^(?:ssh:\/\/[^@\s]+@[^\s/:@]+(?::\d{1,5})?\/?|unix:\/\/\/\S+|\/\S+)$/; +const dockerUrlRegex = /^(?:unix:\/\/\/\S+|\/\S+)$/; -const refineDockerUrl = (data: { type?: string; url?: string; sshPrivateKey?: string }, ctx: z.RefinementCtx) => { +const refineDockerUrl = (data: { type?: string; url?: string }, ctx: z.RefinementCtx) => { if (data.type !== "docker" || data.url === undefined) return; if (!dockerUrlRegex.test(data.url)) { ctx.addIssue({ code: "custom", path: ["url"], - message: "Docker host must be ssh://user@host[:port], unix:///path, or an absolute socket path", - }); - } - if (data.url.startsWith("ssh://") && !data.sshPrivateKey) { - ctx.addIssue({ - code: "custom", - path: ["sshPrivateKey"], - message: "SSH Docker hosts require a private key", + message: "Docker host must be unix:///path or an absolute socket path", }); } }; @@ -164,7 +157,6 @@ export const createMonitorBodyValidation = z tags: z.array(z.string()).optional(), customUpCodes: z.array(httpStatusCode).default([]), secret: z.string().optional(), - sshPrivateKey: z.string().optional(), jsonPath: z.union([z.string(), z.literal("")]).optional(), expectedValue: z.union([z.string(), z.literal("")]).optional(), matchMethod: z.union([z.enum(MonitorMatchMethods), z.literal("")]).optional(), @@ -201,7 +193,6 @@ export const editMonitorBodyValidation = z tags: z.array(z.string()).optional(), customUpCodes: z.array(httpStatusCode).optional(), secret: z.string().optional(), - sshPrivateKey: z.string().optional(), ignoreTlsErrors: z.boolean().optional(), proxyMode: z.enum(ProxyModes).optional(), proxyId: proxyIdValidation, @@ -283,7 +274,6 @@ const importedMonitorSchema = z tags: z.array(z.string()).default([]), customUpCodes: z.array(httpStatusCode).default([]), secret: z.string().optional(), - sshPrivateKey: z.string().optional(), cpuAlertThreshold: z.number().default(100), cpuAlertCounter: z.number().default(5), memoryAlertThreshold: z.number().default(100), @@ -379,7 +369,6 @@ export const monitorResponseSchema = z tags: z.array(z.string()), customUpCodes: z.array(httpStatusCode).optional(), secret: z.string().optional(), - sshPrivateKey: z.string().optional(), cpuAlertThreshold: z.number(), memoryAlertThreshold: z.number(), diskAlertThreshold: z.number(), diff --git a/server/src/db/migration/0012_migrateDockerMonitorUrls.ts b/server/src/db/migration/0012_migrateDockerMonitorUrls.ts index 75f3d5acf..d4d3bd143 100644 --- a/server/src/db/migration/0012_migrateDockerMonitorUrls.ts +++ b/server/src/db/migration/0012_migrateDockerMonitorUrls.ts @@ -7,8 +7,8 @@ import type { ILogger } from "@/utils/logger.js"; * The old docker provider interpreted monitor.url as a container name/ID and only * ever talked to the local socket, so every pre-existing docker monitor's host is * definitionally unix:///var/run/docker.sock. The container name is preserved in - * the description. Urls already in host form (unix://, ssh://, or an absolute - * socket path) are left untouched, which makes the migration idempotent. + * the description. Urls already in host form (unix:// or an absolute socket + * path) are left untouched, which makes the migration idempotent. */ export async function migrateDockerMonitorUrls(logger: ILogger): Promise { const SERVICE_NAME = "Migration:MigrateDockerMonitorUrls"; @@ -16,7 +16,7 @@ export async function migrateDockerMonitorUrls(logger: ILogger): Promise { try { logger.info({ service: SERVICE_NAME, message: "Starting docker monitor url migration" }); - const result = await MonitorModel.updateMany({ type: "docker", url: { $not: /^((unix|ssh):\/\/|\/)/ } }, [ + const result = await MonitorModel.updateMany({ type: "docker", url: { $not: /^(unix:\/\/|\/)/ } }, [ { $set: { description: { diff --git a/server/src/domain/monitors/monitor.model.ts b/server/src/domain/monitors/monitor.model.ts index f45b613a2..2517f5d8e 100644 --- a/server/src/domain/monitors/monitor.model.ts +++ b/server/src/domain/monitors/monitor.model.ts @@ -235,9 +235,6 @@ const MonitorSchema = new Schema( secret: { type: String, }, - sshPrivateKey: { - type: String, - }, cpuAlertThreshold: { type: Number, default: 100, diff --git a/server/src/domain/monitors/monitor.repository.mongo.ts b/server/src/domain/monitors/monitor.repository.mongo.ts index d31d830ac..db6b79853 100644 --- a/server/src/domain/monitors/monitor.repository.mongo.ts +++ b/server/src/domain/monitors/monitor.repository.mongo.ts @@ -436,7 +436,6 @@ class MongoMonitorsRepository implements IMonitorsRepository { tags: tagIds, customUpCodes: doc.customUpCodes ?? [], secret: doc.secret ?? undefined, - sshPrivateKey: doc.sshPrivateKey ?? undefined, cpuAlertThreshold: doc.cpuAlertThreshold, cpuAlertCounter: doc.cpuAlertCounter, memoryAlertThreshold: doc.memoryAlertThreshold, diff --git a/server/src/domain/monitors/monitor.type.ts b/server/src/domain/monitors/monitor.type.ts index 587112c24..12fcd1036 100644 --- a/server/src/domain/monitors/monitor.type.ts +++ b/server/src/domain/monitors/monitor.type.ts @@ -92,7 +92,6 @@ export interface Monitor { tags: string[]; customUpCodes: HttpStatusCode[]; secret?: string; - sshPrivateKey?: string; cpuAlertThreshold: number; cpuAlertCounter: number; memoryAlertThreshold: number; diff --git a/server/src/service/network/DockerProvider.ts b/server/src/service/network/DockerProvider.ts index 38bb59fef..776da3f92 100644 --- a/server/src/service/network/DockerProvider.ts +++ b/server/src/service/network/DockerProvider.ts @@ -63,31 +63,7 @@ export class DockerProvider implements IStatusProvider { return { socketPath }; } if (url.startsWith("/")) return { socketPath: url }; - const match = url.match(/^ssh:\/\/(?:([^@\s]+)@)?([^\s/:@]+)(?::(\d{1,5}))?\/?$/); - if (!match) throw new AppError({ message: `Invalid Docker host URL: ${url}`, status: 422, service: SERVICE_NAME, method: "toDockerOptions" }); - const [, username, host, port] = match; - if (!username) - throw new AppError({ - message: "SSH Docker host URL requires a user: ssh://user@host", - status: 422, - service: SERVICE_NAME, - method: "toDockerOptions", - }); - - if (!monitor.sshPrivateKey) - throw new AppError({ - message: "SSH Docker host requires a private key", - status: 422, - service: SERVICE_NAME, - method: "toDockerOptions", - }); - return { - protocol: "ssh", - host, - port: port ? Number(port) : 22, - username, - sshOptions: { privateKey: monitor.sshPrivateKey }, - }; + throw new AppError({ message: `Invalid Docker host URL: ${url}`, status: 422, service: SERVICE_NAME, method: "toDockerOptions" }); }; private async mapWithConcurrency(items: T[], limit: number, fn: (item: T) => Promise): Promise { diff --git a/server/test/unit/providers/network/dockerProvider.test.ts b/server/test/unit/providers/network/dockerProvider.test.ts index ba9d90568..4c27985ef 100644 --- a/server/test/unit/providers/network/dockerProvider.test.ts +++ b/server/test/unit/providers/network/dockerProvider.test.ts @@ -9,8 +9,6 @@ import { DOCKER_LOG_TAIL_LINES } from "../../../../src/domain/docker/docker.type // ── Fixtures ───────────────────────────────────────────────────────────────── -const PRIVATE_KEY = "-----BEGIN OPENSSH PRIVATE KEY-----\nfake\n-----END OPENSSH PRIVATE KEY-----"; - const makeMonitor = (overrides?: Partial): Monitor => ({ id: "mon-1", @@ -116,28 +114,6 @@ describe("DockerProvider", () => { expect(DockerLib).toHaveBeenCalledWith({ socketPath: "/var/run/docker.sock" }); }); - - it("parses ssh urls with a private key, defaulting to port 22", async () => { - const { provider, DockerLib } = setup(); - - await provider.handle(makeMonitor({ url: "ssh://deploy@prod-swarm-01.internal", sshPrivateKey: PRIVATE_KEY })); - - expect(DockerLib).toHaveBeenCalledWith({ - protocol: "ssh", - host: "prod-swarm-01.internal", - port: 22, - username: "deploy", - sshOptions: { privateKey: PRIVATE_KEY }, - }); - }); - - it("parses an explicit ssh port", async () => { - const { provider, DockerLib } = setup(); - - await provider.handle(makeMonitor({ url: "ssh://deploy@host:2222", sshPrivateKey: PRIVATE_KEY })); - - expect(DockerLib).toHaveBeenCalledWith(expect.objectContaining({ port: 2222 })); - }); }); // ── Fail-loudly url validation ─────────────────────────────────────── @@ -151,7 +127,7 @@ describe("DockerProvider", () => { ["a container name (old semantics)", "my-container", "Invalid Docker host URL"], ["tcp engine urls (unsupported)", "tcp://host:2375", "Invalid Docker host URL"], ["https engine urls (unsupported)", "https://host", "Invalid Docker host URL"], - ["ssh without a user", "ssh://host", "SSH Docker host URL requires a user: ssh://user@host"], + ["ssh engine urls (unsupported)", "ssh://deploy@host", "Invalid Docker host URL"], ]; it.each(invalidUrls)("throws AppError for %s and never constructs a client", async (_label, url, message) => { @@ -161,15 +137,6 @@ describe("DockerProvider", () => { expect(DockerLib).not.toHaveBeenCalled(); }); - it("throws AppError for an ssh url with no private key", async () => { - const { provider, DockerLib } = setup(); - - await expect(provider.handle(makeMonitor({ url: "ssh://deploy@host", sshPrivateKey: undefined }))).rejects.toThrow( - "SSH Docker host requires a private key" - ); - expect(DockerLib).not.toHaveBeenCalled(); - }); - it("preserves the toDockerOptions AppError through handle's catch", async () => { const { provider } = setup(); diff --git a/server/test/unit/validation/monitorValidation.test.ts b/server/test/unit/validation/monitorValidation.test.ts index bae34f830..1cd224ab5 100644 --- a/server/test/unit/validation/monitorValidation.test.ts +++ b/server/test/unit/validation/monitorValidation.test.ts @@ -608,7 +608,6 @@ describe("monitorValidation — Docker host url", () => { type: "docker" as const, url: "unix:///var/run/docker.sock", }; - const PRIVATE_KEY = "-----BEGIN OPENSSH PRIVATE KEY-----\nfake\n-----END OPENSSH PRIVATE KEY-----"; describe("createMonitorBodyValidation", () => { it("accepts the default unix socket url", () => { @@ -621,32 +620,15 @@ describe("monitorValidation — Docker host url", () => { expect(parsed.url).toBe("/run/docker.sock"); }); - it("accepts an ssh url with a private key and retains the key", () => { - for (const url of ["ssh://deploy@host", "ssh://deploy@host:2222"]) { - const parsed = createMonitorBodyValidation.parse({ ...baseDockerBody, url, sshPrivateKey: PRIVATE_KEY }); - expect(parsed.sshPrivateKey).toBe(PRIVATE_KEY); - } - }); - it("rejects container names and unsupported engine urls", () => { - for (const badUrl of ["my-container", "tcp://host:2375", "https://host", "unix://relative/path", ""]) { + for (const badUrl of ["my-container", "tcp://host:2375", "https://host", "ssh://deploy@host", "unix://relative/path", ""]) { expect(() => createMonitorBodyValidation.parse({ ...baseDockerBody, url: badUrl })).toThrow(); } }); - it("rejects an ssh url without a user", () => { - expect(() => createMonitorBodyValidation.parse({ ...baseDockerBody, url: "ssh://host", sshPrivateKey: PRIVATE_KEY })).toThrow(); - }); - - it("rejects an ssh url without a private key, attributing the issue to sshPrivateKey", () => { - const result = createMonitorBodyValidation.safeParse({ ...baseDockerBody, url: "ssh://deploy@host" }); - expect(result.success).toBe(false); - expect(result.error?.issues.some((issue) => issue.path.includes("sshPrivateKey"))).toBe(true); - }); - it("does not apply docker url rules to other monitor types", () => { const parsed = createMonitorBodyValidation.parse({ name: "HTTP check", type: "http", url: "https://example.com" }); - expect(parsed.sshPrivateKey).toBeUndefined(); + expect(parsed.url).toBe("https://example.com"); }); }); @@ -660,7 +642,7 @@ describe("monitorValidation — Docker host url", () => { expect(() => editMonitorBodyValidation.parse({ type: "docker", url: "my-container" })).toThrow(); }); - it("rejects a docker edit switching to ssh without a key", () => { + it("rejects a docker edit with an ssh url", () => { expect(() => editMonitorBodyValidation.parse({ type: "docker", url: "ssh://deploy@host" })).toThrow(); }); });