Inkress ADRsarchitecture decisions
Overview / Identity / auth
ADR 0028

Service-to-service authentication uses three schemes, chosen per trust boundary

AcceptedIdentity / auth

Context

commerce-api sits between commerce-risk (the risk/KYC decision service, ADR 0024) and commerce-payments/the CDE (the money control plane, ADR 0006/0009), and commerce-risk also calls back into commerce-api. Each of these boundaries has a different threat model — a read-only verdict lookup, a money-moving request, and a low-sensitivity inbound lookup — so a single uniform scheme would be either over- or under-fit somewhere.

Decision

Use three different service-to-service authentication schemes, one per boundary, each matched to that boundary's integrity, replay, and latency needs. No mTLS or service mesh.

  • Outbound → commerce-risk: a short-lived HS256 service JWT. Minted per call with iss=commerce-api, aud=commerce-risk, a scope claim, and exp = now + 60s; verified by risk's service-auth guard. Audience-binding + auto-expiry suit a read-only verdict lookup.
  • Outbound → commerce-payments / CDE: HMAC-SHA256 request signing. A canonical timestamp.nonce.METHOD.path.sha256hex(body) string (dot-separated, body hashed) is signed and sent as x-key-id / x-timestamp / x-nonce / x-signature. Per-request body integrity + nonce replay-resistance is the right bar for money movement.
  • Inbound ← commerce-risk (disputes): a static shared bearer. A DISPUTE_LOOKUP_TOKEN, constant-time compared, on the otherwise-unauthenticated internal pipeline. This began as a low-sensitivity txn lookup, but the same bearer also guards money-moving dispute endpointssettle (enqueues the chargeback debit) and hold/release (change available balance) — which is a weaker mechanism than those actions warrant (see to-revisit).

Consequences

  • Positive: the JWT and HMAC boundaries fit their threat models — expiring audience-bound tokens for the verdict read, signed bodies with nonces for money moves.
  • Neutral / to revisit — the static dispute bearer is under-scoped for what it now guards. It began as a lookup credential but also authenticates settle/hold/release, which move money — a plain shared bearer with no per-request integrity or expiry is weaker than those actions deserve. Upgrading that boundary (HMAC or a scoped short-lived token) is a follow-up.
  • Neutral / to revisit: three secret types to provision and rotate, and no single revocation story; trust rests on shared secrets in each service's environment, so a leaked secret is a boundary compromise until rotated.
  • Neutral / to revisit: there is no mTLS/service-mesh identity layer, so the schemes are application-level; even the "internal" inbound route is explicitly authenticated rather than trusting the private network.

Alternatives considered

  • One uniform scheme everywhere (a single shared JWT or bearer): rejected — it would be over-fit for the cheap inbound lookup and under-fit for money movement (no per-request body integrity).
  • mTLS / a service mesh: rejected — infrastructure weight not justified for a handful of service-to-service endpoints; application-level schemes cover the needs today.
  • Trust the private network with no per-request auth: rejected — even the internal inbound route is authenticated, so a foothold in the network is not a free pass to the money boundary.
← 0027 A first-party OAuth 2.1 + PKCE provider for third-party developer apps 0029 The double-entry ledger is an async, rebuildable projection of the capture-path ledger →