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:
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¶
Forward Auth¶
Delegate to an external auth service:
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 |
Production TLS: Reverse Proxy Recommended¶
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:8200HOOKAIDO_VAULT_TOKEN(required): Vault token used asX-Vault-TokenHOOKAIDO_VAULT_NAMESPACE(optional): sent asX-Vault-NamespaceHOOKAIDO_VAULT_TIMEOUT(optional): request timeout (default5s)HOOKAIDO_VAULT_CACERT(optional): CA bundle path for TLS verificationHOOKAIDO_VAULT_CLIENT_CERT+HOOKAIDO_VAULT_CLIENT_KEY(optional): mTLS client cert/key pairHOOKAIDO_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_fromis inclusive,valid_untilis exclusive.- Omit
valid_untilfor 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 (SQLiteruntime_secretstable, 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/verifyCituro5-minute tolerance window will silently drop early requests. - Backup policy: the
runtime_secretstable contains sealed ciphertext; withoutHOOKAIDO_SECRET_ENCRYPTION_KEYit is unrecoverable. Store the key separately from the DB backup. - Key rotation: rotating
HOOKAIDO_SECRET_ENCRYPTION_KEYinvalidates 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_afteron the POST body to describe the rotation overlap.Set.ValidAtalready 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_afteris in the past, both from the in-memory pool and from the persistedruntime_secretstable. Without it, short overlap windows would causeGET /admin/secrets/<name>and the DB to grow unboundedly since the per-POSTopportunistic prune only fires when the pool is atmax_versions. Each sweep emits asecret_gc_prunedlog line per affected pool and incrementshookaido_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:
Per-route tokens can override the global allowlist:
Admin API¶
Default bind: 127.0.0.1:2019 (localhost only). Optional token auth:
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— requireX-Hookaido-Audit-Actorrequire_request_id on— requireX-Request-ID
Scoped managed operations can additionally restrict actors:
actor_allow "ci-bot"— only listed actors allowedactor_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:, orvault:refs - [ ] Enable TLS on all externally accessible listeners (ingress, Pull API)
- [ ] Admin API: keep on localhost or protect with mTLS + token auth
- [ ] Configure
egress.denyfor internal network ranges - [ ] Enable audit requirements (
require_actor,require_request_id) in production - [ ] Rotate secrets using named
secret_refentries with overlapping validity windows - [ ] Review
publish_policydefaults — restrict by actor/route mode as needed