fix(nginx): only write/reload when certs change; derive primary domain and guard SSL directives

- WriteServerCertToDisk: read existing cert/key from disk and compare with DB values; skip writing when unchanged. After writing, run envsubst-on-templates.sh and reload nginx with try/catch and logging.
- envsubst-on-templates.sh: derive PRIMARY_DOMAIN from HOST when not set, compute cert/key paths, and only export PROVISION_SSL_* directives when certificate files exist (otherwise clear directives and log).
This commit is contained in:
Nawaz Dhandala
2025-11-04 19:44:55 +00:00
parent 3d2bcfa579
commit 158663c44b
3 changed files with 154 additions and 8 deletions

View File

@@ -6,6 +6,7 @@ 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";
import NginxConfigurator from "../Utils/NginxConfigurator";
const JOB_NAME: string = "CoreSSL:WritePrimaryHostCertificateToDisk";
const SERVER_CERTS_DIRECTORY: string = "/etc/nginx/certs/ServerCerts";
@@ -69,15 +70,58 @@ export default class WriteServerCertToDiskJob {
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());
const certificatePem: string = certificate.certificate.toString();
const certificateKeyPem: string = certificate.certificateKey.toString();
const existingCertificate: string | null =
(await LocalFile.doesFileExist(certificatePath))
? await LocalFile.read(certificatePath)
: null;
const existingKey: string | null =
(await LocalFile.doesFileExist(keyPath))
? await LocalFile.read(keyPath)
: null;
const certificateChanged: boolean =
existingCertificate !== certificatePem ||
existingKey !== certificateKeyPem;
if (!certificateChanged) {
logger.debug(
`${JOB_NAME}: certificate for ${hostnameOnly} already up to date; no changes written.`,
);
try {
await NginxConfigurator.ensurePrimarySslConfigured({
hostname: hostnameOnly,
forceReload: false,
});
} catch (err) {
logger.error(
`${JOB_NAME}: failed to ensure nginx configuration for ${hostnameOnly} while certificate unchanged.`,
);
logger.error(err);
}
return;
}
await LocalFile.write(certificatePath, certificatePem);
await LocalFile.write(keyPath, certificateKeyPem);
logger.debug(
`${JOB_NAME}: wrote certificate for ${hostnameOnly} to disk.`,
);
try {
await NginxConfigurator.ensurePrimarySslConfigured({
hostname: hostnameOnly,
forceReload: true,
});
} catch (err) {
logger.error(
`${JOB_NAME}: failed to reload nginx after writing certificate for ${hostnameOnly}.`,
);
logger.error(err);
}
},
});
}

View File

@@ -0,0 +1,79 @@
import Exec from "Common/Server/Utils/Execute";
import LocalFile from "Common/Server/Utils/LocalFile";
import logger from "Common/Server/Utils/Logger";
export interface EnsurePrimarySslOptions {
hostname: string;
forceReload?: boolean;
}
export default class NginxConfigurator {
private static readonly DEFAULT_CONF_PATH: string =
"/etc/nginx/conf.d/default.conf";
private static readonly ENVSUBST_SCRIPT_PATH: string =
"/etc/nginx/envsubst-on-templates.sh";
public static async ensurePrimarySslConfigured(
options: EnsurePrimarySslOptions,
): Promise<void> {
const normalizedHost: string = options.hostname.trim().toLowerCase();
if (!normalizedHost) {
logger.warn(
"[NginxConfigurator] Cannot configure SSL because hostname is empty.",
);
return;
}
const certificateDirective: string =
`ssl_certificate /etc/nginx/certs/ServerCerts/${normalizedHost}.crt;`;
let nginxConfig: string = "";
try {
nginxConfig = await LocalFile.read(this.DEFAULT_CONF_PATH);
} catch (err) {
logger.debug(
`[NginxConfigurator] Unable to read ${this.DEFAULT_CONF_PATH}; regenerating configuration.`,
);
logger.debug(err);
}
const templateHasDirective: boolean = nginxConfig.includes(
certificateDirective,
);
const shouldRefreshTemplate: boolean = !templateHasDirective;
const shouldReload: boolean = options.forceReload === true || shouldRefreshTemplate;
if (!shouldReload) {
return;
}
const originalPrimaryDomain: string | undefined =
process.env["PRIMARY_DOMAIN"];
try {
process.env["PRIMARY_DOMAIN"] = normalizedHost;
if (shouldRefreshTemplate) {
await Exec.executeCommand(this.ENVSUBST_SCRIPT_PATH);
}
await Exec.executeCommand("nginx -s reload");
logger.info(
`[NginxConfigurator] Reloaded nginx after updating certificate for ${normalizedHost}.`,
);
} catch (err) {
logger.error(
"[NginxConfigurator] Failed to reload nginx after certificate update.",
);
logger.error(err);
throw err;
} finally {
if (originalPrimaryDomain !== undefined) {
process.env["PRIMARY_DOMAIN"] = originalPrimaryDomain;
} else {
delete process.env["PRIMARY_DOMAIN"];
}
}
}
}

