mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
feat(coressl): add automated Let's Encrypt provisioning for primary host
- add ProvisionPrimaryDomain worker job to order/renew ACME certificates for the HOST - register job import in Worker Routes - add ProvisionSsl env flag in Common/Server/EnvironmentConfig - expose PROVISION_SSL in Helm chart (values.yaml, values.schema.json, _helpers.tpl)
This commit is contained in:
@@ -276,6 +276,8 @@ export const HttpProtocol: Protocol =
|
||||
|
||||
export const Host: string = process.env["HOST"] || "";
|
||||
|
||||
export const ProvisionSsl: boolean = process.env["PROVISION_SSL"] === "true";
|
||||
|
||||
export const WorkflowScriptTimeoutInMS: number = process.env[
|
||||
"WORKFLOW_SCRIPT_TIMEOUT_IN_MS"
|
||||
]
|
||||
|
||||
@@ -49,6 +49,8 @@ Usage:
|
||||
value: {{ $.Values.slackApp.clientId | quote }}
|
||||
- name: HOST
|
||||
value: {{ $.Values.host }}
|
||||
- name: PROVISION_SSL
|
||||
value: {{ ternary "true" "false" $.Values.provisionSSL | quote }}
|
||||
- name: STATUS_PAGE_CNAME_RECORD
|
||||
value: {{ $.Values.statusPage.cnameRecord }}
|
||||
- name: ALLOWED_ACTIVE_MONITOR_COUNT_IN_FREE_PLAN
|
||||
|
||||
@@ -22,6 +22,10 @@
|
||||
"type": "string",
|
||||
"enum": ["http", "https"]
|
||||
},
|
||||
"provisionSSL": {
|
||||
"type": "boolean",
|
||||
"description": "Automatically provision a Let's Encrypt certificate for the primary host"
|
||||
},
|
||||
"oneuptimeSecret": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
|
||||
@@ -5,6 +5,8 @@ global:
|
||||
# Please change this to the domain name / IP where OneUptime server is hosted on.
|
||||
host: localhost
|
||||
httpProtocol: http
|
||||
# Automatically provision a Let's Encrypt certificate for the primary host when set to true.
|
||||
provisionSSL: false
|
||||
|
||||
# Important: You do need to set this to a long random values if you're using OneUptime in production.
|
||||
# Please set this to string.
|
||||
|
||||
94
Worker/Jobs/CoreSsl/ProvisionPrimaryDomain.ts
Normal file
94
Worker/Jobs/CoreSsl/ProvisionPrimaryDomain.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import RunCron from "../../Utils/Cron";
|
||||
import { EVERY_DAY, EVERY_FIFTEEN_MINUTE } from "Common/Utils/CronTime";
|
||||
import {
|
||||
Host,
|
||||
ProvisionSsl,
|
||||
IsDevelopment,
|
||||
} from "Common/Server/EnvironmentConfig";
|
||||
import logger from "Common/Server/Utils/Logger";
|
||||
import Domain from "Common/Types/Domain";
|
||||
import AcmeCertificateService from "Common/Server/Services/AcmeCertificateService";
|
||||
import GreenlockUtil from "Common/Server/Utils/Greenlock/Greenlock";
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
import AcmeCertificate from "Common/Models/DatabaseModels/AcmeCertificate";
|
||||
|
||||
const JOB_NAME: string = "CoreSSL:EnsurePrimaryHostCertificate";
|
||||
|
||||
RunCron(
|
||||
JOB_NAME,
|
||||
{
|
||||
schedule: IsDevelopment ? EVERY_FIFTEEN_MINUTE : EVERY_DAY,
|
||||
runOnStartup: true,
|
||||
timeoutInMS: OneUptimeDate.convertMinutesToMilliseconds(30),
|
||||
},
|
||||
async () => {
|
||||
if (!ProvisionSsl) {
|
||||
logger.debug(`${JOB_NAME}: provisioning disabled. Skipping execution.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedHost: string = Host.trim().toLowerCase();
|
||||
const hostnameOnly: string = normalizedHost.split(":")[0] || "";
|
||||
|
||||
if (!hostnameOnly) {
|
||||
logger.warn(`${JOB_NAME}: HOST environment variable is empty. Unable to provision SSL.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Domain.isValidDomain(hostnameOnly)) {
|
||||
logger.warn(
|
||||
`${JOB_NAME}: HOST "${hostnameOnly}" is not a valid domain. Skipping SSL provisioning.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const existingCertificate: AcmeCertificate | null =
|
||||
await AcmeCertificateService.findOneBy({
|
||||
query: {
|
||||
domain: hostnameOnly,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
expiresAt: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingCertificate?.expiresAt) {
|
||||
const renewalCheckDate: Date = OneUptimeDate.addRemoveDays(
|
||||
OneUptimeDate.getCurrentDate(),
|
||||
30,
|
||||
);
|
||||
|
||||
if (existingCertificate.expiresAt > renewalCheckDate) {
|
||||
logger.debug(
|
||||
`${JOB_NAME}: existing certificate for ${hostnameOnly} is valid until ${existingCertificate.expiresAt.toISOString()}.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`${JOB_NAME}: ordering or renewing certificate for ${hostnameOnly}.`,
|
||||
);
|
||||
|
||||
await GreenlockUtil.orderCert({
|
||||
domain: hostnameOnly,
|
||||
validateCname: async () => {
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(
|
||||
`${JOB_NAME}: certificate successfully issued or renewed for ${hostnameOnly}.`,
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error(`${JOB_NAME}: failed to provision SSL for ${hostnameOnly}.`);
|
||||
logger.error(err);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -62,6 +62,7 @@ import "./Jobs/ServerMonitor/CheckOnlineStatus";
|
||||
|
||||
// // Certs Routers
|
||||
import "./Jobs/StatusPageCerts/StatusPageCerts";
|
||||
import "./Jobs/CoreSsl/ProvisionPrimaryDomain";
|
||||
|
||||
// Status Page Announcements
|
||||
import "./Jobs/StatusPageOwners/SendAnnouncementCreatedNotification";
|
||||
|
||||
Reference in New Issue
Block a user