phacility/phabricator · critical · Exception

Mail signature is not valid. Check your Mailgun API key.

Error message

Mail signature is not valid. Check your Mailgun API key.

What it means

Mailgun's inbound webhook posts timestamp, token and signature; the controller recomputes hash_hmac('sha256', timestamp.token, api_key) for every configured inbound Mailgun mailer (reading its 'api-key' option) and compares against the posted signature with phutil_hashes_are_identical(). No configured mailer's key matched, so the request is rejected rather than processed - either Phabricator holds the wrong key for the account signing the webhook, or the request did not come from that Mailgun account.

Source

Thrown at src/applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php:44

      ));
    foreach ($mailers as $mailer) {
      $api_key = $mailer->getOption('api-key');
      $hash = hash_hmac('sha256', $timestamp.$token, $api_key);
      if (phutil_hashes_are_identical($sig, $hash)) {
        return true;
      }
    }

    return false;
  }

  public function handleRequest(AphrontRequest $request) {

    // No CSRF for Mailgun.
    $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();

    if (!$this->verifyMessage()) {
      throw new Exception(
        pht('Mail signature is not valid. Check your Mailgun API key.'));
    }

    $raw_headers = $request->getStr('message-headers');
    $raw_dict = array();
    if (strlen($raw_headers)) {
      $raw_headers = phutil_json_decode($raw_headers);
      foreach ($raw_headers as $raw_header) {
        list($name, $value) = $raw_header;
        $raw_dict[$name] = $value;
      }
    }

    $headers = array(
      'to'      => $request->getStr('recipient'),
      'from'    => $request->getStr('from'),
      'subject' => $request->getStr('subject'),
    ) + $raw_dict;

View on GitHub (pinned to 5720a38cfe)

Solutions

  1. Update the mailer's 'api-key' option in cluster.mailers to the current private API key of the Mailgun account that owns the webhook route, then re-send a test message.
  2. In the Mailgun dashboard, confirm the webhook URL points at this Phabricator install and belongs to the same account/domain as the configured key.
  3. If multiple Mailgun accounts are in play, add one inbound-enabled mailer entry per account so each signature can match its own key.
  4. Monitor the daemon/web log after the fix: a stream of these exceptions means inbound mail is being silently discarded.

Example fix

# before: key stale / from another account
# cluster.mailers: [{"key":"mailgun","type":"mailgun","options":{"domain":"mg.example.com","api-key":"key-old..."}}]

# after: current private key of the account that owns the webhook route
# cluster.mailers: [{"key":"mailgun","type":"mailgun","options":{"domain":"mg.example.com","api-key":"key-current..."}}]
# then re-verify by sending a test mail through Mailgun
Defensive patterns

Strategy: validation

Validate before calling

// Reproduce the controller's check when testing your webhook wiring.
$mailers = PhabricatorMetaMTAMail::newMailers(array(
  'inbound' => true,
  'types' => array(PhabricatorMailMailgunAdapter::ADAPTERTYPE),
));
foreach ($mailers as $mailer) {
  $hash = hash_hmac('sha256', $timestamp.$token, $mailer->getOption('api-key'));
  if (phutil_hashes_are_identical($signature, $hash)) {
    return true; // webhook will be accepted
  }
}
return false;

Prevention

When it happens

Trigger: A POST to the Mailgun receive endpoint where the signature verifies against none of the cluster.mailers entries of type mailgun with inbound enabled: api-key rotated in Mailgun but not in cluster.mailers; webhook registered to an install keyed for a different Mailgun account; forged or replayed/modified requests.

Common situations: Key rotation done on one side only; test/stage installs sharing a webhook URL; multiple Mailgun accounts (the signature only matches the account whose private key Phabricator stores). Every failing POST means that inbound mail was dropped.

Related errors


AI-assisted analysis of phacility/phabricator@5720a38cfe (2026-08-21). Data as JSON: /api/errors/ad410ddd0aaa9dc6. Report an issue: GitHub.