Gareth Wilson Gareth Wilson

How to Secure and Verify Jira Webhooks with Hookdeck

Published


Jira is the most widely used project management and issue tracking platform, powering development workflows for teams across every industry. Jira's webhook system enables real-time integration with external tools — pushing event data to your endpoints whenever issues change, comments are added, sprints progress, or versions are released.

However, accepting webhooks without proper verification leaves your application vulnerable to spoofed requests from malicious actors. In this guide, we'll show you how to secure and verify Jira webhooks manually and with Hookdeck, ensuring that every webhook your application processes genuinely originated from Jira. For a comprehensive overview of Jira webhook capabilities, see our guide to Jira webhooks.

Why webhook security matters

Webhooks are HTTP callbacks that notify your application when events occur. Without verification, an attacker could send fake webhook payloads to your endpoint, potentially triggering unauthorized actions like:

  • Creating or modifying issues in your downstream systems based on fabricated events
  • Triggering CI/CD pipelines with spoofed issue transitions
  • Corrupting project data synced from Jira webhooks
  • Manipulating workflow automations tied to Jira events

Jira addresses this by supporting HMAC-SHA256 signature verification via the X-Hub-Signature header. By verifying this signature, you can be confident that webhooks are authentic and haven't been tampered with in transit.

How Jira webhook signatures work

Jira Cloud uses HMAC-SHA256 signature verification (added in February 2024). When you register a webhook with a secret, every webhook request includes an X-Hub-Signature header containing the signature in the format sha256=<hex_digest>.

The X-Hub-Signature header

The header format looks like this:

sha256=5d7370c827b66f5e0b8e2e9b6ab0dc4f1e8a92c3d7f6b5a4e3d2c1b0a9f8e7d6

The signature is computed by:

  1. Computing an HMAC-SHA256 hash of the raw request body using the webhook secret you configured at registration time
  2. Hex-encoding the result
  3. Prepending sha256=

Important limitations

  • HMAC-SHA256 verification is only available for Jira Cloud admin-registered and REST API webhooks (added February 2024)
  • Connect app webhooks use a separate JWT-based mechanism via the Authorization header
  • Jira Data Center support depends on your version — check your instance's documentation
  • The webhook secret cannot be retrieved after creation — store it securely when you first configure it

Getting your Jira webhook secret

When registering a webhook via the Jira Cloud administration UI or REST API, you provide a secret during setup:

Via the Jira administration UI

  1. Log in to your Jira Cloud instance as an administrator
  2. Navigate to Settings (gear icon) > System > WebHooks
  3. Click Create a WebHook
  4. Enter the webhook URL, Name, and Secret
  5. Select the events you want to receive
  6. Optionally configure a JQL filter to limit which issue events trigger the webhook
  7. Click Create

Via the REST API

curl -X POST "https://your-domain.atlassian.net/rest/api/2/webhook" \
  -H "Content-Type: application/json" \
  -H "Authorization: Basic <base64_credentials>" \
  -d '{
    "name": "My Webhook",
    "url": "https://your-endpoint.com/webhooks/jira",
    "events": ["jira:issue_created", "jira:issue_updated"],
    "secret": "your-webhook-secret"
  }'

Important: Store the secret securely at the time of creation — Jira does not allow you to retrieve it later.

Verifying webhook signatures manually

Verification process overview

  1. Extract the X-Hub-Signature header and confirm it starts with sha256=
  2. Strip the sha256= prefix to get the received signature
  3. Compute HMAC-SHA256 over the raw (unparsed) request body using your configured secret
  4. Hex-encode the result
  5. Compare using a constant-time function

Node.js verification example

const express = require("express");
const crypto = require("crypto");

const app = express();
const JIRA_WEBHOOK_SECRET = process.env.JIRA_WEBHOOK_SECRET;

