WhollySoftware
Back to blogBackend

API Keys, OAuth, or mTLS: Authenticating Service-to-Service Calls

Wholly Software TeamSeptember 17, 20257 min read
API Keys, OAuth, or mTLS: Authenticating Service-to-Service Calls

For a client's public-facing API used by third-party integrators, we used API keys, and kept it simple on purpose: a key per integration partner, scoped to specific endpoints and rate limits, hashed at rest (we store only a SHA-256 hash, never the plaintext key after issuance) so a database leak doesn't hand out live credentials. API keys are easy for external partners to understand and rotate, which matters when the consumer isn't a team we control.

For that same client's internal microservices — order service calling inventory service calling pricing service — we didn't want long-lived static credentials sitting in every service's config, since a leaked key there has a much larger blast radius than a leaked partner API key. We used OAuth 2.0 client credentials flow with short-lived tokens (10-minute expiry) issued by an internal auth server, so a compromised token is only useful for a narrow window.

For the highest-sensitivity boundary — the payment service talking to a PCI-scoped vault holding tokenized card data — we went further and used mutual TLS on top of the OAuth token, so both sides verify each other's certificate, not just a bearer token that could be replayed if intercepted. This was the most operationally expensive choice: certificate issuance, rotation before 90-day expiry, and debugging TLS handshake failures are all real ongoing costs, which is exactly why we reserved it for the one boundary where the risk justified it.

The mistake we made initially was applying OAuth client credentials uniformly everywhere, including low-risk internal calls between two services in the same VPC with no external exposure. That added latency (a token fetch and validation round trip) and operational surface (another thing that can fail, another dependency on the auth server being up) without a corresponding security benefit for calls that never leave a private subnet we already trust.

Our current default: API keys for external partners, short-lived OAuth tokens for internal service-to-service calls that cross any meaningful trust boundary, and mTLS reserved for the specific boundaries — payments, PII stores — where certificate-level identity verification earns its operational cost. Matching the mechanism to the actual risk at each boundary, rather than picking one pattern for the whole system, kept both security and complexity proportionate.

AuthenticationOAuthSecurityBackend
API Keys, OAuth, or mTLS: Authenticating Service-to-Service Calls — Wholly Software