Files
dokploy/packages/server/src/services/certificate.ts
Mauricio Siu 16b5b7293f fix(security): escape file paths and remote schedule command in shell invocations
quote() the user-derived paths that reach the shell in file mounts
(mount.ts, docker getCreateFileCommand), patch repo read (repoPath/filePath),
certificate create/remove (certificatePath), and the remote scheduled command
(containerId/shellType/command/logPath), so $(), backticks and traversal in
these fields can no longer inject commands.
2026-07-20 16:48:43 -06:00

163 lines
4.2 KiB
TypeScript

import fs from "node:fs";
import path from "node:path";
import { paths } from "@dokploy/server/constants";
import { db } from "@dokploy/server/db";
import {
type apiCreateCertificate,
certificates,
} from "@dokploy/server/db/schema";
import { removeDirectoryIfExistsContent } from "@dokploy/server/utils/filesystem/directory";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { quote } from "shell-quote";
import { stringify } from "yaml";
import type { z } from "zod";
import { encodeBase64 } from "../utils/docker/utils";
import { execAsyncRemote } from "../utils/process/execAsync";
export type Certificate = typeof certificates.$inferSelect;
export const findCertificateById = async (certificateId: string) => {
const certificate = await db.query.certificates.findFirst({
where: eq(certificates.certificateId, certificateId),
});
if (!certificate) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Certificate not found",
});
}
return certificate;
};
export const createCertificate = async (
certificateData: z.infer<typeof apiCreateCertificate>,
organizationId: string,
) => {
const certificate = await db
.insert(certificates)
.values({
...certificateData,
organizationId: organizationId,
})
.returning();
if (!certificate || certificate[0] === undefined) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Failed to create the certificate",
});
}
const cer = certificate[0];
createCertificateFiles(cer);
return cer;
};
export const removeCertificateById = async (certificateId: string) => {
const certificate = await findCertificateById(certificateId);
const { CERTIFICATES_PATH } = paths(!!certificate.serverId);
const certDir = path.join(CERTIFICATES_PATH, certificate.certificatePath);
if (certificate.serverId) {
await execAsyncRemote(certificate.serverId, `rm -rf ${quote([certDir])}`);
} else {
await removeDirectoryIfExistsContent(certDir);
}
const result = await db
.delete(certificates)
.where(eq(certificates.certificateId, certificateId))
.returning();
if (!result) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Failed to delete the certificate",
});
}
return result;
};
const createCertificateFiles = async (certificate: Certificate) => {
const { CERTIFICATES_PATH } = paths(!!certificate.serverId);
const certDir = path.join(CERTIFICATES_PATH, certificate.certificatePath);
const crtPath = path.join(certDir, "chain.crt");
const keyPath = path.join(certDir, "privkey.key");
const chainPath = path.join(certDir, "chain.crt");
const keyPathDocker = path.join(certDir, "privkey.key");
const traefikConfig = {
tls: {
certificates: [
{
certFile: chainPath,
keyFile: keyPathDocker,
},
],
},
};
const yamlConfig = stringify(traefikConfig);
const configFile = path.join(certDir, "certificate.yml");
if (certificate.serverId) {
const certificateData = encodeBase64(certificate.certificateData);
const privateKey = encodeBase64(certificate.privateKey);
const command = `
mkdir -p ${quote([certDir])};
echo "${certificateData}" | base64 -d > ${quote([crtPath])};
echo "${privateKey}" | base64 -d > ${quote([keyPath])};
echo "${yamlConfig}" > ${quote([configFile])};
`;
await execAsyncRemote(certificate.serverId, command);
} else {
if (!fs.existsSync(certDir)) {
fs.mkdirSync(certDir, { recursive: true });
}
fs.writeFileSync(crtPath, certificate.certificateData);
fs.writeFileSync(keyPath, certificate.privateKey);
fs.writeFileSync(configFile, yamlConfig);
}
};
export const updateCertificate = async (
certificateId: string,
updates: {
name?: string;
certificateData?: string;
privateKey?: string;
},
) => {
const updated = await db
.update(certificates)
.set({
...updates,
})
.where(eq(certificates.certificateId, certificateId))
.returning();
if (!updated || updated[0] === undefined) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Failed to update the certificate",
});
}
const cert = updated[0];
// If cert data or private key changed, rewrite files
if (updates.certificateData || updates.privateKey) {
await createCertificateFiles(cert);
}
return cert;
};