function verifyJiraWebhook(rawBody, signatureHeader) {
  if (!signatureHeader || !signatureHeader.startsWith("sha256=")) {
    console.error("Missing or malformed X-Hub-Signature header");
    return false;
  }

  // Strip the sha256= prefix
  const receivedSignature = signatureHeader.slice(7);

  // Compute HMAC-SHA256
  const computedSignature = crypto
    .createHmac("sha256", JIRA_WEBHOOK_SECRET)
    .update(rawBody, "utf8")
    .digest("hex");

  // Use timing-safe comparison
  const receivedBuffer = Buffer.from(receivedSignature, "utf8");
  const expectedBuffer = Buffer.from(computedSignature, "utf8");

  if (receivedBuffer.length !== expectedBuffer.length) {
    return false;
  }

  return crypto.timingSafeEqual(receivedBuffer, expectedBuffer);
}

app.post(
  "/webhooks/jira",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const signatureHeader = req.headers["x-hub-signature"];
    const rawBody = req.body.toString();

    if (!verifyJiraWebhook(rawBody, signatureHeader)) {
      return res.status(401).json({ error: "Invalid signature" });
    }

    const event = JSON.parse(rawBody);

    // Process the verified webhook
    switch (event.webhookEvent) {
      case "jira:issue_created":
        handleIssueCreated(event);
        break;
      case "jira:issue_updated":
        handleIssueUpdated(event);
        break;
      default:
        console.log(`Received event: ${event.webhookEvent}`);
    }

    res.status(200).json({ received: true });
  }
);

app.listen(3000, () => {
  console.log("Webhook server listening on port 3000");
});

Python verification example

import hmac
import hashlib
import os
from flask import Flask, request, jsonify, abort

app = Flask(__name__)
JIRA_WEBHOOK_SECRET = os.environ.get("JIRA_WEBHOOK_SECRET")

def verify_jira_webhook(raw_body, signature_header):
    """Verify a Jira webhook signature."""
    if not signature_header or not signature_header.startswith("sha256="):
        return False

    # Strip the sha256= prefix
    received_signature = signature_header[7:]

    # Compute HMAC-SHA256
    computed_signature = hmac.new(
        JIRA_WEBHOOK_SECRET.encode("utf-8"),
        raw_body,
        hashlib.sha256
    ).hexdigest()

    # Use timing-safe comparison
    return hmac.compare_digest(computed_signature, received_signature)

@app.route("/webhooks/jira", methods=["POST"])
def handle_webhook():
    signature_header = request.headers.get("X-Hub-Signature", "")
    raw_body = request.get_data()

    if not verify_jira_webhook(raw_body, signature_header):
        abort(401, "Invalid signature")

    event = request.get_json()
    print(f"Received verified {event.get('webhookEvent')} event")

    # Process the webhook payload
    return jsonify({"received": True}), 200

if __name__ == "__main__":
    app.run(port=3000)

Critical security best practices

Use timing-safe comparisons

Never use standard equality operators (== or ===) to compare signatures. These can leak timing information that attackers exploit. Always use:

  • Node.js: crypto.timingSafeEqual()
  • Python: hmac.compare_digest()
  • Go: subtle.ConstantTimeCompare()

Preserve the raw request body

The HMAC signature is computed on the exact bytes Jira sends. Any parsing, reformatting, or middleware that modifies the body before verification will produce a different signature. Capture the raw body before JSON parsing.

Secure secret storage

  • Store secrets in environment variables or a secrets manager
  • Never hardcode secrets in source code
  • Never commit secrets to version control
  • Since Jira doesn't allow retrieving the secret after creation, back it up securely

Use idempotent processing

Jira may deliver the same webhook multiple times due to retries. Use the X-Atlassian-Webhook-Identifier header (when available) to deduplicate events and ensure idempotent processing.

Respond within Jira's timeout window

Jira enforces a 5-second connection timeout and 20-second response timeout. Ensure your endpoint acknowledges webhooks promptly to avoid retries and potential webhook disabling.

Simplifying verification with Hookdeck

