Skip to content

Security

Hookaido provides layered security across ingress authentication, transport encryption, secret management, API access control, and egress protection.

Inbound Authentication (Ingress)

HMAC Signature Verification

Verify webhook signatures using HMAC-SHA256 with replay protection:

/webhooks/github {
  auth hmac env:HOOKAIDO_GITHUB_SECRET
}

Full control via block form:

/webhooks/github {
  auth hmac {
    secret env:HOOKAIDO_GITHUB_SECRET
    signature_header "X-Hub-Signature-256"
    timestamp_header "X-Timestamp"
    nonce_header "X-Nonce"
    tolerance 5m
  }
}

Replay protection: The timestamp header is checked against the tolerance window. The nonce header (when configured) provides additional replay defense.

Secret rotation: With secret_ref, verification tries all secrets valid at the request timestamp (from the signed timestamp header), allowing overlapping key rotation with zero downtime.

Providers

Provider-compatible HMAC verification short-circuits the canonical format and instead uses the wire format of a specific webhook provider:

/webhooks/github { auth hmac { provider github; secret env:GH_SECRET } }
/webhooks/gitea  { auth hmac { provider gitea;  secret env:GITEA_SECRET } }
/webhooks/stripe { auth hmac { provider stripe; secret env:STRIPE_SECRET } }
/webhooks/cituro { auth hmac { provider cituro; secret env:CITURO_SECRET } }
Provider Header Signed payload Replay protection
github X-Hub-Signature-256: sha256=<hex> body none (GitHub omits timestamp)
gitea X-Gitea-Signature: <hex> body none
stripe Stripe-Signature: t=<ts>,v1=<hex>[,v0=<hex>...] <ts>.<body> 5 min fixed tolerance
cituro X-CITURO-SIGNATURE: t=<ts>,s=<hex> <ts>.<body> 5 min fixed tolerance

