Skip to content

Authentication

Every incoming webhook is an integration runbook exposed on a public endpoint, and the runbook's Authentication setting decides what a caller has to present. The platform offers six methods. The three shipped endpoints (Alerts, Validation Checks, Validation Records) are delivered set to Halo API Bearer Token, and an administrator can change any of them to another method.

The callers are systems, not people: a scanner, a SIEM, a CI pipeline, an automation engine. Pick the method from what the sending system can actually do, not from what is strongest on paper.

Navigation

The method is set per runbook under Configuration > Integrations > Custom Integrations > Integration Runbooks, on the runbook's Details tab. API applications for the bearer-token method live under Configuration > Integrations > Halo API.


Choosing a method

Method What the caller sends Credential lifetime Use it when
Halo API Bearer Token (shipped default) Authorization: Bearer <token> obtained from the token endpoint first About an hour, then refetch The sender is code you control and can run a token request before each batch: a pipeline, a script, an n8n workflow
Validate Signature using Secret Key An HMAC of the request body in a header you name Static, until you rotate it The sender can compute an HMAC but cannot fetch a token. Also proves the body was not altered in transit
Secret in Header A fixed secret in a custom header Static The sender can only attach fixed headers (most SIEM and scanner webhook outputs)
Basic Authentication Authorization: Basic <base64 user:password> Static The sender offers basic auth and nothing else (Alertmanager-style receivers, event subscriptions that accept one static header)
Secret in URI Parameter The secret as a query-string parameter on the URL Static Only when the sender can set nothing but the URL. Last resort
No Authentication Nothing - Never, on a tenant that holds compliance evidence

Recommendation. Keep the bearer token where the caller is your own code. Where a sender can only attach static values, prefer Validate Signature, because the secret never leaves the sender and a modified payload is rejected. Fall back to Secret in Header or Basic Authentication when the sender cannot sign. Avoid Secret in URI Parameter, and never use No Authentication.

One method per endpoint

The setting belongs to the runbook, so changing it changes what every caller of that endpoint must send. Before switching an endpoint away from the bearer token, confirm every existing sender can meet the new method. If two senders genuinely need different methods, that is a case for a second runbook, not for weakening the shared one.

A rejected signature returns 401 Unauthorized, and the other methods return the same class of response when the credential is missing or wrong. A 202 means the credential was accepted; see Responses for what it does and does not tell you after that.


Halo API Bearer Token

The API uses OAuth2. A system authenticates with the client credentials grant: it exchanges an API application's client id and secret for a short-lived access token, then presents that token on each webhook call. (The token endpoint also supports grants for people, such as the password and authorization-code grants. A webhook sender should not use those.)

1. Create the API application

Create one application per integration, not one shared across all of them. Separate applications give you separate credentials to rotate, separate permissions to scope, and a distinguishable actor in the audit trail.

  1. Go to Configuration > Integrations > Halo API.
  2. Add a new application.
  3. Set the authentication method to Client ID and Secret (Services) and the login type to Agent.
  4. Record the Client ID and Client Secret. Copy the secret when it is generated and store it in your secrets manager immediately.
  5. Grant only the module permissions the integration needs. An alert-filing pipeline does not need read access to billing.

The application is the identity your integration authenticates as

Give it a name that identifies the integration (GuardDuty Alert Feed, not API User 3). That name is what makes the credential recognisable in the application list, in any log that records it, and to an assessor asking which system holds it.

2. Get a token

curl -X POST "https://<your Halo domain>/auth/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=<your client id>" \
  -d "client_secret=<your client secret>" \
  -d "scope=all"

The response contains an access_token and an expires_in value, the token's lifetime in seconds.

On a hosted tenant the token endpoint may need the tenant named on the query string: /auth/token?tenant=<tenant id>. The tenant id is the tenant_id value at https://<your Halo domain>/api/instanceinfo. If the request above returns an error about the tenant, add the parameter.

Tokens expire after about an hour

Fetch a token, use it, and fetch a new one when it expires. Do not hardcode a token. It will stop working roughly an hour after you paste it.

This is the single most common cause of an integration that works on the day it is built and fails the next morning. If your sending system can only attach fixed, static headers and cannot run a token request first, do not force it onto this method. Switch the endpoint to Validate Signature or Secret in Header instead.

Restrict the scope below all where your integration allows it.

3. Call the webhook with the token

