Health & metrics
The API exposes two health probes, a bearer-gated Prometheus endpoint, and a per-client rate limiter. Every response — healthy or not — carries a fixed set of security headers, and every error is returned as an RFC 7807 problem document.Health probes
Two probes are registered on the FastAPI app, both withinclude_in_schema=False so they
do not appear in the public OpenAPI document:
apps/api/src/neobank/main.py
Liveness confirms the process is up; readiness confirms it can serve a real request. Wire
/health/live to a liveness probe and /health/ready to a readiness probe in Kubernetes
or your load balancer — they are exempt from the rate limiter (see below), so probe traffic
does not consume any client’s budget.
Prometheus metrics
The endpoint is gated by a static bearer token. If
NEOBANK_METRICS_TOKEN is unset, or
the Authorization header does not match Bearer <token>, the endpoint returns 404
rather than 401 — it does not advertise that it exists:
apps/api/src/neobank/main.py
Behind the single-server Caddy edge and the shipped Helm ingress,
/internal/* is not
routed publicly. On Kubernetes, only networkPolicy.metricsNamespace may reach API port
8000 in addition to the ingress controller namespace. Scrape from that namespace with the
32-character-or-longer bearer token. On the server, scrape inside the private network or
through an authenticated sidecar.Rate limiting
The rate limiter runs as ASGI middleware ahead of the router./health/* and /internal/*
are exempt; every other request is limited:
apps/api/src/neobank/middleware.py
The limit is keyed first by client address, then by credential (the
Authorization
header or session cookie). A request consumes budget on both buckets when a credential is
present, so a single token cannot bypass the per-IP ceiling and a single IP cannot exhaust
a per-token ceiling alone.
Uvicorn accepts X-Forwarded-For only from NEOBANK_TRUSTED_PROXY_IPS. The server Compose
deployment pins Caddy to 172.30.255.2 and trusts only that address. The Helm value
config.trustedProxyIps must match the source address or CIDR of the ingress controller
pods; the NetworkPolicy separately limits port 8000 ingress to
networkPolicy.ingressNamespace. Never use *, 0.0.0.0/0, or ::/0. A mismatched value causes callers to
share the proxy address bucket; an overbroad value lets another network peer spoof client
addresses.
When a bucket is exhausted the middleware returns a 429 as an RFC 7807 problem document
with a Retry-After: 60 header:
apps/api/src/neobank/middleware.py
Headers on every response
The request-context middleware attaches five headers to every HTTP response, including errors:apps/api/src/neobank/middleware.py
X-Request-ID— generated per request (or echoed when the caller supplies one); the same value is written to the audit trail and the structured log line. Use it to correlate a user report with logs and an Audit row.X-Content-Type-Options: nosniff— prevents MIME sniffing on responses.Referrer-Policy: same-origin— the API origin is not leaked as a referrer.Permissions-Policy— denies camera, microphone, and geolocation to the API origin. The WebAuthn entries forapp.perflo.ailive on the web app’s edge headers, not here; see Perflo hosted origins.Cache-Control: no-store— API responses are never cached.
apps/web/security-headers.conf) adds the CSP and the WebAuthn
Permissions-Policy entries in addition to these.
Errors are problem documents
Every error — validation failures, auth rejections, provider errors, rate limits — is serialized asapplication/problem+json with the ProblemDetails shape (type, title,
status, detail, instance, code, request_id, retryable, submission_uncertain).
The retryable and submission_uncertain flags tell callers whether to retry and whether
a retried financial submission could double-spend; see
Confirmation & idempotency.
Related
- Audit — correlate an
X-Request-IDagainst the audit trail. - Confirmation & idempotency — what
submission_uncertainmeans for retries. - Perflo hosted origins — the WebAuthn
Permissions-Policyentries on the web edge.