{"record":{"id":"ad410ddd0aaa9dc6","repo":"phacility/phabricator","slug":"mail-signature-is-not-valid-check-your-mailgun-ap","errorCode":null,"errorMessage":"Mail signature is not valid. Check your Mailgun API key.","messagePattern":"Mail signature is not valid\\. Check your Mailgun API key\\.","errorType":"exception","errorClass":"Exception","httpStatus":null,"severity":"critical","filePath":"src/applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php","lineNumber":44,"sourceCode":"      ));\n    foreach ($mailers as $mailer) {\n      $api_key = $mailer->getOption('api-key');\n      $hash = hash_hmac('sha256', $timestamp.$token, $api_key);\n      if (phutil_hashes_are_identical($sig, $hash)) {\n        return true;\n      }\n    }\n\n    return false;\n  }\n\n  public function handleRequest(AphrontRequest $request) {\n\n    // No CSRF for Mailgun.\n    $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();\n\n    if (!$this->verifyMessage()) {\n      throw new Exception(\n        pht('Mail signature is not valid. Check your Mailgun API key.'));\n    }\n\n    $raw_headers = $request->getStr('message-headers');\n    $raw_dict = array();\n    if (strlen($raw_headers)) {\n      $raw_headers = phutil_json_decode($raw_headers);\n      foreach ($raw_headers as $raw_header) {\n        list($name, $value) = $raw_header;\n        $raw_dict[$name] = $value;\n      }\n    }\n\n    $headers = array(\n      'to'      => $request->getStr('recipient'),\n      'from'    => $request->getStr('from'),\n      'subject' => $request->getStr('subject'),\n    ) + $raw_dict;","sourceCodeStart":26,"sourceCodeEnd":62,"githubUrl":"https://github.com/phacility/phabricator/blob/5720a38cfe95b00ca4be5016dd0d2f3195f4fa04/src/applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php#L26-L62","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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.","In the Mailgun dashboard, confirm the webhook URL points at this Phabricator install and belongs to the same account/domain as the configured key.","If multiple Mailgun accounts are in play, add one inbound-enabled mailer entry per account so each signature can match its own key.","Monitor the daemon/web log after the fix: a stream of these exceptions means inbound mail is being silently discarded."],"exampleFix":"# before: key stale / from another account\n# cluster.mailers: [{\"key\":\"mailgun\",\"type\":\"mailgun\",\"options\":{\"domain\":\"mg.example.com\",\"api-key\":\"key-old...\"}}]\n\n# after: current private key of the account that owns the webhook route\n# cluster.mailers: [{\"key\":\"mailgun\",\"type\":\"mailgun\",\"options\":{\"domain\":\"mg.example.com\",\"api-key\":\"key-current...\"}}]\n# then re-verify by sending a test mail through Mailgun","handlingStrategy":"validation","validationCode":"// Reproduce the controller's check when testing your webhook wiring.\n$mailers = PhabricatorMetaMTAMail::newMailers(array(\n  'inbound' => true,\n  'types' => array(PhabricatorMailMailgunAdapter::ADAPTERTYPE),\n));\nforeach ($mailers as $mailer) {\n  $hash = hash_hmac('sha256', $timestamp.$token, $mailer->getOption('api-key'));\n  if (phutil_hashes_are_identical($signature, $hash)) {\n    return true; // webhook will be accepted\n  }\n}\nreturn false;","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Rotate Mailgun API keys and the matching cluster.mailers 'api-key' option together, never one side alone.","Register the webhook URL per Mailgun account, and configure one inbound mailgun mailer per account so every signature can match.","Alert on this exception: each occurrence is dropped inbound mail, not noise."],"tags":["phabricator","metamta","mailgun","webhook","hmac","api-key","security"],"backgroundTag":"webhook-signature-invalid","analyzedSha":"5720a38cfe95b00ca4be5016dd0d2f3195f4fa04","analyzedAt":"2026-08-21T05:07:25.672Z","schemaVersion":2},"datasetVersion":"2026-08-21T11:28:35.574Z"}