mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
feat(ssl): add automatic Let's Encrypt provisioning for primary OneUptime host
- Introduce ENABLE_SSL_PROVIONING_FOR_ONEUPTIME env flag (EnvironmentConfig, docker-compose, config.example) - Add Helm chart support: values.yaml, values.schema.json, _helpers.tpl, and README entry - Add DB fields to GlobalConfig for oneuptime SSL certificate, key, issuedAt and expiresAt - Implement OneuptimeSslCertificateService to request/renew certs and persist to GlobalConfig - Add worker cron (OneuptimeCerts) to ensure provisioning runs regularly - Add WriteOneuptimeCertToDisk job and hook into Nginx startup to write certs to /etc/nginx/certs/OneUptime - Update Nginx templates and run.sh to load certificate directives, serve ACME challenge endpoint and handle redirects - Extend Greenlock.orderCert to support onCertificateIssued callback and optional persistence of ACME certificates - Minor: update .gitignore to include OneUptime cert paths
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -76,6 +76,8 @@ logs.txt
|
||||
|
||||
Certs/StatusPageCerts/*.crt
|
||||
Certs/StatusPageCerts/*.key
|
||||
Certs/OneUptime/*.crt
|
||||
Certs/OneUptime/*.key
|
||||
|
||||
Certs/ServerCerts/*.crt
|
||||
Certs/ServerCerts/*.key
|
||||
|
||||
@@ -497,4 +497,74 @@ export default class GlobalConfig extends GlobalConfigModel {
|
||||
transformer: Email.getDatabaseTransformer(),
|
||||
})
|
||||
public adminNotificationEmail?: Email = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.VeryLongText,
|
||||
title: "OneUptime SSL Certificate",
|
||||
description:
|
||||
"TLS certificate issued by Let's Encrypt for the primary OneUptime host.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.VeryLongText,
|
||||
nullable: true,
|
||||
unique: false,
|
||||
})
|
||||
public oneuptimeSslCertificate?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.VeryLongText,
|
||||
title: "OneUptime SSL Certificate Key",
|
||||
description:
|
||||
"Private key that pairs with the primary OneUptime TLS certificate.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.VeryLongText,
|
||||
nullable: true,
|
||||
unique: false,
|
||||
})
|
||||
public oneuptimeSslCertificateKey?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.Date,
|
||||
title: "OneUptime SSL Issued At",
|
||||
description: "Date when the primary TLS certificate was issued.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Date,
|
||||
nullable: true,
|
||||
unique: false,
|
||||
})
|
||||
public oneuptimeSslIssuedAt?: Date = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.Date,
|
||||
title: "OneUptime SSL Expires At",
|
||||
description: "Expiry date of the primary OneUptime TLS certificate.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Date,
|
||||
nullable: true,
|
||||
unique: false,
|
||||
})
|
||||
public oneuptimeSslExpiresAt?: Date = undefined;
|
||||
}
|
||||
|
||||
@@ -239,6 +239,9 @@ export const ClickhousePort: Port = new Port(
|
||||
process.env["CLICKHOUSE_PORT"] || "8123",
|
||||
);
|
||||
|
||||
export const EnableSslProvisioningForOneuptime: boolean =
|
||||
process.env["ENABLE_SSL_PROVIONING_FOR_ONEUPTIME"] === "true";
|
||||
|
||||
export const ClickhouseUsername: string =
|
||||
process.env["CLICKHOUSE_USER"] || "default";
|
||||
|
||||
|
||||
143
Common/Server/Services/OneuptimeSslCertificateService.ts
Normal file
143
Common/Server/Services/OneuptimeSslCertificateService.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import GlobalConfigService from "./GlobalConfigService";
|
||||
import GlobalConfig from "../../Models/DatabaseModels/GlobalConfig";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import Hostname from "../../Types/API/Hostname";
|
||||
import OneUptimeDate from "../../Types/Date";
|
||||
import logger from "../Utils/Logger";
|
||||
import GreenlockUtil from "../Utils/Greenlock/Greenlock";
|
||||
import {
|
||||
EnableSslProvisioningForOneuptime,
|
||||
Host,
|
||||
} from "../EnvironmentConfig";
|
||||
|
||||
export default class OneuptimeSslCertificateService {
|
||||
private static isProvisioning: boolean = false;
|
||||
|
||||
public static async ensureCertificateProvisioned(): Promise<void> {
|
||||
if (!EnableSslProvisioningForOneuptime) {
|
||||
logger.debug(
|
||||
"Oneuptime SSL provisioning is disabled. Skipping certificate check.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let host: string = Host.trim();
|
||||
|
||||
if (!host) {
|
||||
logger.error(
|
||||
"ENABLE_SSL_PROVIONING_FOR_ONEUPTIME is true but HOST is not set. Skipping certificate provisioning.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
host = Hostname.fromString(host).toString().toLowerCase();
|
||||
} catch (err) {
|
||||
logger.error("Failed to parse HOST for SSL provisioning.");
|
||||
logger.error(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (host === "localhost" || host === "127.0.0.1") {
|
||||
logger.warn(
|
||||
"Skipping OneUptime SSL provisioning because HOST resolves to a loopback address.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isProvisioning) {
|
||||
logger.debug(
|
||||
"SSL provisioning already in progress for OneUptime host. Skipping concurrent run.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const globalConfig: GlobalConfig | null = await GlobalConfigService.findOneBy({
|
||||
query: {
|
||||
_id: ObjectID.getZeroObjectID().toString(),
|
||||
},
|
||||
select: {
|
||||
oneuptimeSslCertificate: true,
|
||||
oneuptimeSslCertificateKey: true,
|
||||
oneuptimeSslIssuedAt: true,
|
||||
oneuptimeSslExpiresAt: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!globalConfig) {
|
||||
logger.error("GlobalConfig document not found. Unable to provision SSL certificate.");
|
||||
return;
|
||||
}
|
||||
|
||||
const hasCertificate: boolean = Boolean(
|
||||
globalConfig.oneuptimeSslCertificate &&
|
||||
globalConfig.oneuptimeSslCertificateKey,
|
||||
);
|
||||
|
||||
const expiresAt: Date | undefined = globalConfig.oneuptimeSslExpiresAt
|
||||
? new Date(globalConfig.oneuptimeSslExpiresAt)
|
||||
: undefined;
|
||||
|
||||
const shouldRenew: boolean = !expiresAt
|
||||
? true
|
||||
: OneuptimeSslCertificateService.isExpiringSoon(expiresAt, 30);
|
||||
|
||||
if (hasCertificate && !shouldRenew) {
|
||||
logger.debug(
|
||||
"Existing OneUptime SSL certificate is still valid. No renewal required.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.isProvisioning = true;
|
||||
|
||||
try {
|
||||
logger.debug(`Ordering SSL certificate for OneUptime host: ${host}`);
|
||||
|
||||
await GreenlockUtil.orderCert({
|
||||
domain: host,
|
||||
validateCname: async () => {
|
||||
return true;
|
||||
},
|
||||
persistAcmeCertificate: false,
|
||||
onCertificateIssued: async (info) => {
|
||||
await GlobalConfigService.updateOneById({
|
||||
id: ObjectID.getZeroObjectID(),
|
||||
data: {
|
||||
oneuptimeSslCertificate: info.certificate,
|
||||
oneuptimeSslCertificateKey: info.certificateKey,
|
||||
oneuptimeSslIssuedAt: info.issuedAt,
|
||||
oneuptimeSslExpiresAt: info.expiresAt,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
logger.debug(
|
||||
`SSL certificate provisioning completed for OneUptime host: ${host}`,
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
"Failed to provision OneUptime SSL certificate via Let's Encrypt.",
|
||||
);
|
||||
logger.error(err);
|
||||
} finally {
|
||||
this.isProvisioning = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static isExpiringSoon(expiresAt: Date, renewBeforeDays: number): boolean {
|
||||
const renewThreshold: Date = OneUptimeDate.addRemoveDays(
|
||||
expiresAt,
|
||||
-1 * renewBeforeDays,
|
||||
);
|
||||
|
||||
return OneUptimeDate.isOnOrAfter(OneUptimeDate.getCurrentDate(), renewThreshold);
|
||||
}
|
||||
}
|
||||
@@ -135,6 +135,13 @@ export default class GreenlockUtil {
|
||||
public static async orderCert(data: {
|
||||
domain: string;
|
||||
validateCname: (domain: string) => Promise<boolean>;
|
||||
onCertificateIssued?: (info: {
|
||||
certificate: string;
|
||||
certificateKey: string;
|
||||
issuedAt: Date;
|
||||
expiresAt: Date;
|
||||
}) => Promise<void>;
|
||||
persistAcmeCertificate?: boolean;
|
||||
}): Promise<void> {
|
||||
try {
|
||||
logger.debug(
|
||||
@@ -262,61 +269,81 @@ export default class GreenlockUtil {
|
||||
logger.debug(`Certificate expires at: ${expiresAt}`);
|
||||
logger.debug(`Certificate issued at: ${issuedAt}`);
|
||||
|
||||
// check if the certificate is already in the database.
|
||||
const existingCertificate: AcmeCertificate | null =
|
||||
await AcmeCertificateService.findOneBy({
|
||||
query: {
|
||||
domain: domain,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
const certificateInfo: {
|
||||
certificate: string;
|
||||
certificateKey: string;
|
||||
issuedAt: Date;
|
||||
expiresAt: Date;
|
||||
} = {
|
||||
certificate: certificate.toString(),
|
||||
certificateKey: certificateKey.toString(),
|
||||
issuedAt,
|
||||
expiresAt,
|
||||
};
|
||||
|
||||
if (existingCertificate) {
|
||||
logger.debug(`Updating certificate for domain: ${domain}`);
|
||||
if (data.onCertificateIssued) {
|
||||
await data.onCertificateIssued(certificateInfo);
|
||||
}
|
||||
|
||||
// update the certificate
|
||||
await AcmeCertificateService.updateBy({
|
||||
query: {
|
||||
domain: domain,
|
||||
},
|
||||
limit: 1,
|
||||
skip: 0,
|
||||
data: {
|
||||
certificate: certificate.toString(),
|
||||
certificateKey: certificateKey.toString(),
|
||||
issuedAt: issuedAt,
|
||||
expiresAt: expiresAt,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
const shouldPersist: boolean = data.persistAcmeCertificate !== false;
|
||||
|
||||
logger.debug(`Certificate updated for domain: ${domain}`);
|
||||
} else {
|
||||
logger.debug(`Creating certificate for domain: ${domain}`);
|
||||
// create the certificate
|
||||
const acmeCertificate: AcmeCertificate = new AcmeCertificate();
|
||||
if (shouldPersist) {
|
||||
// check if the certificate is already in the database.
|
||||
const existingCertificate: AcmeCertificate | null =
|
||||
await AcmeCertificateService.findOneBy({
|
||||
query: {
|
||||
domain: domain,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
acmeCertificate.domain = domain;
|
||||
acmeCertificate.certificate = certificate.toString();
|
||||
acmeCertificate.certificateKey = certificateKey.toString();
|
||||
acmeCertificate.issuedAt = issuedAt;
|
||||
acmeCertificate.expiresAt = expiresAt;
|
||||
if (existingCertificate) {
|
||||
logger.debug(`Updating certificate for domain: ${domain}`);
|
||||
|
||||
await AcmeCertificateService.create({
|
||||
data: acmeCertificate,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
// update the certificate
|
||||
await AcmeCertificateService.updateBy({
|
||||
query: {
|
||||
domain: domain,
|
||||
},
|
||||
limit: 1,
|
||||
skip: 0,
|
||||
data: {
|
||||
certificate: certificateInfo.certificate,
|
||||
certificateKey: certificateInfo.certificateKey,
|
||||
issuedAt: issuedAt,
|
||||
expiresAt: expiresAt,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
logger.debug(`Certificate created for domain: ${domain}`);
|
||||
logger.debug(`Certificate updated for domain: ${domain}`);
|
||||
} else {
|
||||
logger.debug(`Creating certificate for domain: ${domain}`);
|
||||
// create the certificate
|
||||
const acmeCertificate: AcmeCertificate = new AcmeCertificate();
|
||||
|
||||
acmeCertificate.domain = domain;
|
||||
acmeCertificate.certificate = certificateInfo.certificate;
|
||||
acmeCertificate.certificateKey = certificateInfo.certificateKey;
|
||||
acmeCertificate.issuedAt = issuedAt;
|
||||
acmeCertificate.expiresAt = expiresAt;
|
||||
|
||||
await AcmeCertificateService.create({
|
||||
data: acmeCertificate,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
logger.debug(`Certificate created for domain: ${domain}`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(`Error ordering certificate for domain: ${data.domain}`);
|
||||
|
||||
@@ -79,6 +79,7 @@ The following table lists the configurable parameters of the OneUptime chart and
|
||||
| `global.storageClass` | Storage class to be used for all persistent volumes | `nil` | 🚨 |
|
||||
| `host` | Hostname for the ingress | `localhost` | 🚨 |
|
||||
| `httpProtocol` | If the server is hosted with SSL/TLS cert then change this value to https | `http` | 🚨 |
|
||||
| `enableSslProvisioningForOneuptime` | When true the ingress will request and renew Let's Encrypt certificates for the primary OneUptime host (requires letsEncrypt.* settings) | `false` | ⚠️ |
|
||||
| `oneuptimeSecret` | Value used to define ONEUPTIME_SECRET | `nil` | |
|
||||
| `encryptionSecret` | Value used to define ENCRYPTION_SECRET | `nil` | |
|
||||
| `global.clusterDomain` | Kubernetes Cluster Domain | `cluster.local` | |
|
||||
|
||||
@@ -196,6 +196,8 @@ Usage:
|
||||
|
||||
- name: LETS_ENCRYPT_ACCOUNT_KEY
|
||||
value: {{ $.Values.letsEncrypt.accountKey }}
|
||||
- name: ENABLE_SSL_PROVIONING_FOR_ONEUPTIME
|
||||
value: {{ $.Values.enableSslProvisioningForOneuptime | quote }}
|
||||
|
||||
- name: ENCRYPTION_SECRET
|
||||
{{- if $.Values.encryptionSecret }}
|
||||
|
||||
@@ -22,6 +22,11 @@
|
||||
"type": "string",
|
||||
"enum": ["http", "https"]
|
||||
},
|
||||
"enableSslProvisioningForOneuptime": {
|
||||
"type": "boolean",
|
||||
"description": "Enable automatic Let's Encrypt provisioning for the primary OneUptime host",
|
||||
"default": false
|
||||
},
|
||||
"oneuptimeSecret": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
|
||||
@@ -5,6 +5,7 @@ global:
|
||||
# Please change this to the domain name / IP where OneUptime server is hosted on.
|
||||
host: localhost
|
||||
httpProtocol: http
|
||||
enableSslProvisioningForOneuptime: 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.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import AcmeWriteCertificatesJob from "./Jobs/AcmeWriteCertificates";
|
||||
import WriteCustomCertsToDiskJob from "./Jobs/WriteCustomCertsToDisk";
|
||||
import WriteOneuptimeCertToDiskJob from "./Jobs/WriteOneuptimeCertToDisk";
|
||||
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<void> => {
|
||||
|
||||
AcmeWriteCertificatesJob.init();
|
||||
WriteCustomCertsToDiskJob.init();
|
||||
WriteOneuptimeCertToDiskJob.init();
|
||||
|
||||
// add default routes
|
||||
await App.addDefaultRoutes();
|
||||
|
||||
128
Nginx/Jobs/WriteOneuptimeCertToDisk.ts
Normal file
128
Nginx/Jobs/WriteOneuptimeCertToDisk.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { EVERY_FIFTEEN_MINUTE, EVERY_MINUTE } from "Common/Utils/CronTime";
|
||||
import { EnableSslProvisioningForOneuptime, Host, IsDevelopment } from "Common/Server/EnvironmentConfig";
|
||||
import BasicCron from "Common/Server/Utils/BasicCron";
|
||||
import LocalFile from "Common/Server/Utils/LocalFile";
|
||||
import logger from "Common/Server/Utils/Logger";
|
||||
import GlobalConfigService from "Common/Server/Services/GlobalConfigService";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import GlobalConfig from "Common/Models/DatabaseModels/GlobalConfig";
|
||||
|
||||
const CERT_DIRECTORY: string = "/etc/nginx/certs/OneUptime";
|
||||
|
||||
export default class WriteOneuptimeCertToDiskJob {
|
||||
public static init(): void {
|
||||
BasicCron({
|
||||
jobName: "OneuptimeCerts:WriteToDisk",
|
||||
options: {
|
||||
schedule: IsDevelopment ? EVERY_MINUTE : EVERY_FIFTEEN_MINUTE,
|
||||
runOnStartup: true,
|
||||
},
|
||||
runFunction: async () => {
|
||||
await this.syncCertificateToDisk();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public static async syncCertificateToDisk(): Promise<void> {
|
||||
const originalHost: string = Host.trim();
|
||||
const host: string = originalHost.toLowerCase();
|
||||
|
||||
if (!EnableSslProvisioningForOneuptime || !host) {
|
||||
await this.removeCertificateFromDisk(originalHost, host);
|
||||
return;
|
||||
}
|
||||
|
||||
const globalConfig: GlobalConfig | null = await GlobalConfigService.findOneBy({
|
||||
query: {
|
||||
_id: ObjectID.getZeroObjectID().toString(),
|
||||
},
|
||||
select: {
|
||||
oneuptimeSslCertificate: true,
|
||||
oneuptimeSslCertificateKey: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!globalConfig) {
|
||||
logger.error(
|
||||
"GlobalConfig record not found while attempting to write OneUptime SSL certificate to disk.",
|
||||
);
|
||||
await this.removeCertificateFromDisk(originalHost, host);
|
||||
return;
|
||||
}
|
||||
|
||||
const certificate: string | undefined =
|
||||
globalConfig.oneuptimeSslCertificate || undefined;
|
||||
const certificateKey: string | undefined =
|
||||
globalConfig.oneuptimeSslCertificateKey || undefined;
|
||||
|
||||
if (!certificate || !certificateKey) {
|
||||
await this.removeCertificateFromDisk(originalHost, host);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await LocalFile.makeDirectory(CERT_DIRECTORY);
|
||||
} catch (err) {
|
||||
logger.error("Failed to ensure directory for OneUptime certificates exists.");
|
||||
logger.error(err);
|
||||
return;
|
||||
}
|
||||
|
||||
const fileVariants: Array<string> = [host];
|
||||
|
||||
if (originalHost && originalHost !== host) {
|
||||
fileVariants.push(originalHost);
|
||||
}
|
||||
|
||||
for (const variant of fileVariants) {
|
||||
const certPath: string = `${CERT_DIRECTORY}/${variant}.crt`;
|
||||
const keyPath: string = `${CERT_DIRECTORY}/${variant}.key`;
|
||||
|
||||
await LocalFile.write(certPath, certificate);
|
||||
await LocalFile.write(keyPath, certificateKey);
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Wrote primary OneUptime SSL certificate to disk for host variants: ${fileVariants.join(",")}`,
|
||||
);
|
||||
}
|
||||
|
||||
private static async removeCertificateFromDisk(
|
||||
originalHost: string,
|
||||
host: string,
|
||||
): Promise<void> {
|
||||
if (!host && !originalHost) {
|
||||
return;
|
||||
}
|
||||
|
||||
const variants: Set<string> = new Set<string>();
|
||||
|
||||
if (host) {
|
||||
variants.add(host);
|
||||
}
|
||||
|
||||
if (originalHost) {
|
||||
variants.add(originalHost);
|
||||
}
|
||||
|
||||
for (const variant of variants) {
|
||||
const certPath: string = `${CERT_DIRECTORY}/${variant}.crt`;
|
||||
const keyPath: string = `${CERT_DIRECTORY}/${variant}.key`;
|
||||
|
||||
await LocalFile.deleteFile(certPath).catch(() => {
|
||||
// ignore delete errors.
|
||||
});
|
||||
|
||||
await LocalFile.deleteFile(keyPath).catch(() => {
|
||||
// ignore delete errors.
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Removed OneUptime SSL certificate artifacts from disk for host variants: ${Array.from(variants).join(",")}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -417,8 +417,10 @@ server {
|
||||
|
||||
listen ${NGINX_LISTEN_ADDRESS}7849 ${NGINX_LISTEN_OPTIONS};
|
||||
http2 on;
|
||||
${ONEUPTIME_SSL_LISTEN_DIRECTIVES}
|
||||
|
||||
server_name localhost ingress ${HOST}; #All domains
|
||||
${ONEUPTIME_SSL_CERT_DIRECTIVES}
|
||||
|
||||
proxy_busy_buffers_size 512k;
|
||||
proxy_buffers 4 512k;
|
||||
@@ -441,6 +443,7 @@ server {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
${ONEUPTIME_SSL_REDIRECT_SNIPPET}
|
||||
|
||||
# If billing_enabled is true then proxy to home otherwise to dashboard because we dont need marketing paages for on-prem install.
|
||||
if ($billing_enabled = true) {
|
||||
@@ -452,6 +455,21 @@ server {
|
||||
}
|
||||
}
|
||||
|
||||
# ACME challenge endpoint for OneUptime host cert provisioning.
|
||||
location /.well-known {
|
||||
resolver 127.0.0.1 valid=30s;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
proxy_pass http://app/api/status-page/.well-known;
|
||||
}
|
||||
|
||||
# PWA manifest and service worker with proper headers for home
|
||||
location ~* ^/(manifest\.json|service-worker\.js)$ {
|
||||
resolver 127.0.0.1 valid=30s;
|
||||
|
||||
30
Nginx/run.sh
30
Nginx/run.sh
@@ -1,5 +1,35 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ "${ENABLE_SSL_PROVIONING_FOR_ONEUPTIME}" = "true" ] && [ -n "${HOST}" ]; then
|
||||
ONEUPTIME_SSL_LISTEN_DIRECTIVES=$(cat <<'EOF'
|
||||
listen ${NGINX_LISTEN_ADDRESS}7850 ssl http2 ${NGINX_LISTEN_OPTIONS};
|
||||
EOF
|
||||
)
|
||||
|
||||
ONEUPTIME_SSL_CERT_DIRECTIVES=$(cat <<'EOF'
|
||||
ssl_certificate /etc/nginx/certs/OneUptime/$ssl_server_name.crt;
|
||||
ssl_certificate_key /etc/nginx/certs/OneUptime/$ssl_server_name.key;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_prefer_server_ciphers off;
|
||||
EOF
|
||||
)
|
||||
|
||||
ONEUPTIME_SSL_REDIRECT_SNIPPET=$(cat <<'EOF'
|
||||
if ($scheme = "http") {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
EOF
|
||||
)
|
||||
else
|
||||
export ONEUPTIME_SSL_LISTEN_DIRECTIVES=""
|
||||
export ONEUPTIME_SSL_CERT_DIRECTIVES=""
|
||||
export ONEUPTIME_SSL_REDIRECT_SNIPPET=""
|
||||
fi
|
||||
|
||||
export ONEUPTIME_SSL_LISTEN_DIRECTIVES
|
||||
export ONEUPTIME_SSL_CERT_DIRECTIVES
|
||||
export ONEUPTIME_SSL_REDIRECT_SNIPPET
|
||||
|
||||
# Run envsubst on template
|
||||
/etc/nginx/envsubst-on-templates.sh
|
||||
|
||||
|
||||
28
Worker/Jobs/OneuptimeCerts/OneuptimeCerts.ts
Normal file
28
Worker/Jobs/OneuptimeCerts/OneuptimeCerts.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import RunCron from "../../Utils/Cron";
|
||||
import { EVERY_DAY, EVERY_FIFTEEN_MINUTE } from "Common/Utils/CronTime";
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
import {
|
||||
EnableSslProvisioningForOneuptime,
|
||||
IsDevelopment,
|
||||
} from "Common/Server/EnvironmentConfig";
|
||||
import OneuptimeSslCertificateService from "Common/Server/Services/OneuptimeSslCertificateService";
|
||||
import logger from "Common/Server/Utils/Logger";
|
||||
|
||||
RunCron(
|
||||
"OneuptimeCerts:EnsureProvisioned",
|
||||
{
|
||||
schedule: IsDevelopment ? EVERY_FIFTEEN_MINUTE : EVERY_DAY,
|
||||
runOnStartup: true,
|
||||
timeoutInMS: OneUptimeDate.convertMinutesToMilliseconds(15),
|
||||
},
|
||||
async () => {
|
||||
if (!EnableSslProvisioningForOneuptime) {
|
||||
logger.debug(
|
||||
"ENABLE_SSL_PROVIONING_FOR_ONEUPTIME is disabled. Skipping OneUptime certificate provisioning run.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await OneuptimeSslCertificateService.ensureCertificateProvisioned();
|
||||
},
|
||||
);
|
||||
@@ -62,6 +62,7 @@ import "./Jobs/ServerMonitor/CheckOnlineStatus";
|
||||
|
||||
// // Certs Routers
|
||||
import "./Jobs/StatusPageCerts/StatusPageCerts";
|
||||
import "./Jobs/OneuptimeCerts/OneuptimeCerts";
|
||||
|
||||
// Status Page Announcements
|
||||
import "./Jobs/StatusPageOwners/SendAnnouncementCreatedNotification";
|
||||
|
||||
@@ -9,13 +9,12 @@ ONEUPTIME_HTTP_PORT=80
|
||||
# ==============================================
|
||||
# SETTING UP TLS/SSL CERTIFICATES
|
||||
# ==============================================
|
||||
# OneUptime DOES NOT support setting up SSL/TLS certificates. You need to setup SSL/TLS certificates on your own.
|
||||
# If you need to use SSL/TLS certificates, then you need to use a reverse proxy like Nginx/Caddy and use LetsEncrypt to provision the certificates.
|
||||
# You then need to point the reverse proxy to the OneUptime server.
|
||||
# Once you have done that,
|
||||
# - You can set the HTTP_PROTOCOL to https
|
||||
# - Change the HOST to the domain name of the server where reverse proxy is hosted.
|
||||
# OneUptime can now provision TLS certificates for the primary host via Let's Encrypt.
|
||||
# - Set ENABLE_SSL_PROVIONING_FOR_ONEUPTIME=true to automatically request and renew certificates inside the ingress container.
|
||||
# - You must still supply LETS_ENCRYPT_NOTIFICATION_EMAIL and LETS_ENCRYPT_ACCOUNT_KEY for provisioning to succeed.
|
||||
# If you prefer managing TLS yourself in front of OneUptime, leave this option disabled and continue using an external reverse proxy.
|
||||
HTTP_PROTOCOL=http
|
||||
ENABLE_SSL_PROVIONING_FOR_ONEUPTIME=false
|
||||
|
||||
# Secrets - PLEASE CHANGE THESE. Please change these to something random. All of these can be different values.
|
||||
ONEUPTIME_SECRET=please-change-this-to-random-value
|
||||
|
||||
@@ -83,6 +83,8 @@ x-common-server-variables: &common-server-variables
|
||||
<<: *common-variables
|
||||
ONEUPTIME_SECRET: ${ONEUPTIME_SECRET}
|
||||
|
||||
ENABLE_SSL_PROVIONING_FOR_ONEUPTIME: ${ENABLE_SSL_PROVIONING_FOR_ONEUPTIME}
|
||||
|
||||
VAPID_PRIVATE_KEY: ${VAPID_PRIVATE_KEY}
|
||||
DATABASE_PORT: ${DATABASE_PORT}
|
||||
DATABASE_USERNAME: ${DATABASE_USERNAME}
|
||||
|
||||
Reference in New Issue
Block a user