stripe and cituro share the same signature scheme (timestamped HMAC-SHA256 over <ts>.<body>) — cituro is effectively an alias with a different header name (X-CITURO-SIGNATURE) and signature tag (s instead of v1). Both accept multiple comma-separated <tag>=<hex> pairs in the header; any matching signature verifies the request (useful for Stripe's v0/v1 rotation).

Provider mode is mutually exclusive with signature_header, timestamp_header, nonce_header, and tolerance — use the canonical block form (without provider) if you need to customise those.

Basic Auth

auth basic "webhook-user" "env:WEBHOOK_PASSWORD"

Forward Auth

Delegate to an external auth service:

auth forward "https://auth.example/check" {
  timeout 5s
  copy_headers "X-User-ID"
  body_limit 64kb
}

Forward auth fails closed: transport errors, timeouts, and non-2xx/401/403 responses all result in 503.

TLS and mTLS

Every listener (ingress, Pull API, Admin API) supports TLS with optional mutual TLS:

ingress {
  listen :8080
  tls {
    cert_file /path/to/cert.pem
    key_file  /path/to/key.pem
    client_ca /path/to/ca.pem         # enables mTLS
    client_auth require_and_verify    # optional
  }
}

pull_api {
  listen :9443
  tls {
    cert_file /path/to/pull-cert.pem
    key_file  /path/to/pull-key.pem
    client_ca /path/to/pull-ca.pem
  }
}

Client Auth Modes

Value Description
none No client certificate requested
request Request certificate, don't require it
require Require certificate (default when client_ca is set)
verify_if_given Verify certificate only if provided
require_and_verify Require and verify a valid client certificate

Hookaido does not include automatic certificate provisioning (ACME / Let's Encrypt). For production deployments, terminate TLS at a reverse proxy or cloud load balancer and forward plain HTTP to Hookaido's ingress listener:

Approach Example
Reverse proxy Caddy, nginx, Traefik (automatic ACME)
Cloud LB AWS ALB/NLB, GCP HTTPS LB, Azure App Gateway
Service mesh Istio, Linkerd sidecar TLS

Use Hookaido's built-in tls { cert_file, key_file } when you need:

  • mTLS between Hookaido and internal callers (Pull API, Admin API).
  • Direct TLS with certificates from a private CA or cert-manager.

Tip: In the common DMZ-pull deployment, the ingress listener sits behind a TLS-terminating reverse proxy while the Pull API and Admin API use mTLS or stay on localhost.

Secret Management

Secrets should never appear as plaintext in config files. Hookaido supports several reference types:

Reference Example Description
env: env:MY_SECRET Environment variable
file: file:/run/secrets/key File content
vault: vault:secret/data/hookaido#token HashiCorp Vault (or compatible HTTP API)
raw: raw:literal-value Inline value (dev only)

For vault: refs, configure access via environment variables:

  • HOOKAIDO_VAULT_ADDR (required): Vault base URL, e.g. https://vault.example.com:8200
  • HOOKAIDO_VAULT_TOKEN (required): Vault token used as X-Vault-Token
  • HOOKAIDO_VAULT_NAMESPACE (optional): sent as X-Vault-Namespace
  • HOOKAIDO_VAULT_TIMEOUT (optional): request timeout (default 5s)
  • HOOKAIDO_VAULT_CACERT (optional): CA bundle path for TLS verification
  • HOOKAIDO_VAULT_CLIENT_CERT + HOOKAIDO_VAULT_CLIENT_KEY (optional): mTLS client cert/key pair
  • HOOKAIDO_VAULT_INSECURE_SKIP_VERIFY (optional): on|off / true|false / 1|0

Named Secrets with Rotation

Define secrets with validity windows in a secrets block:

secrets {
  secret "hmac-v1" {
    value env:HMAC_SECRET_V1
    valid_from "2026-01-01T00:00:00Z"
    valid_until "2026-07-01T00:00:00Z"
  }
  secret "hmac-v2" {
    value env:HMAC_SECRET_V2
    valid_from "2026-06-01T00:00:00Z"
  }
}
  • valid_from is inclusive, valid_until is exclusive.
  • Omit valid_until for keys that don't expire.
  • Referenced via secret_ref "hmac-v1" in auth and signing blocks.

Rotation semantics:

  • Signing selects the newest (or oldest) valid secret at signing time.
  • Verification tries all secrets valid at the request timestamp — not wall-clock time — enabling safe rotation with overlapping windows.

Runtime Rotation via Admin API

For HMAC verification secrets that the upstream issuer rotates at runtime (Cituro's roll-secret, Stripe webhook-endpoint rotation, etc.), declare the pool as runtime true:

secrets {
  secret "cituro" {
    runtime true
    max_versions 16
    # optional bootstrap: seeded once if the DB pool is empty
    value env:CITURO_BOOTSTRAP_SECRET
    valid_from "2026-04-21T00:00:00Z"
  }
}

The issuer service (e.g. soapNEO) then pushes fresh secrets via the admin API:

POST /admin/secrets/cituro   # add new secret during overlap window
DELETE /admin/secrets/cituro/sec_<old>  # after cut-over

Requirements:

  • HOOKAIDO_SECRET_ENCRYPTION_KEY (32 bytes, base64) must be exported. Runtime-added secrets are AES-256-GCM sealed before they are written to the backing store (SQLite runtime_secrets table, or Postgres equivalent). The key is only in memory — the DB alone is not sufficient to decrypt.
  • Backing store must be SQLite or Postgres. Memory-only deployments accept the writes but lose them on restart (a warning is logged at startup).
  • The admin token already protects the endpoint. Keep the admin listener on an internal interface.

Operational notes:

  • Push before cut-over: the issuer must POST the secret to Hookaido before the upstream provider begins issuing webhooks signed with it. Otherwise the verifyStripe/verifyCituro 5-minute tolerance window will silently drop early requests.
  • Backup policy: the runtime_secrets table contains sealed ciphertext; without HOOKAIDO_SECRET_ENCRYPTION_KEY it is unrecoverable. Store the key separately from the DB backup.
  • Key rotation: rotating HOOKAIDO_SECRET_ENCRYPTION_KEY invalidates all persisted runtime secrets. Plan a two-phase rotation (new key → issuer re-pushes → old key retired) — no automated re-wrap in v1.
  • Overlap windows: use not_before/not_after on the POST body to describe the rotation overlap. Set.ValidAt already filters by time, so both secrets can be live for the cut-over window.
  • Expired-version GC: a background sweeper runs every 5 minutes (plus once at startup) and removes versions whose not_after is in the past, both from the in-memory pool and from the persisted runtime_secrets table. Without it, short overlap windows would cause GET /admin/secrets/<name> and the DB to grow unboundedly since the per-POST opportunistic prune only fires when the pool is at max_versions. Each sweep emits a secret_gc_pruned log line per affected pool and increments hookaido_runtime_secret_gc_pruned_total{pool="<name>"}. The interval is not user-configurable in v1.

API Access Control

Pull API

Token-based authentication is required:

pull_api {
  auth token env:HOOKAIDO_PULL_TOKEN
}

Per-route tokens can override the global allowlist:

/webhooks/github {
  pull {
    path /pull/github
    auth token env:GITHUB_PULL_TOKEN
  }
}

Admin API

Default bind: 127.0.0.1:2019 (localhost only). Optional token auth:

admin_api {
  listen 127.0.0.1:2019
  auth token env:HOOKAIDO_ADMIN_TOKEN
}

The Admin API should only be exposed over a secure channel. Use TLS + token auth or mTLS when exposing beyond localhost.

Audit Trail

All Admin API mutations require X-Hookaido-Audit-Reason and emit structured audit log events with:

  • Timestamp, actor, reason, request ID
  • Operation details (state transitions, queue counts, config changes)

Optional stricter audit requirements via defaults.publish_policy:

  • require_actor on — require X-Hookaido-Audit-Actor
  • require_request_id on — require X-Request-ID

Scoped managed operations can additionally restrict actors:

  • actor_allow "ci-bot" — only listed actors allowed
  • actor_prefix "deploy-" — actors must match prefix

Egress Protection (SSRF)

All outbound deliveries (push mode) enforce SSRF-safe defaults:

defaults {
  egress {
    allow "*.internal.example.com"
    deny "169.254.0.0/16"     # AWS metadata
    deny "10.0.0.0/8"         # private ranges
    https_only on                # only HTTPS targets (default)
    redirects off                # don't follow redirects (default)
    dns_rebind_protection on     # block DNS rebinding (default)
  }
}

Evaluation order: Deny rules first, then allowlist (if configured, target must match).

Wildcard support:

  • * — matches any host
  • *.example.com — matches subdomains only (not the apex)

Publish Policy

Control who can publish messages programmatically via the Admin API:

defaults {
  publish_policy {
    direct on        # allow POST /messages/publish
    managed on       # allow endpoint-scoped publish
    allow_pull_routes on            # allow publish to pull-mode routes
    allow_deliver_routes on         # allow publish to push-mode routes
    require_actor off         # require X-Hookaido-Audit-Actor
    require_request_id off    # require X-Request-ID
    fail_closed off  # fail closed when context unavailable
    actor_allow "ci-bot"     # explicit actor allowlist
    actor_prefix "deploy-"   # actor prefix match
  }
}

Per-route publish control:

/webhooks/github {
  publish off            # block all manual publish to this route
  # or selectively:
  publish {
    direct off           # block global direct publish path
    managed off          # block endpoint-scoped managed publish path
  }
}

Security Checklist

  • [ ] Use HMAC or forward auth on all ingress routes
  • [ ] Never put secrets as plaintext in the Hookaidofile — use env:, file:, or vault: refs
  • [ ] Enable TLS on all externally accessible listeners (ingress, Pull API)
  • [ ] Admin API: keep on localhost or protect with mTLS + token auth
  • [ ] Configure egress.deny for internal network ranges
  • [ ] Enable audit requirements (require_actor, require_request_id) in production
  • [ ] Rotate secrets using named secret_ref entries with overlapping validity windows
  • [ ] Review publish_policy defaults — restrict by actor/route mode as needed

Documentation Index