Security
Trust model, payload exposure, webhook signing, and dashboard access.
Trust model, payload exposure, webhook signing, and dashboard access.
A worker runs whatever task name a job names, with whatever payload it carries. So anyone who can write to the storage can run code on your workers. Treat the database / Redis credentials as code-execution credentials:
Task arguments and results are serialized with JSON or msgpack and stored as plain bytes — by default not encrypted. Anyone who can read the storage can read them.
SignedSerializer — it HMAC-signs each
payload so a worker rejects anything altered or forged (integrity, not
confidentiality).EncryptedSerializer — AES-256-GCM gives
both confidentiality and integrity. Share the secret across producers/workers.Webhook deliveries are HMAC-SHA256 signed when the subscription has a secret;
the signature rides in x-flexiq-signature: sha256=…. Verify it on the
receiver before trusting the body.
const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
// timing-safe compare against the x-flexiq-signature headerDelivery targets are checked against an SSRF (server-side request forgery)
guard. A URL must be http/https with a host, and the host must not be a
local-only name (localhost, *.internal, *.local, …) or resolve to a
loopback, unspecified, RFC1918, CGNAT, link-local (including
169.254.169.254), multicast, reserved, or IPv6 unique-local address. The URL
itself is checked when a subscription is registered and again before every
delivery attempt, and redirects are never followed — a 3xx ends the
attempt-chain instead of walking past the guard. A blocked delivery is recorded
with status failed and is not retried.
FLEXIQ_WEBHOOKS_ALLOW_PRIVATE=1 # disable the guard for local developmentThe address check runs inside the resolver the socket dials with, so the address
connected to is the address that passed — a name rebound after registration, or
between the check and the connect, cannot slip through. The original hostname
still drives the Host header and TLS SNI. Resolution is bounded by the
subscription's timeoutMs; a resolver failure is a transient error and is
retried like any other. For user-supplied URLs, keep an egress allowlist in
front of the worker as defense in depth.
The dashboard runs in open mode by
default — anyone who reaches the port has full control. For production,
pass authEnabled: true (or --auth) to enable the session flow —
first-run setup, password login, CSRF, admin/viewer roles — or
auth: { token } to gate the API with a legacy shared bearer token. Either
way, bind it to localhost or a private network, or mount it behind your own
auth via the Express /
Fastify helpers.
Every dashboard response carries defense-in-depth headers: a
Content-Security-Policy locked to the dashboard's own origin,
X-Content-Type-Options: nosniff, X-Frame-Options: DENY (the dashboard
can't be framed), and Referrer-Policy: same-origin.
FLEXIQ_WEBHOOKS_ALLOW_PRIVATE is unset in production.authEnabled: true / --auth), and the port
bound to localhost or behind auth.onEnqueue to
redact).