Every origin decides who may call it with an authentication block, a sibling of action in sb.yml (auth is an accepted alias). SBproxy ships twelve built-in providers, from a static API key to a full OpenID Connect login. This page is the chooser: which provider fits which caller, how to accept more than one on the same origin, and what the rest of the gateway does with the identity a provider establishes. The field-by-field reference for all twelve lives in configuration.md.
Two related things are deliberately absent from the tables below. mTLS client certificates are verified during the TLS handshake, before any auth provider runs, so they are configured on the listener rather than per origin; see what rides alongside. And a type: value that names none of the twelve falls through to the auth plugin registry, so a linked plugin crate can add types such as oauth_introspection or saml without patching the proxy (configuration.md).
The whole decision path, socket to policy chain. Stages the pipeline runs between these boxes (CORS preflight, bot and agent identity resolution, and the rest) are in architecture.md:
flowchart TD
REQ["Request arrives,\nhostname matched to an origin"] --> TLS{"Listener mTLS\nconfigured?"}
TLS -->|no| AB{"The origin's\nauthentication block"}
TLS -->|yes| MV["Client certificate verified\nin the TLS handshake"]
MV -->|"handshake fails"| DROP["Rejected before\nany provider runs"]
MV -->|verified| AB
AB -->|"single provider"| ONE["The one provider\naccepts or rejects"]
AB -->|"composition list"| SLOT["Try the next entry\nin declared order"]
SLOT -->|"rejects, entries remain"| SLOT
SLOT -->|accepts| WIN["The winner binds the principal:\nattribution, access log principal_kind,\ndecision records, the auth metric"]
SLOT -->|"rejects, list exhausted"| DENY["Denied at the proxy:\nthe first entry's status and message,\nall WWW-Authenticate challenges\nmerged on (RFC 7235)"]
ONE -->|accepts| WIN
ONE -->|rejects| DENY
WIN --> POL["Request policy chain\n(rate limits, WAF, object_authz, ...)"]
Which provider
People at browsers and terminals
| Provider | What it proves | Reach for it when | Example |
|---|---|---|---|
basic_auth |
The caller knows a username and password pair. | A simple internal service or admin panel needs a lock on it. | auth-basic |
digest |
The caller knows a password, proven by an RFC 7616 hash exchange that keeps the password off the wire. | A legacy system insists on digest auth. SHA-256 is the default; MD5 stays available but you have to ask for it. | auth-digest |
oidc |
The caller completed a login at your IdP and holds a sealed session cookie. | You want SSO in front of an app that has none, the oauth2-proxy and Cloudflare Access use case. | oidc |
forward_auth |
An external HTTP service you run answered the success status for a per-request subrequest. | Auth logic already lives in its own service and the gateway should defer to it. | auth-forward |
Services, agents, and crawlers
| Provider | What it proves | Reach for it when | Example |
|---|---|---|---|
api_key |
Possession of a static key, sent in a header or an opt-in query parameter. | The plainest machine-to-machine handshake is enough. | auth-api-key |
bearer |
Possession of a static token, optionally bound to a DPoP proof-of-possession key so a stolen token alone is not enough. | Token-based service auth without an issuer in the loop. | auth-bearer, auth-bearer-dpop |
jwt |
A token signed by an issuer you trust, with issuer, audience, algorithm, and claim checks. Accepts JWE-encrypted tokens (decrypt, then verify) and can require DPoP or mTLS-bound cnf claims. |
Callers hold OAuth2/OIDC-issued tokens and the gateway should verify them locally. | auth-jwt |
hmac_auth |
The caller holds a shared secret and signed this exact request (RFC 9421, hmac-sha256), so a captured request replays nowhere else. |
Webhook senders and API clients that should never put a reusable credential on the wire. | auth-hmac |
ldap_auth |
The directory accepted a bind with the presented credentials. | Service accounts, and people, whose credentials live in LDAP or Active Directory and should stop working the moment the directory says so. | auth-ldap |
bot_auth |
An RFC 9421 signature from a key in your agent directory, Ed25519 in the IETF Web Bot Auth pattern. | You admit known AI crawlers by signature and reject anything merely claiming to be one. | web-bot-auth |
cap |
A capability token from a trusted issuer, carrying its own path and rate grants. | Paid or contracted crawler traffic proves its grant per request. | auth-cap |
Scaffolding
| Provider | What it proves | Reach for it when | Example |
|---|---|---|---|
noop |
Nothing. Every request passes. | You want the config to say out loud that an origin is deliberately unauthenticated. | One line of config; no example needed. |
The same split, as a triage. The tables above carry the detail:
flowchart TD
Q{"Who calls this origin?"} -->|"people at browsers\nand terminals"| P{"Where does the\ncredential live?"}
Q -->|"services, agents,\nand crawlers"| M{"What can the\ncaller present?"}
Q -->|"no one is challenged,\non purpose"| NOOP["noop"]
P -->|"your IdP, as an\nSSO login"| OIDC["oidc"]
P -->|"an auth service\nyou already run"| FA["forward_auth"]
P -->|"a username and\npassword pair"| PW["basic_auth, or digest\nwhen a legacy system\ninsists on RFC 7616"]
M -->|"issuer-signed\ntokens"| JWT["jwt"]
M -->|"credentials in LDAP or\nActive Directory"| LDAP["ldap_auth"]
M -->|"a crawler signature\nor grant"| BOT["bot_auth, or cap for\npaid contracted traffic"]
M -->|"a static secret"| KEY["api_key or bearer;\nhmac_auth to keep it\noff the wire"]
Accepting more than one provider
authentication also takes a list of providers. The list is an OR: entries run in declared order and the first one that accepts the request wins.
- Order matters. Every request walks the list from the top, so put the provider most callers use first.
- The winner binds the request. Audit events, decision records, and the auth metric name the provider that authenticated, and principal attribution comes from the winning entry's own credential metadata. Nothing merges across entries.
- Rejection is collective, challenges are merged. When every provider rejects, the response carries the first provider's status and message, with each provider's
WWW-Authenticatechallenge merged onto it (RFC 7235 allows several challenges on one response). A client that failed everywhere sees every scheme the origin accepts. - A failing provider loses only its own slot. Whatever the reason, the next entry still runs.
Four shapes are refused when the config compiles, each for a stated reason. A one-entry list: write a single provider as a plain mapping. noop in a list: it would admit every request and make the other entries decorative. forward_auth in a list: it runs as a separate subrequest and works only as an origin's sole provider. oidc in a list: it needs the login-callback endpoint that only a sole oidc block wires up.
The migration shape
The list exists mostly for credential cutovers. Keep the credential your callers hold today in the first slot, add the one you are moving them to underneath, and there is no flag day: both work on the same origin until the old one stops winning. This is examples/auth-composition/, runnable as shipped:
proxy:
http_bind_port: 8080
origins:
"api.local":
action:
type: proxy
url: https://test.sbproxy.dev
# Providers run top to bottom; the first success wins and names
# itself in the audit and decision records. Put the provider most
# callers still use first.
authentication:
- type: api_key
header_name: X-Api-Key
api_keys:
- legacy-key-1
- type: bearer
tokens:
- new-token-1
For a cutover to issuer-signed tokens instead, the second entry becomes a jwt block with your jwks_url and issuer; configuration.md shows exactly that pairing. Either way, progress is measurable: the access log's principal_kind column names the provider that won each request, so you can watch legacy traffic drain and delete the old entry when it reaches zero.
One round trip through that config, from a caller already moved to the new token:
sequenceDiagram
participant C as Client
participant P as SBproxy
participant U as test.sbproxy.dev
C->>P: GET /get, Host: api.local<br/>Authorization: Bearer new-token-1
Note over P: authentication list: [api_key, bearer]
P->>P: api_key finds no X-Api-Key header,<br/>loses its slot
P->>P: bearer matches new-token-1 and wins
P->>U: Request forwarded,<br/>bearer principal bound
U-->>P: 200 OK
P-->>C: 200 OK
Note over P: Access log principal_kind names bearer,<br/>the winner, never the composite
What rides alongside authentication
- Principal attribution. Each credential entry can carry
project,user,team,tags, and free-formmetadata; a match stamps them on the request principal, and they surface in the access log'sprincipal_kindand attribution columns, in metric labels, and in policy scripts asprincipal.attrs.*. See per-credential metadata. - Decision records. Every auth allow and deny publishes an
authrecord on the decision-audit feed for SIEM consumers. The record carries the method, never the subject. See decision-records.md. - Trust tiers. Verifier outcomes feed the four-value trust tier: a verified
bot_authsignature or CAP token earnsstrong, a failed one drops the request tosuspicious, and policies read the result asrequest.trust_tier. See trust-tiers.md. - mTLS at the listener.
proxy.mtlsverifies client certificates in the TLS handshake, before any provider here runs, and passes the verified cert metadata upstream asX-Client-Cert-*headers. See mTLS client authentication and mtls-client-auth. - DPoP binding.
bearerandjwttakerequire_dpopto demand an RFC 9449 proof on every request, andjwtadditionally takesrequire_mtls_boundfor RFC 8705 certificate-bound tokens. See sender-constrained Bearer. For the proofs SBproxy itself mints on upstream calls, see outbound-dpop.md.
Honest notes
Three providers make a network call while the request waits. ldap_auth binds against the directory on every request; bind results are deliberately not cached, so a password the directory revokes stops working immediately, and an unreachable directory answers 503 rather than admitting anyone. forward_auth sends one subrequest per request, and an unreachable auth service is likewise a 503, never a pass. oidc touches the IdP only during login, where the code exchange is a live POST to token_endpoint; requests carrying a session cookie authenticate locally until the session expires, so an IdP outage blocks new logins, not established sessions. Budget latency accordingly, and set the timeouts (http_client_timeouts, ldap_auth.timeout_secs) for your backends' real behavior. Everything else verifies against local state, with one caveat: providers configured with remote key material (jwt with a jwks_url, cap, bot_auth with a hosted directory) fetch and cache those keys over the network, and cap and bot_auth refuse rather than admit when a fetch fails.
Secrets in auth config ride the central resolver. API keys, tokens, HA1 hashes, HMAC secrets, and client secrets all accept the same reference forms as every other secret-bearing field: ${VAR}, env:, file:, or a backend URI such as vault://. A reference nothing can resolve refuses to boot instead of becoming the credential. See secrets.md.
See also
- configuration.md - the field tables for all twelve providers.
- api-gateway.md - where auth sits in the traditional reverse-proxy pillar.
- key-management.md - dynamic virtual keys minted and revoked at runtime. A request authenticated by a minted key skips the origin's configured provider; the two paths are alternatives, never a chain.
- object-authz.md - fine-grained authorization after a caller is authenticated.