mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
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:
@@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
79
Nginx/Utils/NginxConfigurator.ts
Normal file
79
Nginx/Utils/NginxConfigurator.ts
Normal 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"];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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=""
|
||||
|
||||
Reference in New Issue
Block a user