ADR 0056 — Reasonator service auth: bearer token in vault
Status: Accepted
Date: 2026-05-09 UTC
Refs: #1385, docs/architecture/reasonator/design.md (Decision 3)
Context
Raptor must authenticate to Reasonator on every request. Reasonator is a private service (not public-facing), but it handles scoring requests that are tier-tagged (X-Raxx-Tier). An unauthorized caller that spoofed a pro_plus tier header could consume priority capacity.
Three options were evaluated: Bearer token in Infisical, mTLS, and OAuth2 client credentials.
Decision
Bearer token stored in Infisical at /reasonator/prod/REASONATOR_SERVICE_TOKEN. Raptor reads the token at startup and re-reads on SIGHUP (for rotation without redeploy). Reasonator verifies the token on every inbound request via a middleware check. The token is 32 bytes random, base64url-encoded (256-bit entropy).
Vault folder /reasonator/ must be created in Infisical before the first secret write. The scaffold sub-card is responsible for this step.
Consequences
- Positive: Simple to implement, auditable (every token use is logged with the token truncated to first 8 chars), and rotatable without redeploy.
- Positive: Follows the existing pattern for service-to-service auth in the stack.
- Negative: Single shared token per service pair. Token compromise means full access to Reasonator from any caller with the token. Mitigation: Reasonator is not public-facing; network access is Heroku-internal or via private networking.
- Upgrade path: If SOC 2 or a multi-tenant future requires mutual authentication, mTLS is added as the transport layer without changing the application-level token check. The token becomes a fallback identity check; mTLS provides transport identity.
Alternatives Considered
mTLS: Strong mutual authentication. Requires client certificate provisioning, renewal, and Heroku routing that can verify client certs (Heroku does not terminate mTLS by default — would require an nginx sidecar or SNI passthrough). Significant ops overhead for a private service-to-service call. Deferred to a future security hardening ADR if SOC 2 scope demands it.
OAuth2 client credentials: Standards-based with built-in expiry. Requires an authorization server (another service to run and maintain). Over-engineered for a single service-to-service pair. Rejected.
IP allowlist only: Insufficient — Heroku dynos share IP ranges with other tenants. IP allowlist cannot uniquely identify Raptor as the caller.