Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Email Encryption with PGP #4657

Open
hozza opened this issue Apr 7, 2024 · 4 comments
Open

Add Email Encryption with PGP #4657

hozza opened this issue Apr 7, 2024 · 4 comments
Labels
area:notifications Everything related to notifications feature-request Request for new features to be added type:enhance-existing feature wants to enhance existing monitor

Comments

@hozza
Copy link

hozza commented Apr 7, 2024

📑 I have found these related issues/pull requests

class SMTP extends NotificationProvider {
name = "smtp";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
const config = {
host: notification.smtpHost,
port: notification.smtpPort,
secure: notification.smtpSecure,
tls: {
rejectUnauthorized: !notification.smtpIgnoreTLSError || false,
}
};
// Fix #1129
if (notification.smtpDkimDomain) {
config.dkim = {
domainName: notification.smtpDkimDomain,
keySelector: notification.smtpDkimKeySelector,
privateKey: notification.smtpDkimPrivateKey,
hashAlgo: notification.smtpDkimHashAlgo,
headerFieldNames: notification.smtpDkimheaderFieldNames,
skipFields: notification.smtpDkimskipFields,
};
}
// Should fix the issue in https://github.com/louislam/uptime-kuma/issues/26#issuecomment-896373904
if (notification.smtpUsername || notification.smtpPassword) {
config.auth = {
user: notification.smtpUsername,
pass: notification.smtpPassword,
};
}
// default values in case the user does not want to template
let subject = msg;
let body = msg;
if (heartbeatJSON) {
body = `${msg}\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`;
}
// subject and body are templated
if ((monitorJSON && heartbeatJSON) || msg.endsWith("Testing")) {
// cannot end with whitespace as this often raises spam scores
const customSubject = notification.customSubject?.trim() || "";
const customBody = notification.customBody?.trim() || "";
const context = this.generateContext(msg, monitorJSON, heartbeatJSON);
const engine = new Liquid();
if (customSubject !== "") {
const tpl = engine.parse(customSubject);
subject = await engine.render(tpl, context);
}
if (customBody !== "") {
const tpl = engine.parse(customBody);
body = await engine.render(tpl, context);
}
}
// send mail with defined transport object
let transporter = nodemailer.createTransport(config);
await transporter.sendMail({
from: notification.smtpFrom,
cc: notification.smtpCC,
bcc: notification.smtpBCC,
to: notification.smtpTo,
subject: subject,
text: body,
});
return okMsg;
}
/**
* Generate context for LiquidJS
* @param {string} msg the message that will be included in the context
* @param {?object} monitorJSON Monitor details (For Up/Down/Cert-Expiry only)
* @param {?object} heartbeatJSON Heartbeat details (For Up/Down only)
* @returns {{STATUS: string, status: string, HOSTNAME_OR_URL: string, hostnameOrUrl: string, NAME: string, name: string, monitorJSON: ?object, heartbeatJSON: ?object, msg: string}} the context
*/
generateContext(msg, monitorJSON, heartbeatJSON) {
// Let's start with dummy values to simplify code
let monitorName = "Monitor Name not available";
let monitorHostnameOrURL = "testing.hostname";
if (monitorJSON !== null) {
monitorName = monitorJSON["name"];
if (monitorJSON["type"] === "http" || monitorJSON["type"] === "keyword" || monitorJSON["type"] === "json-query") {
monitorHostnameOrURL = monitorJSON["url"];
} else {
monitorHostnameOrURL = monitorJSON["hostname"];
}
}
let serviceStatus = "⚠️ Test";
if (heartbeatJSON !== null) {
serviceStatus = (heartbeatJSON["status"] === DOWN) ? "🔴 Down" : "✅ Up";
}
return {
// for v1 compatibility, to be removed in v3
"STATUS": serviceStatus,
"NAME": monitorName,
"HOSTNAME_OR_URL": monitorHostnameOrURL,
// variables which are officially supported
"status": serviceStatus,
"name": monitorName,
"hostnameOrURL": monitorHostnameOrURL,
monitorJSON,
heartbeatJSON,
msg,
};
}
}

🏷️ Feature Request Type

Change to existing notification-provider

🔖 Feature description

Email notifications are pretty great and reliable, but it would be absolutely fantastic to be able to encrypt the email messages using PGP.

This would work nicely with secure email providers.

✔️ Solution

Add PGP email encryption module to Nodemailer for more secure email notifications. 🔒

https://github.com/nodemailer/nodemailer-openpgp

❓ Alternatives

using an unencrypted email? 🔓

📝 Additional Context

Nope

@hozza hozza added the feature-request Request for new features to be added label Apr 7, 2024
@CommanderStorm
Copy link
Collaborator

CommanderStorm commented Apr 7, 2024

Said notification provider is located here:

