~xenrox/ntfy-alertmanager

25e65db8bdd2de06aea5897a599ef746fadfa4f8 — Thorben Günther 8 months ago 1bfb814
Add support for self signed ntfy certificates

A fingerprint can be specified in the configuration file. If it matches
the one from the server certificate, it will be accepted.

Closes: https://todo.xenrox.net/~xenrox/ntfy-alertmanager/22
4 files changed, 51 insertions(+), 7 deletions(-)

M config.scfg
M config/config.go
M config/config_test.go
M main.go
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