Securing Sparrow
Sparrow’s built-in authentication is deliberately minimal: one shared secret
(SPARROW_API_KEY) checked on every API request — optional in development,
required when ENVIRONMENT=production (the server refuses to start without
it). Everything beyond that — per-user logins, SSO, MFA, audit of who did
what — is a deployment concern, solved by putting an identity-aware proxy in
front of Sparrow rather than by building an identity provider into it.
This page covers what Sparrow does natively, what it assumes about your network, and the recommended way to add real user authentication. For the implementation details — SSRF protection, HTTP hardening headers, secret masking, tenant isolation — see the Security Model reference page.
What Sparrow provides natively
Section titled “What Sparrow provides natively”| Control | Mechanism |
|---|---|
| API authentication | SPARROW_API_KEY (master key) or per-person access tokens → X-API-Key or Authorization: Bearer header; master key mandatory in production |
| Secrets at rest | Envelope encryption (AES-256-GCM, per-record DEK) keyed by the SPARROW_ENCRYPTION_KEYS keyring |
| Outbound signing | Standard Webhooks signatures (v1 HMAC-SHA256, v1a Ed25519) on every delivery |
| SSRF protection | Private/loopback/link-local/metadata IPs blocked at connect time in every mode; cloud metadata endpoints always blocked (even with SPARROW_ALLOW_PRIVATE_NETWORKS=true); SPARROW_ALLOWED_NETWORKS for targeted internal access; redirects re-validated |
| Custom header validation | Header names must be valid HTTP tokens; values reject CR/LF/control chars and are capped at 8 KiB; reserved framing headers (Host, Content-Length, Transfer-Encoding, Connection, Keep-Alive, Proxy-Connection, TE, Trailer, Upgrade) are refused; Sparrow’s own signature and ID headers always win over custom headers |
| Portal path validation | The portal gateway rejects paths containing ., .., empty segments, backslashes, or control characters (403) |
| Browser access | CORS_ALLOWED_ORIGINS allowlist; security headers on all responses |
What it does not provide:
- User accounts. Access tokens give each person and machine their own credential, but identity is possession-based — there are no accounts, passwords, or MFA.
- Tenant isolation via consumers. Consumers organize resources; they are not a security boundary. Any valid API key can read and write every consumer.
- Rate limiting on the API. Put a reverse proxy in front if you need it.
Data retention and compliance posture
Section titled “Data retention and compliance posture”Sparrow stores event payloads and delivery request bodies as plaintext in PostgreSQL — they must stay queryable and transformable. Secrets and signing keys are envelope-encrypted; payloads are not. If payloads carry regulated data (PHI, PII), plan for it:
- Encrypt at the storage layer. Encryption-at-rest requirements (HIPAA-style) are satisfied below the application: encrypted disks/volumes, or the managed encryption RDS/Cloud SQL enable by default, plus TLS to PostgreSQL.
- Bound how long data lives. Set
SPARROW_EVENT_RETENTION_DAYSto purge events — and, via cascade, their deliveries — older than N days. Runs hourly in the background. Unset (the default), data is kept forever. - Keep response capture off.
capture_response_bodyis off by default; leave it off if endpoints may echo sensitive data back. - Self-hosting is the compliance boundary. Payloads never leave your infrastructure — there is no vendor to sign a BAA with.
Trust model of the embedded dashboard
Section titled “Trust model of the embedded dashboard”The embedded web UI (SPARROW_SERVE_UI=true) is designed for trusted,
private networks — a team dashboard on a VPN or internal network, not a
public-facing app:
- The dashboard and its assets are served without authentication so the login-free UI works out of the box.
- The dashboard needs a credential to call the API. When
SPARROW_API_KEYis set, the UI shows a Sign in to Sparrow prompt on the first401. A pasted master key is exchanged for a browser token so the master key is never stored. Access tokens and invite links also work. The server never writes the API key into the served HTML. - A separately hosted UI behaves the same
way by default. An
apiKeyin itsconfig.jsskips the prompt, but anyone who can load the UI can read the key. - To give a teammate access, create an invite (
sparrow invite alice) instead of pasting the key into chat. The invite link lives in the URL fragment, so it never reaches server or proxy logs. It works once, expires (default 24 hours, max 7 days), and creates a named token the invitee can use. The invite can be cancelled before use; the resulting token can be revoked at any time. See Access: Tokens and Invites for the full guide.
Consequences:
- On a private/VPN network where everyone with network access is trusted: fine as-is.
- On a shared or internet-facing network: do not expose the port directly. Put an authenticating proxy in front (next section) or disable the UI.
The consumer portal
Section titled “The consumer portal”The portal (/portal) uses a different trust model: each visitor carries a
portal token — an
expiring, revocable, consumer-scoped access token minted with the admin key
(POST /v1/tokens with a consumer). Portal users never log in and
need no accounts; the magic link is the credential, and it can only touch
that one consumer’s webhooks, subscriptions, and deliveries (never event
injection, other consumers, or admin routes). The /portal HTML is served
without the admin API key, and its framing headers allow embedding in
an iframe. See the security reference
for the exact scope.
How that plays out per network topology:
-
Sparrow on a VPN, consumers internal too — works as-is. Hand teams portal links instead of dashboard access: they manage their own endpoints without ever holding the admin key.
-
Sparrow on a VPN, consumers external — expose only the portal slice through your reverse proxy and keep everything else private:
SPARROW_API_KEYmust be set in this topology — without it the auth middleware is disabled and the exposed paths are open to everyone. The “proxy” can be a standalone reverse proxy or your own product’s backend forwarding those routes over the VPN — same origin as your app, so no CORS setup and no iframe required. Two constraints: paths must be preserved verbatim (/portal,/portal/api,/_appare root-absolute in the SPA — use a dedicated subdomain if they collide with your app’s routes), and the proxy must never inject the adminX-API-Keyon these routes. -
Embedded in your own product (the intended design) — your app is already public and already authenticates its users. Its backend calls the mint endpoint over the private network and renders the returned
/portal#token=...path in an iframe (or links to it via the proxy above). Sparrow delegates identity entirely to your product; the portal slice above is the only thing that needs to be reachable from the user’s browser.
What does not work: fetching /portal HTML server-side and inlining it
into your own pages. The portal is a single-page app — the HTML is only a
shell whose scripts and API calls (/_app/*, /portal/api/*) run from the
visitor’s browser and must reach Sparrow through one of the routes above.
Copying the HTML copies neither. If you want zero Sparrow UI in the browser,
build your own screens on the consumer-scoped REST API instead: your backend
holds the token, calls Sparrow over the VPN, and renders native components.
A full worked example of the external topology — nginx and Express proxy configs, the mint flow, and the threat analysis — is in Embedding the Consumer Portal.
Portal caveats: the token rides in the URL fragment, so anyone the link is
forwarded to can use it until it expires (default 7 days, max 30 — use a short
ttl_seconds for embedding) or you revoke it with DELETE /v1/tokens/{id}.
For named individuals, sparrow invite "acme support" --consumer acme creates
a one-time link that opens the portal and grants a named token of the same
kind. See Access: Tokens and
Invites.
Recommended: SSO via an identity-aware proxy
Section titled “Recommended: SSO via an identity-aware proxy”For per-user login — username/password, Microsoft Entra ID, Google, or any
OIDC/SAML provider — run an open-source auth overlay in front of Sparrow. The
proxy authenticates humans, then injects the X-API-Key header on every
authenticated request. Sparrow needs zero configuration changes beyond setting
the key, and the browser never sees the key at all.
Rules for this topology:
- Sparrow’s port must be reachable only from the proxy (and trusted machine clients). If clients can bypass the proxy, the overlay is decoration.
- Set
SPARROW_API_KEY; give it only to the proxy config and machine clients. - Set
CORS_ALLOWED_ORIGINSto the proxy’s public origin (or leave unset if the UI is served through the same origin). - Never inject
X-API-Keyon/portalor/portal/api/*if portal users go through the same proxy. A valid API key outranks portal token scoping, so blanket header injection silently escalates every portal visitor to admin. Exclude those paths from injection, or serve the portal on a separate route/vhost without the overlay.
Option A — Authentik (username/password + Entra + Google in one tool)
Section titled “Option A — Authentik (username/password + Entra + Google in one tool)”Authentik is a self-hosted identity provider with a built-in Proxy Provider mode, so it is both the IdP and the overlay:
- Local user database (passwords, passkeys, MFA) and federated sources (Microsoft Entra ID, Google, generic OIDC/SAML) side by side.
- Proxy Provider runs as an embedded outpost or standalone container; speaks forward-auth natively with Traefik, nginx, and Caddy.
- In the Proxy Provider settings, add a custom header injection so every
proxied request carries
X-API-Key: <SPARROW_API_KEY>.
Deployment: two containers (server + worker) plus PostgreSQL — you already run PostgreSQL for Sparrow. Verify that the external OAuth/SAML source types you need are available in the open-source tier of your Authentik version.
Option B — oauth2-proxy (single IdP, smallest footprint)
Section titled “Option B — oauth2-proxy (single IdP, smallest footprint)”If all users live in one IdP (an Entra tenant or a Google Workspace domain), oauth2-proxy is a single Go binary that does the whole job:
oauth2-proxy \ --provider=oidc \ --oidc-issuer-url=https://login.microsoftonline.com/<tenant>/v2.0 \ --upstream=http://sparrow:8080 \ --email-domain=yourcompany.comInject the API key upstream via its header configuration (e.g. an
injectRequestHeaders entry in the alpha config, or terminate at nginx and add
proxy_set_header X-API-Key ...). No local username/password support — that’s
the tradeoff for the small footprint.
Option C — Keycloak + oauth2-proxy (maximum boring)
Section titled “Option C — Keycloak + oauth2-proxy (maximum boring)”Keycloak if you want the battle-tested enterprise IdP: local users, identity brokering (Entra, Google, SAML), fine-grained roles — all free. Keycloak is only the IdP; pair it with oauth2-proxy (pointing at Keycloak as its OIDC issuer) as the actual overlay. Two moving parts instead of one, but every part is thoroughly documented and widely deployed.
Not a fit
Section titled “Not a fit”- Authelia — local users only; it cannot consume external OIDC providers (no Relying Party role), so no “login with Entra/Google”.
- Pomerium — excellent identity-aware proxy, but delegates to a single upstream IdP and has no built-in username/password store.
Hardening checklist
Section titled “Hardening checklist”Whether or not you add SSO:
- Set
SPARROW_API_KEY(generate:openssl rand -hex 32, at least 32 characters) — mandatory whenENVIRONMENT=production, and without it anyone who can reach the port owns the instance regardless of environment. - Configure
SPARROW_ENCRYPTION_KEYSandSPARROW_ENCRYPTION_PRIMARY_KEY_ID, and store the key material in a secret manager, never in the database or repo. - Use TLS to PostgreSQL (
sslmode=requireor stronger) whenever the database is not on localhost. - Set
CORS_ALLOWED_ORIGINSexplicitly for any browser-based access. - Use
SPARROW_ALLOWED_NETWORKS(comma-separated CIDRs) to reach internal webhook targets instead ofSPARROW_ALLOW_PRIVATE_NETWORKS=true. Cloud metadata endpoints are always blocked regardless of either setting. - Set
SPARROW_EVENT_RETENTION_DAYSif payloads carry regulated data or you have any retention policy — the default keeps events forever. - Terminate TLS and apply rate limiting at a reverse proxy; Sparrow serves plain HTTP.
- Confirm the server never writes
SPARROW_API_KEYinto the UI (this is now the default — the server no longer supports key injection). - Give people and CI jobs their own access tokens
instead of sharing
SPARROW_API_KEY. Use invites for people, direct token creation for machines. - Revoke tokens when someone leaves or a machine credential is retired
(
sparrow tokens revoke <id>orDELETE /v1/tokens/{id}). - Keep
/health,/ready,/docs, and/openapi.*in mind: they are intentionally unauthenticated. Restrict at the proxy if the OpenAPI spec is sensitive in your environment.