class SMTP extends NotificationProvider {
name = "smtp";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
const config = {
host: notification.smtpHost,
port: notification.smtpPort,
secure: notification.smtpSecure,
tls: {
rejectUnauthorized: !notification.smtpIgnoreTLSError || false,
}
};
// Fix #1129
if (notification.smtpDkimDomain) {
config.dkim = {
domainName: notification.smtpDkimDomain,
keySelector: notification.smtpDkimKeySelector,
privateKey: notification.smtpDkimPrivateKey,
hashAlgo: notification.smtpDkimHashAlgo,
headerFieldNames: notification.smtpDkimheaderFieldNames,
skipFields: notification.smtpDkimskipFields,
};
}
// Should fix the issue in https://github.com/louislam/uptime-kuma/issues/26#issuecomment-896373904
if (notification.smtpUsername || notification.smtpPassword) {
config.auth = {
user: notification.smtpUsername,
pass: notification.smtpPassword,
};
}
// default values in case the user does not want to template
let subject = msg;
let body = msg;
if (heartbeatJSON) {
body = `${msg}\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`;
}
// subject and body are templated
if ((monitorJSON && heartbeatJSON) || msg.endsWith("Testing")) {
// cannot end with whitespace as this often raises spam scores
const customSubject = notification.customSubject?.trim() || "";
const customBody = notification.customBody?.trim() || "";
const context = this.generateContext(msg, monitorJSON, heartbeatJSON);
const engine = new Liquid();
if (customSubject !== "") {
const tpl = engine.parse(customSubject);
subject = await engine.render(tpl, context);
}
if (customBody !== "") {
const tpl = engine.parse(customBody);
body = await engine.render(tpl, context);
}
}
// send mail with defined transport object
let transporter = nodemailer.createTransport(config);
await transporter.sendMail({
from: notification.smtpFrom,
cc: notification.smtpCC,
bcc: notification.smtpBCC,
to: notification.smtpTo,
subject: subject,
text: body,
});
return okMsg;
}
/**
* Generate context for LiquidJS
* @param {string} msg the message that will be included in the context
* @param {?object} monitorJSON Monitor details (For Up/Down/Cert-Expiry only)
* @param {?object} heartbeatJSON Heartbeat details (For Up/Down only)
* @returns {{STATUS: string, status: string, HOSTNAME_OR_URL: string, hostnameOrUrl: string, NAME: string, name: string, monitorJSON: ?object, heartbeatJSON: ?object, msg: string}} the context
*/
generateContext(msg, monitorJSON, heartbeatJSON) {
// Let's start with dummy values to simplify code
let monitorName = "Monitor Name not available";
let monitorHostnameOrURL = "testing.hostname";
if (monitorJSON !== null) {
monitorName = monitorJSON["name"];
if (monitorJSON["type"] === "http" || monitorJSON["type"] === "keyword" || monitorJSON["type"] === "json-query") {
monitorHostnameOrURL = monitorJSON["url"];
} else {
monitorHostnameOrURL = monitorJSON["hostname"];
}
}
let serviceStatus = "⚠️ Test";
if (heartbeatJSON !== null) {
serviceStatus = (heartbeatJSON["status"] === DOWN) ? "🔴 Down" : "✅ Up";
}
return {
// for v1 compatibility, to be removed in v3
"STATUS": serviceStatus,
"NAME": monitorName,
"HOSTNAME_OR_URL": monitorHostnameOrURL,
// variables which are officially supported
"status": serviceStatus,
"name": monitorName,
"hostnameOrURL": monitorHostnameOrURL,
monitorJSON,
heartbeatJSON,
msg,
};
}
}

Agree that Transmitting notifications via plain text is likely not ideal.
The thing is that is that PGP does not encrypt the headers => also not the subject
Yes, the body could be encrypted, but the value here is likely lower

If you want to "secure" this part, I would suggest using a notification provider which is designed for this use case.

I am a bit unsure if the added maintenance effort adds value.
=> what would you like to achieve/protect from in the first place?

What do you mean by

This would world nicely with secure email providers

@CommanderStorm CommanderStorm added question Further information is requested area:notifications Everything related to notifications type:enhance-existing feature wants to enhance existing monitor labels Apr 7, 2024
@hozza
Copy link
Author

hozza commented Apr 7, 2024

Said notification provider is located here:

