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
- 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.
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
- 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.
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
- Failed to JSON decode response.
- Request failed with errors: %s.
- Provide a public key, not a private key!
- Invalid response token for this challenge: token digest does
- Request includes restricted parameter "%s", but this control
AI-assisted analysis of phacility/phabricator@5720a38cfe (2026-08-21).
Data as JSON: /api/errors/ad410ddd0aaa9dc6.
Report an issue: GitHub.