Manually implementing and maintaining webhook verification across multiple providers can be complex and error-prone. Hookdeck provides a webhook gateway that handles verification automatically, allowing you to focus on processing events.

What is Hookdeck?

Hookdeck provides an event gateway that sits between webhook providers (like Jira) and your application. It provides:

  • Automatic signature verification
  • Event queuing and retry logic
  • Request logging and debugging tools
  • Local development tunneling

Setting up Jira webhooks with Hookdeck

Step 1: Install the Hookdeck CLI

  npm install hookdeck-cli -g
  
  
  yarn global add hookdeck-cli
  
  
    brew install hookdeck/hookdeck/hookdeck
    
    
  1.     scoop bucket add hookdeck https://github.com/hookdeck/scoop-hookdeck-cli.git
        
        
  2.   scoop install hookdeck
      
      
  1. Download the latest release's tar.gz file.

  2.     tar -xvf hookdeck_X.X.X_linux_x86_64.tar.gz
        
        
  3.   ./hookdeck
      
      

Step 2: Authenticate

hookdeck login

This opens your browser for authentication. If you don't have a Hookdeck account, you can create one during this step.

Step 3: Create a connection

hookdeck listen 3000 jira-source --path /webhooks/jira

This command:

  • Creates a public URL for receiving webhooks
  • Forwards events to http://localhost:3000/webhooks/jira
  • Displays the Source URL to configure in Jira

Step 4: Configure Jira

  1. Copy the Hookdeck Source URL from the CLI output
  2. In Jira, go to Settings > System > WebHooks
  3. Click Create a WebHook
  4. Set the URL to your Hookdeck Source URL
  5. Set a Secret for HMAC verification
  6. Select the events you want to receive
  7. Click Create

Step 5: Configure source verification

  1. Open the Hookdeck Dashboard
  2. Navigate to Connections and select your source
  3. Under Advanced Source Configuration, enable Source Authentication
  4. Select HMAC as the authentication method
  5. Configure the HMAC settings:
    • Algorithm: SHA-256
    • Header: X-Hub-Signature
    • Encoding: Hex
    • Signature Prefix: sha256=
    • Secret: Your Jira webhook secret
  6. Click Save

How Hookdeck verification works

When verification is enabled:

  1. Hookdeck receives the webhook from Jira
  2. Hookdeck validates the X-Hub-Signature against the payload using your secret
  3. Valid requests are forwarded to your endpoint with x-hookdeck-verified: true
  4. Invalid requests are rejected and logged as "Verification Failed"

This means your application can trust any request from Hookdeck without implementing its own Jira signature verification logic. You only need to implement Hookdeck's signature verification on your server.

Troubleshooting common issues

Signature mismatch

If signatures don't match, verify:

  1. Raw body usage: Ensure you're using the exact bytes received, not a parsed/stringified version
  2. Secret accuracy: Confirm the secret matches what was configured when creating the webhook in Jira
  3. Header name: The header is X-Hub-Signature (following the WebSub standard), not a Jira-specific header name
  4. Prefix handling: Remember to strip the sha256= prefix before comparing

Missing X-Hub-Signature header

  • Confirm you configured a secret when creating the webhook — without a secret, Jira doesn't send the signature header
  • Check that you're using Jira Cloud — Data Center versions may not support HMAC verification depending on the version
  • Connect app webhooks use JWT authentication instead of HMAC

Webhooks not arriving

  1. Check Jira's webhook logs: In Settings > System > WebHooks, check the Recent Deliveries section
  2. Verify JQL filter: If you configured a JQL filter, ensure your test events match the filter criteria
  3. Check concurrency limits: Jira enforces a limit of 20 concurrent requests per tenant + webhook URL host pair

Conclusion

Securing Jira webhooks requires verifying the HMAC-SHA256 signature on every incoming request. While you can implement this verification manually, Hookdeck simplifies the process by providing automatic signature verification, event management, and development tools, allowing you to focus on building your integration rather than infrastructure.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.