docs: Document Aura HITL approval webhook HMAC signing - #91
promptless[bot] wants to merge 1 commit into
Conversation
Document the opt-in HMAC-SHA256 root of trust for the Aura HITL approval webhook exchange added in mezmo/aura PR #491. Covers enabling signing via the AURA_HITL_WEBHOOK_SECRET* environment variables, the request/decision signature contract and header shape, the responder signing obligation for both approval routes, and the zero-downtime key rotation procedure. Relates to: mezmo/aura PR #491
|
|
||
| ### Enable Signing | ||
|
|
||
| To enable signing, set the `AURA_HITL_WEBHOOK_SECRET` environment variable. The |
There was a problem hiding this comment.
AURA_HITL_WEBHOOK_SECRET (primary, enables signing), AURA_HITL_WEBHOOK_SECRET_SECONDARY (verify-only), AURA_HITL_WEBHOOK_TOLERANCE_SECS (default 300, max 86400) and MIN_SECRET_BYTES=32 constants.
| `openssl rand -hex 32`, which produces a 64-character value used as-is with no | ||
| decoding step. | ||
|
|
||
| A present-but-empty or whitespace-only secret is a hard startup error, so Aura |
There was a problem hiding this comment.
load_from_env: unset primary = feature off (warn! log "HITL webhook HMAC verification DISABLED"); present-but-empty/whitespace primary = ConfigError::PrimaryTooShort; secondary without primary = ConfigError::SecondaryWithoutPrimary; successful load emits info! "HITL webhook HMAC signing and verification enabled".
| A present-but-empty or whitespace-only secret is a hard startup error, so Aura | ||
| fails loudly rather than silently disabling signing. When a secret is set | ||
| together with an `http://` (unencrypted) webhook URL, Aura refuses to start. | ||
| HTTPS is required when signing is on. Secrets are read once at startup, so |
There was a problem hiding this comment.
Secrets loaded once at startup via WebhookHmac::load_from_env(); a config error fails server boot. Same once-at-startup load pattern in crates/aura-cli/src/backend/direct.rs#L94-L103.
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
|
|
||
| A present-but-empty or whitespace-only secret is a hard startup error, so Aura | ||
| fails loudly rather than silently disabling signing. When a secret is set | ||
| together with an `http://` (unencrypted) webhook URL, Aura refuses to start. |
There was a problem hiding this comment.
validate_webhook_signing_config: with an HMAC secret configured, an http:// webhook URL fails at boot with PlaintextWebhookUrlError; HTTPS required when signing is on.
| Aura's outbound approval-request POST carries two headers: | ||
|
|
||
| ```http | ||
| X-Aura-Signature-256: sha256=<64 lowercase hex> |
There was a problem hiding this comment.
Header contract: X-Aura-Signature-256: sha256=<64 lowercase hex chars>, X-Aura-Timestamp: .
| ``` | ||
|
|
||
| The signed string is the canonical form | ||
| `{unix_timestamp}.{context}.{raw_request_body}`. Aura computes HMAC-SHA256 over |
There was a problem hiding this comment.
Canonical signed payload "{timestamp}.{context}.{body}" via keyed_mac, shared by sign() and verify() so the two legs cannot drift on payload encoding. Context labels: egress request "approval-request:{decision_id}" (route.rs#L381-L383), decision "approval-decision:{decision_id}" (route.rs#L419-L421, handlers.rs#L1141).
| that string with the secret and hex-encodes the result in lowercase. The approval | ||
| request uses the context `approval-request:{decision_id}`. | ||
|
|
||
| `raw_request_body` is the exact bytes of the request body as sent on the wire. |
There was a problem hiding this comment.
Ingress endpoint buffers raw bytes via the Bytes extractor and calls authorize_ingress() before any JSON parse; only after verification does it re-parse through the stock Json extractor.
| ```text | ||
| signed_string = "{timestamp}.{context}.{raw_body}" | ||
| signature = "sha256=" + lowercase_hex(hmac_sha256(secret, signed_string)) | ||
| # Verify an inbound request: recompute with context "approval-request:{decision_id}" |
There was a problem hiding this comment.
verify(): checks timestamp skew against tolerance, then evaluates primary and secondary candidates via constant-time verify_slice before returning Ok/Mismatch.
|
|
||
| | Where the signature check fails | Result | | ||
| |---|---| | ||
| | Webhook response leg | The tool fails closed. The worker receives an approval channel error. | |
There was a problem hiding this comment.
Webhook response leg: signature verification failure returns ApprovalError::ResponseUnverified, which tool.rs maps to a generic Err(ToolError::ToolCallError) tool failure — same generic-error path as the pre-existing BadStatus/Parse rows on this page.
| | Where the signature check fails | Result | | ||
| |---|---| | ||
| | Webhook response leg | The tool fails closed. The worker receives an approval channel error. | | ||
| | Approval ingress endpoint (`POST /v1/approvals/{decision_id}`) | Aura returns a uniform `401`. | |
There was a problem hiding this comment.
unauthorized_response(): every ingress verification failure (missing/malformed headers, skew, mismatch) maps to the same uniform 401.
| decisions. Complete this on all instances before continuing. | ||
| 2. Update each responder to sign its decisions with the new key. During this | ||
| window, keep each responder able to verify Aura's requests with both the old | ||
| and new keys, because Aura still signs its outbound requests with the old key |
There was a problem hiding this comment.
sign() computes the outbound HMAC using only self.primary — the secondary key is never used for signing, only for verification (L390-L421) — supporting the rotation guidance that Aura keeps signing outbound requests with the primary key until it is promoted.
|
|
||
| | Environment variable | Default | Description | | ||
| |---|---|---| | ||
| | `AURA_HITL_WEBHOOK_SECRET` | none (signing off) | Primary HMAC key. Presence enables signing. Raw UTF-8 bytes are used as the key, with a minimum of 32 bytes. An empty or whitespace-only value is a startup error. | |
There was a problem hiding this comment.
Same three env vars and defaults (AURA_HITL_WEBHOOK_SECRET, _SECONDARY, _TOLERANCE_SECS default 300 max 86400) reused in the configuration-reference table.
| | `AURA_HITL_WEBHOOK_SECRET_SECONDARY` | none | Optional second key, used for verification only, to support zero-downtime key rotation. Setting it without a primary is a startup error. | | ||
| | `AURA_HITL_WEBHOOK_TOLERANCE_SECS` | `300` | Allowed timestamp skew window, in seconds. Valid range is 1 to 86400. | | ||
|
|
||
| When a secret is set together with an `http://` (unencrypted) webhook URL, Aura |
There was a problem hiding this comment.
Boot-time validate_webhook_signing_config rejects an http:// webhook URL when a secret is configured (PlaintextWebhookUrlError).
Open in Promptless
Aura added an opt-in HMAC-SHA256 "root of trust" for the human-in-the-loop (HITL) approval webhook exchange (mezmo/aura PR #491), which verifies that the party returning an approval decision is the same party Aura sent the request to. The Aura docs did not cover it, and the HITL page still stated that webhook egress had "no built-in authentication layer."
This suggestion documents the feature on two existing Aura pages:
aura/hitl.mdx: a new "Sign and Verify Approval Webhooks" section covering how to enable signing with theAURA_HITL_WEBHOOK_SECRET*environment variables (32-byte minimum,openssl rand -hex 32, fail-loud on an empty secret, HTTPS required when signing is on, read once at startup), the request signature and header contract Aura sends, the responder's obligation to sign its decision on both the webhook response and thePOST /v1/approvals/{decision_id}route, the invalid-signature outcomes, and a zero-downtime key rotation procedure. It also updates the "Conversational route" note and replaces the now-outdated "Current limitations" bullet.aura/configuration-reference.mdx: a concise "Webhook Signing" subsection under[hitl]documenting the three environment variables, framed as environment-variable configuration, with a cross-link to the HITL page.No navigation change is needed because
aura/hitlis already in the sidebar.Trigger Events
Tip: Use Slack message actions (⋯ menu → Update Docs) to capture doc updates without interrupting conversations 💬