View File

@@ -4,11 +4,34 @@ set -e
ME=$(basename $0)
PRIMARY_DOMAIN_LOWER=""
PRIMARY_DOMAIN_LOG_LABEL="primary-domain-not-set"
if [ -n "${PRIMARY_DOMAIN}" ]; then
PRIMARY_DOMAIN_LOWER=$(printf '%s' "${PRIMARY_DOMAIN}" | tr '[:upper:]' '[:lower:]')
PRIMARY_DOMAIN_LOG_LABEL="${PRIMARY_DOMAIN_LOWER}"
fi
SERVER_CERT_DIRECTORY="/etc/nginx/certs/ServerCerts"
SERVER_CERT_PATH=""
SERVER_CERT_KEY_PATH=""
if [ -n "${PRIMARY_DOMAIN_LOWER}" ]; then
SERVER_CERT_PATH="${SERVER_CERT_DIRECTORY}/${PRIMARY_DOMAIN_LOWER}.crt"
SERVER_CERT_KEY_PATH="${SERVER_CERT_DIRECTORY}/${PRIMARY_DOMAIN_LOWER}.key"
fi
# Prepare conditional SSL directives for templates that need them.
if [ -n "${PROVISION_SSL}" ]; then
export PROVISION_SSL_LISTEN_DIRECTIVE=" listen ${NGINX_LISTEN_ADDRESS}7850 ssl ${NGINX_LISTEN_OPTIONS};"
export PROVISION_SSL_CERTIFICATE_DIRECTIVE=" ssl_certificate /etc/nginx/certs/ServerCerts/${PRIMARY_DOMAIN}.crt;"
export PROVISION_SSL_CERTIFICATE_KEY_DIRECTIVE=" ssl_certificate_key /etc/nginx/certs/ServerCerts/${PRIMARY_DOMAIN}.key;"
if [ -n "${SERVER_CERT_PATH}" ] && [ -f "${SERVER_CERT_PATH}" ] && [ -f "${SERVER_CERT_KEY_PATH}" ]; then
export PROVISION_SSL_LISTEN_DIRECTIVE=" listen ${NGINX_LISTEN_ADDRESS}7850 ssl ${NGINX_LISTEN_OPTIONS};"
export PROVISION_SSL_CERTIFICATE_DIRECTIVE=" ssl_certificate ${SERVER_CERT_PATH};"
export PROVISION_SSL_CERTIFICATE_KEY_DIRECTIVE=" ssl_certificate_key ${SERVER_CERT_KEY_PATH};"
else
echo "$ME: SSL provisioning enabled but certificate not yet available for '${PRIMARY_DOMAIN_LOG_LABEL}'. Skipping HTTPS directives until certificate exists."
export PROVISION_SSL_LISTEN_DIRECTIVE=""
export PROVISION_SSL_CERTIFICATE_DIRECTIVE=""
export PROVISION_SSL_CERTIFICATE_KEY_DIRECTIVE=""
fi
else
export PROVISION_SSL_LISTEN_DIRECTIVE=""
export PROVISION_SSL_CERTIFICATE_DIRECTIVE=""