curl -X POST "<webhook URL>" \
  -H "Authorization: Bearer <access_token>" \
  -H "Content-Type: application/json" \
  -d '{ "site_id": 1, "summary": "Example alert" }'

Validate Signature using Secret Key

The sender and the platform share a secret. The sender computes an HMAC of the request body with that secret and puts it in a header. The platform recomputes the HMAC over the body it received and rejects the request if the two differ, so a valid signature proves both that the sender holds the secret and that the body arrived unchanged.

This is the strongest of the static-credential methods. The secret itself is never transmitted.

Configure the runbook

On the runbook's Details tab, set Authentication to Validate Signature using Secret Key and fill in:

Setting Value Notes
Secret Key A random string of at least 32 characters Generate it, do not invent it. Store it in your secrets manager; it is the sender's credential
Signature header name e.g. x-halo-signature Any name. The sender must use exactly this header
Signature prefix e.g. sha256= Prepended to the digest in the header. Whatever is configured here, the sender must send exactly the same prefix
Signing Algorithm e.g. SHA-256 HMAC-SHA256 is the common choice and the one shown below
Digest e.g. Base64 How the HMAC bytes are encoded in the header. The sender must match

The shipped runbooks sign only the request body; no headers are configured for signing.

Sign each request

Compute the HMAC over the exact bytes you send as the body. Serialize the JSON once, sign that string, and send that same string. Re-serializing between signing and sending (a different key order, different whitespace) produces a body that no longer matches the signature and a 401.

With openssl, for header x-halo-signature, prefix sha256=, algorithm SHA-256 and digest Base64:

BODY='{"site_id":1,"summary":"Example alert"}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET_KEY" -binary | base64)

curl -X POST "<webhook URL>" \
  -H "Content-Type: application/json" \
  -H "x-halo-signature: sha256=$SIG" \
  --data-binary "$BODY"

The same in Python, with payload (a dict), secret_key and url set beforehand:

import base64, hmac, hashlib, json, requests

body = json.dumps(payload, separators=(",", ":")).encode()
digest = hmac.new(secret_key.encode(), body, hashlib.sha256).digest()
signature = "sha256=" + base64.b64encode(digest).decode()

requests.post(url, data=body, headers={
    "Content-Type": "application/json",
    "x-halo-signature": signature,
})

If your tenant's Digest option offers hex and you select it, replace the Base64 step with hexdigest() in Python, or in openssl replace -binary | base64 with -hex | awk '{print $NF}', since openssl dgst -hex prefixes its output with a label that must be stripped.

If a correctly computed signature is still rejected

Halo's published signing example sends the bare Base64 digest in the header even though the form shows a sha256= prefix, and it also sends x-halo-date (the current UTC time in RFC 1123 format) and x-halo-signature-algorithm headers. The guide does not say whether the prefix is stripped before comparison or whether those two headers are required. If a signature you have verified locally is rejected, try sending it without the prefix, then add the two headers, and confirm which form your tenant accepts.

A rejected signature leaves no trace in the runbook log

When the signature does not validate, the platform returns 401 and does not record the request. If you are debugging a sender that gets 401 and the runbook's Log tab shows nothing, the signature is the first thing to check: header name, prefix, digest encoding, and whether the body bytes were re-serialized after signing.


Secret in Header

The sender includes a fixed secret in a custom header on every request. The platform compares it with the value stored on the runbook.

Configure the runbook

Set Authentication to Secret in Header. Set the header name and secret as the runbook's settings for this method provide, and generate a secret of at least 32 random characters.

Call the webhook

curl -X POST "<webhook URL>" \
  -H "Content-Type: application/json" \
  -H "<header name>: <secret>" \
  -d '{ "site_id": 1, "summary": "Example alert" }'

This is the natural fit for a scanner or SIEM whose webhook output lets you add static headers but nothing more. Unlike a signature, it does not protect the body from modification, and the secret travels on every request, so it depends entirely on TLS. Rotate it by changing the value on the runbook and on the sender in the same change window; between the two edits, calls fail with 401.


Basic Authentication

The sender presents a username and password in the standard Authorization: Basic header. The platform compares them with the pair stored on the runbook.

Configure the runbook

Set Authentication to Basic Authentication and set the username and password. Treat the password as a generated secret, not a word.

Call the webhook

curl -X POST "<webhook URL>" \
  -u "<username>:<password>" \
  -H "Content-Type: application/json" \
  -d '{ "site_id": 1, "summary": "Example alert" }'

