Credentials
Two credential types are accepted. Endpoints require a scope, never a particular credential, so a caller can move from one to the other without an endpoint change or a flag day.API key
X-API-Key: <secret>Named per caller, several per scope, compared in constant time.
The default for machine callers today.OIDC bearer token
Authorization: Bearer <jwt>RS256, verified against the issuer’s JWKS. For OAuth2
client-credentials clients (Auth0 machine-to-machine).Scopes
A credential that authenticates but lacks the scope is refused with
403,
distinct from the 401 of a missing or invalid credential. The distinction
matters when reading the log: one is a misconfigured client, the other is
someone knocking.
Named keys and rotation
A key is configured asname:secret, and several may be active per scope:
EXTERNAL_API_KEY, PROCESS_API_KEY) still works and
takes the scope as its caller name.
OIDC / OAuth2 client credentials
Set an issuer and an audience and the service additionally accepts bearer tokens:
What is verified: the RS256 signature against the issuer’s published keys,
the audience, the issuer, and the expiry. The algorithm is pinned — an
alg: none or an HMAC-signed token is refused whatever its header claims.
Scopes are read from the scope claim or from Auth0’s permissions claim,
so both plain OAuth2 scopes and Auth0 RBAC work. The signing keys are
cached and refetched when a token presents an unknown kid, so the issuer
can rotate its keys without a restart here.
Leaving OIDC_ISSUER unset disables the path entirely: a bearer token is
then not a credential at all, it is ignored.
The verification path is covered by tests against a locally generated
signing key — signature, audience, issuer, expiry, algorithm pinning,
scope mapping and key rotation. It has not yet been run against a live
tenant; that needs the issuer and audience of the tenant in question.
Fail closed
A service that cannot tell its callers apart serves nobody. With no API key and no OIDC issuer configured, every authenticated route answers503 and
names the missing configuration. /health stays open so the platform can
still probe and schedule the container.
ALLOW_UNAUTHENTICATED=true turns that off for local development. It is
never set by the deployment template, and the service emits a warning event
on every start when it is on.
Rate limiting
A token bucket per caller:RATE_LIMIT_PER_MINUTE (default 120) sustained,
RATE_LIMIT_BURST (default 60) in a burst. Exceeding it returns 429 with
a Retry-After header. Setting the rate to 0 disables it.
The bucket lives in the process, so with several replicas the effective
limit is per replica. It exists to keep one runaway client — or a
credential-guessing loop — from consuming the instance; a global quota
belongs in a gateway in front of the service.
The console stream
The browser’sEventSource cannot set request headers, which is the usual
reason a key ends up in a query string, where gateways and platform logs
record it in plain text. Instead the console exchanges its key for a
short-lived token:
What gets recorded
Every state-changing call and every refusal lands in the event log next to the pipeline events:
Read-only traffic is not logged; it would bury the pipeline events the
console exists to show. The health probe, the console page and the
stream-token exchange are excluded for the same reason.
Secrets at rest
By default the Bicep template stores the secrets as Container Apps secrets. Those are encrypted at rest but readable to anyone who can runaz containerapp show --show-secrets on the resource group.
Deploying with useKeyVault=true instead creates a Key Vault and a
user-assigned managed identity, writes the secrets there, and leaves only
references on the container app. Access then goes through the identity and
is auditable in the vault’s own log.
Outbound
The call to Engrate uses their scheme — the API key raw in theauthorization header, over TLS, per their
authentication guide. The
key is held as a deployment secret and is never written to the repository,
the audit trail or the event log; the console masks it.
Known limits
Stated plainly, because a security page that lists only strengths is not worth reading:- The rate limit is per replica, not global.
- The console is served by the same app as the API; it is protected by the process scope, and anyone holding that credential can change runtime configuration.
- The audit trail and event log are files on an Azure Files share; their integrity depends on the storage account’s access control, and they are not signed or append-only at the storage layer.
- The ACR pull still uses the registry’s admin password rather than the managed identity.