Configuring and Verifying Webhook Signatures
Use Webhook Signatures to verify that webhooks are coming from OpsLevel.
OpsLevel webhooks are sent with a signature that the destination server can use to verify that the event came from the OpsLevel platform and not a third party or malicious system.
Setting up your Account's Signing Secret
Note: You must have an Admin role to configure your account's Signing Secret
- Navigate to the Account page
- Scroll to the Webhook Signing section
- Press + Create Signing Secret
- When you are ready to securely store the Signing Secret, press the Create Signing Secret button in the modal dialog that appears. Note: After the dialog is closed, you will not be able to retrieve the Signing Secret, so ensure that you have a way to securely store the secret.
- Copy the Signing Secret before closing the dialog.
Rotating the Account's Signing Secret
Once a Signing Secret has been created, you may rotate the secret by first deleting the existing Signing Secret.
- Navigate to the Account page
- Scroll to the Webhook Signing section
- Press Delete Signing Secret
- Press Yes to confirm the action and Delete the Secret
- Follow the steps to Create a new Signing Secret
Note: Removing the original Signing Secret will break any existing signature verification that you may have in place. Be sure to be prepared to update the systems that validate the Signing Secret or temporarily disable them during the rotation to reduce or eliminate any outages.
How to verify signatures
Once a Signing Secret is configured for an OpsLevel Account, all OpsLevel webhooks sent from that Account will include an additional signature header: X-OpsLevel-Signature
.
A pseudo-algorithm to verify the signature of a webhook delivery is described below.
Step 1: Extract the signature from the request
- Extract the signature string from the X-OpsLevel-Signature header on the request. (This will look like
sha256=<SOME_SIGNATURE>
)
Step 2: Compute the expected valid signature
Using the received JSON payload, calculate the signature by:
- Start with the entire request, then remove any headers that were not part of the original request other than the X-OpsLevel-Timing header.
- For Actions, this should only include explicit headers that are part of the action configuration and the X-OpsLevel-Timing header.
- In all cases, ensure that the X-OpsLevel-Signature header has been removed before proceeding.
- Sort the remaining headers alphabetically, concatenate the headers (joined by commas) and append a
+
and the payload into a single string.- An example resultant string:
"Content-Type:application/json,X-OpsLevel-Timing:123456789+{\n "service": "shopping-cart-service",\n "repository": "shopping-cart-repo"\n}"
- An example resultant string:
- Compute the SHA-256 HMAC on the clean and sorted request using the shared secret as the key.
- Take the Hex encoding of the result to obtain a value we can compare.
Step 3: Compare the signatures
Compare the expected signature obtained in Step 2 with the provided signature from Step 1.
If the signatures match, the webhook should be considered a trusted and authentic request from OpsLevel that can be processed by the server.
Use the original request!
Computing the signature is sensitive to any change in the headers and request body. Code or middleware that changes the request before the signature is computed will result in the signatures not matching. Common causes of mismatched signatures include: capitalization changes, whitespace changes, character encoding changes, and line-ending changes (i.e. from
\n
to\r\n
or vice versa).
Sample Code Written in Ruby
def verify_webhook(headers, request_body)
# Remove and store the signature we are verifying
hmac_header = headers.delete("X-OpsLevel-Signature")
# Only keep a subset of headers for signature verification
headers_to_keep = ['X-OpsLevel-Timing']
header_string = headers.keep_if{ |key| headers_to_keep.include? key }.map { |key, value| "#{key}:#{value}" }.sort.join(",")
payload = "#{header_string}+#{request_body}"
calculated_hmac = OpenSSL::HMAC.hexdigest('sha256', CLIENT_SECRET, payload)
ActiveSupport::SecurityUtils.secure_compare("sha256=#{calculated_hmac}", hmac_header)
end
Additional Code Examples
Additional code examples (e.g. Python) can be found in our Community Repo here.
Updated about 2 months ago