knadh/listmonk · critical

webhook key is not configured

Error message

webhook key is not configured

What it means

Forwardemail.ProcessBounce verifies webhook payloads with HMAC-SHA256 using an hmacKey supplied at construction (NewForwardemail). This error is returned when that key is empty (len == 0), meaning no verification is possible. The library refuses to process rather than skipping signature verification, which would be a security hole.

Source

Thrown at internal/bounce/webhooks/forwardemail.go:51

	ResponseCode    int               `json:"response_code"`
	TruthSource     string            `json:"truth_source"`
	Headers         map[string]string `json:"headers"`
	Bounce          BounceDetails     `json:"bounce"`
	BouncedAt       time.Time         `json:"bounced_at"`
}

// Forwardemail handles webhook notifications (mainly bounce notifications).
type Forwardemail struct {
	hmacKey []byte
}

func NewForwardemail(key []byte) *Forwardemail {
	return &Forwardemail{hmacKey: key}
}

func (p *Forwardemail) ProcessBounce(sigHex string, body []byte) ([]models.Bounce, error) {
	if len(p.hmacKey) == 0 {
		return nil, errors.New("webhook key is not configured")
	}

	// Decode the hex-encoded signature from the webhook
	sig, err := hex.DecodeString(sigHex)
	if err != nil {
		return nil, fmt.Errorf("invalid signature encoding: %v", err)
	}

	// Generate HMAC using the request body and secret key
	mac := hmac.New(sha256.New, p.hmacKey)
	mac.Write(body)
	expectedSignature := mac.Sum(nil)

	// Compare the generated signature with the provided signature
	if !hmac.Equal(expectedSignature, sig) {
		return nil, errors.New("invalid signature")
	}

View on GitHub (pinned to 670c01717d)

Solutions

  1. Set the Forwardemail webhook key in your configuration/environment and restart so NewForwardemail receives a non-empty key.
  2. Copy the webhook signing secret from your Forwardemail account/dashboard into the deployment secrets store.
  3. Check the code path that reads the key and fails loudly at startup (validate len(key) > 0 when constructing NewForwardemail) instead of at first webhook.
  4. Verify secrets mounts/env propagation (Docker secret file, k8s secret) actually populated the variable.

Example fix

// before
key := os.Getenv("FORWARD_EMAIL_KEY") // "" if unset
p := webhooks.NewForwardemail([]byte(key))
// after
key := os.Getenv("FORWARD_EMAIL_KEY")
if key == "" {
	log.Fatal("FORWARD_EMAIL_KEY must be set")
}
p := webhooks.NewForwardemail([]byte(key))
Defensive patterns

Strategy: validation

Validate before calling

key := os.Getenv("FORWARD_EMAIL_WEBHOOK_KEY")
if key == "" {
	return errors.New("forwardemail webhook key must be configured before processing bounces")
}

Type guard

func forwardemailConfigured(p *webhooks.Forwardemail) bool {
	return p != nil // combine with non-empty key check at construction time
}

Try / catch

bounces, err := p.ProcessBounce(sigHex, body)
if err != nil {
	if strings.Contains(err.Error(), "webhook key is not configured") {
		// deployment misconfiguration: alert ops, return 503
		return echo.NewHTTPError(http.StatusServiceUnavailable, "webhook not configured")
	}
	return err
}

Prevention

When it happens

Trigger: Forwardemail was constructed as NewForwardemail(nil) or NewForwardemail([]byte{}) — typically because the webhook key config value/env var was empty — and then ProcessBounce is called with an incoming signature and body.

Common situations: Missing FORWARD_EMAIL_WEBHOOK_KEY-style env var in the deployment; config key omitted or empty string in the config file; a fresh environment (staging, local dev) where secrets were never provisioned; Docker/secret-mount failed so the value reads as empty.

Understand the failure class

Background: "environment variable is not set" and "Missing keys in environment" errors: what missing required env var messages mean and how to fix them — this error's family across 28 libraries.

Related errors


AI-assisted analysis of knadh/listmonk@670c01717d (2026-09-01). Data as JSON: /api/errors/4ebe9602e1253c94. Report an issue: GitHub.