class SMTP extends NotificationProvider {
name = "smtp";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
const config = {
host: notification.smtpHost,
port: notification.smtpPort,
secure: notification.smtpSecure,
tls: {
rejectUnauthorized: !notification.smtpIgnoreTLSError || false,
}
};
// Fix #1129
if (notification.smtpDkimDomain) {
config.dkim = {
domainName: notification.smtpDkimDomain,
keySelector: notification.smtpDkimKeySelector,
privateKey: notification.smtpDkimPrivateKey,
hashAlgo: notification.smtpDkimHashAlgo,
headerFieldNames: notification.smtpDkimheaderFieldNames,
skipFields: notification.smtpDkimskipFields,
};
}
// Should fix the issue in https://github.com/louislam/uptime-kuma/issues/26#issuecomment-896373904
if (notification.smtpUsername || notification.smtpPassword) {
config.auth = {
user: notification.smtpUsername,
pass: notification.smtpPassword,
};
}
// default values in case the user does not want to template
let subject = msg;
let body = msg;
if (heartbeatJSON) {
body = `${msg}\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`;
}
// subject and body are templated
if ((monitorJSON && heartbeatJSON) || msg.endsWith("Testing")) {
// cannot end with whitespace as this often raises spam scores
const customSubject = notification.customSubject?.trim() || "";
const customBody = notification.customBody?.trim() || "";
const context = this.generateContext(msg, monitorJSON, heartbeatJSON);
const engine = new Liquid();
if (customSubject !== "") {
const tpl = engine.parse(customSubject);
subject = await engine.render(tpl, context);
}
if (customBody !== "") {
const tpl = engine.parse(customBody);
body = await engine.render(tpl, context);
}
}
// send mail with defined transport object
let transporter = nodemailer.createTransport(config);
await transporter.sendMail({
from: notification.smtpFrom,
cc: notification.smtpCC,
bcc: notification.smtpBCC,
to: notification.smtpTo,
subject: subject,
text: body,
});
return okMsg;
}
/**
* Generate context for LiquidJS
* @param {string} msg the message that will be included in the context
* @param {?object} monitorJSON Monitor details (For Up/Down/Cert-Expiry only)
* @param {?object} heartbeatJSON Heartbeat details (For Up/Down only)
* @returns {{STATUS: string, status: string, HOSTNAME_OR_URL: string, hostnameOrUrl: string, NAME: string, name: string, monitorJSON: ?object, heartbeatJSON: ?object, msg: string}} the context
*/
generateContext(msg, monitorJSON, heartbeatJSON) {
// Let's start with dummy values to simplify code
let monitorName = "Monitor Name not available";
let monitorHostnameOrURL = "testing.hostname";
if (monitorJSON !== null) {
monitorName = monitorJSON["name"];
if (monitorJSON["type"] === "http" || monitorJSON["type"] === "keyword" || monitorJSON["type"] === "json-query") {
monitorHostnameOrURL = monitorJSON["url"];
} else {
monitorHostnameOrURL = monitorJSON["hostname"];
}
}
let serviceStatus = "⚠️ Test";
if (heartbeatJSON !== null) {
serviceStatus = (heartbeatJSON["status"] === DOWN) ? "🔴 Down" : "✅ Up";
}
return {
// for v1 compatibility, to be removed in v3
"STATUS": serviceStatus,
"NAME": monitorName,
"HOSTNAME_OR_URL": monitorHostnameOrURL,
// variables which are officially supported
"status": serviceStatus,
"name": monitorName,
"hostnameOrURL": monitorHostnameOrURL,
monitorJSON,
heartbeatJSON,
msg,
};
}
}

Thanks, added to the ticket.

The thing is that is that PGP does not encrypt the headers => also not the subject
Yes, the body could be encrypted, but the value here is likely lower

This is standard for PGP implementations, like in Proton Mail etc.

Getting into the details I'd suppose the security benefit here comes from protecting the uptime status of your services/infrastructure from an attacker. So perhaps uptime status could be removed from the email subject when enabling PGP (and from headers if it's in there?) leaving them generic, only communicating what's down and when in the encrypted body message.

If you want to "secure" this part, I would suggest using a notification provider which is designed for this use case.

Thanks, and I have been for sometime but nothing beats email for its open decentralised nature. Email is often omnipresent on devices whereas specific apps are not so much. Also not being tied into an app or special service provider is nice, favouring an open and standardised format.

I am a bit unsure if the added maintenance effort adds value.
=> what would you like to achieve/protect from in the first place?

I wonder how much extra dev maintenance this would be, perhaps it could be as little as an extra module, and a few input boxes for those that enable it? Support wise, I'd imagine this would be an advanced feature that support was not provided on.

I've touched in this a little above but knowing the uptime status of critical infra could be used in an attack or to validate an attack etc. Uptime of servers/networking equipment etc could be accessed by plain text emails or via unencrypted notification service providers.

This could be rare, targeted and effort filled attack in general but it could also be as easy as using a work based SMTP server, and a disgruntled email IT colleague with access could learn of certain services being down from the plain text and using this info nefariously.

What do you mean by

This would world nicely with secure email providers

Thanks, fixed type.

@CommanderStorm
Copy link
Collaborator

You would be surprised by ho much of the support effort are these harder to configure features.
Have a look at registering your own custom CA with uptime kuma for further details.
The problem is that I don't know anything about PGP
=> Could not support users having trouble.

The support-trouble especially start when a lackluster maintained module gets added (read: I am unsure if the module is working correctly as they have not enabled issues)

We can add such a feature if

  • you or somebody else would be willing to help people having trouble with this feature (Idk how muc trouble this would be as I don't use PGP)
  • you or somebody else provides an implementation and a testcase that this works as expected

I still don't get what you mean by work nicely and secure email providers in

This would work nicely with secure email providers.

@hozza
Copy link
Author

hozza commented Apr 7, 2024

Fair enough. I don't have time to develop this unfortunately. If someone wants to pick it up I'd be happy to help where I can.

@CommanderStorm CommanderStorm removed the question Further information is requested label Apr 8, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area:notifications Everything related to notifications feature-request Request for new features to be added type:enhance-existing feature wants to enhance existing monitor
Projects
None yet
Development

No branches or pull requests

2 participants