From f84434ada4d560ffc730ffc460d50d9e66205e59 Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Mon, 3 Nov 2025 21:27:22 +0000 Subject: [PATCH] feat(nginx,coressl): add job to write primary host TLS certificate to disk and initialize it --- Nginx/Index.ts | 2 + Nginx/Jobs/WriteServerCertToDisk.ts | 73 +++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 Nginx/Jobs/WriteServerCertToDisk.ts diff --git a/Nginx/Index.ts b/Nginx/Index.ts index d03a9fd248..ad18864725 100644 --- a/Nginx/Index.ts +++ b/Nginx/Index.ts @@ -1,5 +1,6 @@ import AcmeWriteCertificatesJob from "./Jobs/AcmeWriteCertificates"; import WriteCustomCertsToDiskJob from "./Jobs/WriteCustomCertsToDisk"; +import WriteServerCertToDiskJob from "./Jobs/WriteServerCertToDisk"; import { PromiseVoidFunction } from "Common/Types/FunctionTypes"; import PostgresAppInstance from "Common/Server/Infrastructure/PostgresDatabase"; import InfrastructureStatus from "Common/Server/Infrastructure/Status"; @@ -37,6 +38,7 @@ const init: PromiseVoidFunction = async (): Promise => { AcmeWriteCertificatesJob.init(); WriteCustomCertsToDiskJob.init(); + WriteServerCertToDiskJob.init(); // add default routes await App.addDefaultRoutes(); diff --git a/Nginx/Jobs/WriteServerCertToDisk.ts b/Nginx/Jobs/WriteServerCertToDisk.ts new file mode 100644 index 0000000000..ca66f070fd --- /dev/null +++ b/Nginx/Jobs/WriteServerCertToDisk.ts @@ -0,0 +1,73 @@ +import { Host, ProvisionSsl } from "Common/Server/EnvironmentConfig"; +import AcmeCertificate from "Common/Models/DatabaseModels/AcmeCertificate"; +import AcmeCertificateService from "Common/Server/Services/AcmeCertificateService"; +import BasicCron from "Common/Server/Utils/BasicCron"; +import LocalFile from "Common/Server/Utils/LocalFile"; +import logger from "Common/Server/Utils/Logger"; +import Domain from "Common/Types/Domain"; +import { EVERY_MINUTE } from "Common/Utils/CronTime"; + +const JOB_NAME: string = "CoreSSL:WritePrimaryHostCertificateToDisk"; +const SERVER_CERTS_DIRECTORY: string = "/etc/nginx/certs/ServerCerts"; + +export default class WriteServerCertToDiskJob { + public static init(): void { + BasicCron({ + jobName: JOB_NAME, + options: { + schedule: EVERY_MINUTE, + runOnStartup: true, + }, + runFunction: async () => { + if (!ProvisionSsl) { + logger.debug(`${JOB_NAME}: SSL provisioning disabled; skipping write.`); + return; + } + + const normalizedHost: string = Host.trim().toLowerCase(); + const hostnameOnly: string = normalizedHost.split(":")[0] || ""; + + if (!hostnameOnly) { + logger.warn(`${JOB_NAME}: HOST environment variable is empty; cannot write certificate.`); + return; + } + + if (!Domain.isValidDomain(hostnameOnly)) { + logger.warn(`${JOB_NAME}: HOST "${hostnameOnly}" is not a valid domain; skipping write.`); + return; + } + + const certificate: AcmeCertificate | null = + await AcmeCertificateService.findOneBy({ + query: { + domain: hostnameOnly, + }, + select: { + certificate: true, + certificateKey: true, + }, + props: { + isRoot: true, + }, + }); + + if (!certificate?.certificate || !certificate.certificateKey) { + logger.debug( + `${JOB_NAME}: certificate data not yet available for ${hostnameOnly}; will retry later.`, + ); + return; + } + + await LocalFile.makeDirectory(SERVER_CERTS_DIRECTORY); + + const certificatePath: string = `${SERVER_CERTS_DIRECTORY}/${hostnameOnly}.crt`; + const keyPath: string = `${SERVER_CERTS_DIRECTORY}/${hostnameOnly}.key`; + + await LocalFile.write(certificatePath, certificate.certificate.toString()); + await LocalFile.write(keyPath, certificate.certificateKey.toString()); + + logger.debug(`${JOB_NAME}: wrote certificate for ${hostnameOnly} to disk.`); + }, + }); + } +}