M config.scfg => config.scfg +3 -0
@@ 51,6 51,9 @@ ntfy {
# ntfy authentication via access tokens (https://docs.ntfy.sh/publish/#access-tokens)
# Either access-token or a user/password combination can be used - not both.
access-token foobar
+ # When using (self signed) certificates that cannot be verified, you can instead specify
+ # the SHA512 fingerprint.
+ certificate-fingerprint 136d2b889c5736d081b4b29c7909276292cfb86a6bd3ad4635cb7017eb996e28082ab8c6794bf62e817941981d53c807b35c245fb18eb6fb66b5ddb4d05c299
# Forward all messages to the specified email address.
email-address foo@bar.com
# Call the specified number for all alerts. Use `yes` to pick the first of your verified numbers.
M config/config.go => config/config.go +14 -6
@@ 36,12 36,13 @@ type Config struct {
}
type ntfyConfig struct {
- Topic string
- User string
- Password string
- AccessToken string
- EmailAddress string
- Call string
+ Topic string
+ User string
+ Password string
+ AccessToken string
+ CertFingerprint string
+ EmailAddress string
+ Call string
}
type labels struct {
@@ 277,6 278,13 @@ func ReadConfig(path string) (*Config, error) {
return nil, errors.New("ntfy: cannot use both an access-token and a user/password at the same time")
}
+ d = ntfyDir.Children.Get("certificate-fingerprint")
+ if d != nil {
+ if err := d.ParseParams(&config.Ntfy.CertFingerprint); err != nil {
+ return nil, err
+ }
+ }
+
d = ntfyDir.Children.Get("email-address")
if d != nil {
if err := d.ParseParams(&config.Ntfy.EmailAddress); err != nil {
M config/config_test.go => config/config_test.go +7 -1
@@ 45,6 45,7 @@ resolved {
ntfy {
topic https://ntfy.sh/alertmanager-alerts
+ certificate-fingerprint 136d2b889c5736d081b4b29c7909276292cfb86a6bd3ad4635cb7017eb996e28082ab8c6794bf62e817941981d53c807b35c245fb18eb6fb66b5ddb4d05c299
user user
password pass
}
@@ 71,7 72,12 @@ cache {
AlertMode: Multi,
User: "webhookUser",
Password: "webhookPass",
- Ntfy: ntfyConfig{Topic: "https://ntfy.sh/alertmanager-alerts", User: "user", Password: "pass"},
+ Ntfy: ntfyConfig{
+ Topic: "https://ntfy.sh/alertmanager-alerts",
+ User: "user",
+ Password: "pass",
+ CertFingerprint: "136d2b889c5736d081b4b29c7909276292cfb86a6bd3ad4635cb7017eb996e28082ab8c6794bf62e817941981d53c807b35c245fb18eb6fb66b5ddb4d05c299",
+ },
Labels: labels{Order: []string{"severity", "instance"},
Label: map[string]labelConfig{
"severity:critical": {
M main.go => main.go +27 -0
@@ 5,9 5,13 @@ import (
"context"
"crypto/sha512"
"crypto/subtle"
+ "crypto/tls"
+ "crypto/x509"
_ "embed"
"encoding/base64"
+ "encoding/hex"
"encoding/json"
+ "errors"
"flag"
"fmt"
"log/slog"
@@ 329,6 333,29 @@ func (br *bridge) publish(n *notification) error {
req.Header.Set("Actions", fmt.Sprintf("http, Silence, %s, method=POST, body=%s%s", url, n.silenceBody, authString))
}
+ configFingerprint := br.cfg.Ntfy.CertFingerprint
+ if configFingerprint != "" {
+ tlsCfg := &tls.Config{}
+ tlsCfg.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
+ for _, rawCert := range rawCerts {
+ hash := sha512.Sum512(rawCert)
+ if hex.EncodeToString(hash[:]) == configFingerprint {
+ return nil
+ }
+ }
+
+ if len(rawCerts) == 0 {
+ return errors.New("the ntfy server does not offer a certificate")
+ }
+
+ hash := sha512.Sum512(rawCerts[0])
+ return fmt.Errorf("ntfy certificate fingerprint does not match: expected %q, got %q", hex.EncodeToString(hash[:]), configFingerprint)
+ }
+
+ tlsCfg.InsecureSkipVerify = true
+ br.client.Transport = &http.Transport{TLSClientConfig: tlsCfg}
+ }
+
resp, err := br.client.Do(req)
if err != nil {
return err