How to Secure and Verify Jira Webhooks with Hookdeck
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:
- Computing an HMAC-SHA256 hash of the raw request body using the webhook secret you configured at registration time
- Hex-encoding the result
- 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
Authorizationheader - 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
- Log in to your Jira Cloud instance as an administrator
- Navigate to Settings (gear icon) > System > WebHooks
- Click Create a WebHook
- Enter the webhook URL, Name, and Secret
- Select the events you want to receive
- Optionally configure a JQL filter to limit which issue events trigger the webhook
- 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
- Extract the
X-Hub-Signatureheader and confirm it starts withsha256= - Strip the
sha256=prefix to get the received signature - Compute HMAC-SHA256 over the raw (unparsed) request body using your configured secret
- Hex-encode the result
- 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
-
scoop bucket add hookdeck https://github.com/hookdeck/scoop-hookdeck-cli.git -
scoop install hookdeck
-
Download the latest release's tar.gz file.
-
tar -xvf hookdeck_X.X.X_linux_x86_64.tar.gz -
./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
- Copy the Hookdeck Source URL from the CLI output
- In Jira, go to Settings > System > WebHooks
- Click Create a WebHook
- Set the URL to your Hookdeck Source URL
- Set a Secret for HMAC verification
- Select the events you want to receive
- Click Create
Step 5: Configure source verification
- Open the Hookdeck Dashboard
- Navigate to Connections and select your source
- Under Advanced Source Configuration, enable Source Authentication
- Select HMAC as the authentication method
- Configure the HMAC settings:
- Algorithm: SHA-256
- Header: X-Hub-Signature
- Encoding: Hex
- Signature Prefix:
sha256= - Secret: Your Jira webhook secret
- Click Save
How Hookdeck verification works
When verification is enabled:
- Hookdeck receives the webhook from Jira
- Hookdeck validates the
X-Hub-Signatureagainst the payload using your secret - Valid requests are forwarded to your endpoint with
x-hookdeck-verified: true - 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:
- Raw body usage: Ensure you're using the exact bytes received, not a parsed/stringified version
- Secret accuracy: Confirm the secret matches what was configured when creating the webhook in Jira
- Header name: The header is
X-Hub-Signature(following the WebSub standard), not a Jira-specific header name - 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
- Check Jira's webhook logs: In Settings > System > WebHooks, check the Recent Deliveries section
- Verify JQL filter: If you configured a JQL filter, ensure your test events match the filter criteria
- 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.