Some senders let you configure a single static header rather than a username and password. The equivalent is an Authorization header whose value is the word Basic, a space, and the Base64 encoding of username:password:

printf '%s' "<username>:<password>" | base64

Basic authentication is functionally the same class as Secret in Header: a static credential on every request, protected only by TLS. Choose it when the sender's configuration offers basic auth as its authentication option, which is common in alert-manager and event-subscription products.


Secret in URI Parameter

The sender appends the secret to the webhook URL as a query-string parameter. The platform compares it with the value stored on the runbook.

<webhook URL>?<parameter name>=<secret>

Use this only when the sending system can set nothing but a URL. Query strings are written to web-server access logs, proxy logs, and monitoring tools on both sides of the connection, so the secret ends up in places a header never reaches. If you have to use it, rotate the secret on a shorter schedule than you would a header secret, and treat any log export that includes request URLs as containing a credential.


No Authentication

The endpoint accepts any request. Do not enable this on a tenant that holds compliance evidence. An unauthenticated endpoint lets anyone who learns the URL file alerts and validation results as though they came from your tooling, and there is no actor to attribute them to afterwards.


Finding the webhook URL

Each incoming endpoint has its own URL, and the URLs are specific to your tenant.

  1. Go to Configuration > Integrations > Custom Integrations > Integration Runbooks.
  2. Open the runbook for the endpoint you want.
  3. On the Details tab, scroll to Runbook Start Access. Under the heading "This runbook can be started by doing a POST to the following URL", the page shows the URL. Copy it exactly.

Three fields on that panel matter. The URL is only displayed when the first is set to the public-endpoint option:

Field Required value
Runbook Start Access Can only be started from Halo and from a public endpoint
Authentication The method you chose above. Shipped as Halo API Bearer Token
Initial Webhook Verification None

If Runbook Start Access reads "Can only be started from Halo", there is no inbound URL and the runbook cannot be called externally until that is changed.

The page also notes that ticket variables cannot be used when a runbook is started from this URL. That does not affect these three endpoints, which take everything they need from the JSON body, but it is worth knowing if you build your own.

Only administrators can create runbooks. An agent who has been granted access to a specific runbook through Webhook and Integration Runbook Access Control can view and edit it without administrator access to the rest of the configuration.

Initial Webhook Verification

Leave this set to None. It exists to answer a challenge-response handshake that some providers send when you register a subscription with them; Slack and Microsoft Graph do this. A scanner or pipeline posting evidence never issues such a challenge, so enabling it only adds a step nobody performs. It is separate from Authentication and does not replace it.


Handling failures

Response Meaning What to do
401 / 403 The credential was missing or rejected Bearer token: fetch a fresh one, then check the application's permissions. Signature: check header name, prefix, digest encoding, and that the body bytes were not re-serialized after signing. Header secret, basic auth, URI secret: confirm the sender's value matches the runbook's, and that nobody rotated one side without the other.
404 Wrong URL, or the runbook's start access does not expose a public endpoint Re-copy the URL from the runbook page and confirm its start access setting.
202 but the record never appears The call authenticated and was accepted, but the payload was rejected during processing Open the runbook's Log tab, which names the failing step and its rejection code, then check the endpoint page's rejection reasons.

A rejected payload is not an authentication problem. A 202 means your credentials worked and the request was accepted; it says nothing about whether the write succeeded. See Responses.


Security considerations

  • Rotate credentials on the schedule your organization requires, and immediately if one may have been exposed. Rotating one integration's credential does not affect the others, which is the reason for one API application, or one runbook secret, per integration.
  • Generate static secrets; never choose them. Signature keys, header secrets and basic-auth passwords should be at least 32 random characters from a generator, stored in a secrets manager on the sending side.
  • Never put a secret in a URL, a query string, or a webhook payload unless the endpoint is deliberately configured for Secret in URI Parameter, and then only because the sender leaves no alternative.
  • Use HTTPS for every call, including the token request. Every static-credential method relies on TLS to keep the credential private.
  • Prefer the method that proves the most. A signature proves possession of the secret and integrity of the body. A bearer token proves possession of a credential that expires on its own. A static header secret proves only that the sender knew a value.
  • In a FedRAMP boundary, an external system posting into the platform is an information exchange that must be documented. See the interconnection guidance on the Integrations overview before enabling a new sender.