Service-to-Service Authentication Best Practices
When services talk to each other, they need to prove identity. This guide covers the three main approaches, when to use each, and how to avoid common mistakes.
The Three Options
| Approach | Identity Proof | Works Across Networks | Complexity |
|---|---|---|---|
| mTLS | X.509 certificates | Yes | High (cert management) |
| JWT (signed) | Signed token with claims | Yes | Medium |
| OAuth 2.0 Client Credentials | Access token from auth server | Yes | Medium-High |
Mutual TLS (mTLS)
Both client and server present certificates and verify each other. The TLS handshake establishes identity before any application data flows.
When to use:
- Zero-trust networks where every connection must be verified.
- Service meshes (Istio, Linkerd handle mTLS automatically).
- High-security environments (financial, healthcare).
How it works:
- Each service has a certificate signed by an internal Certificate Authority (CA).
- On connection, both sides present their cert.
- Both sides verify the cert against the CA.
Kubernetes + Istio example:
Istio automatically provisions and rotates mTLS certificates for all services in the mesh:
# PeerAuthentication — enforce mTLS
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default
namespace: production
spec:
mtls:
mode: STRICT
Reference: Istio — Mutual TLS
Without a service mesh:
Use your platform's certificate management:
- AWS: ACM Private CA: AWS — Private CA
- GCP: Certificate Authority Service: GCP — CA Service
- Azure: Key Vault certificates: Azure — Key Vault Certificates
Certificate rotation: automate this. Manual rotation breaks. Use short-lived certs (hours to days) whenever possible.
JWT-Based Service Auth
Services authenticate by presenting a signed JWT. The receiving service validates the signature and claims.
When to use:
- Services already have a JWT infrastructure.
- You need to pass claims (roles, scopes, tenant ID) between services.
- Simpler setup than mTLS in environments without a service mesh.
Implementation:
import jwt from "jsonwebtoken";
// Service A — create a service token
const serviceToken = jwt.sign(
{
sub: "order-service",
aud: "inventory-service",
scope: "inventory:read",
},
PRIVATE_KEY,
{ algorithm: "RS256", expiresIn: "5m" }
);
// Service B — verify the token
const payload = jwt.verify(serviceToken, PUBLIC_KEY, {
algorithms: ["RS256"],
audience: "inventory-service",
});
Key considerations:
- Use asymmetric keys (RS256 or ES256) so services only need the public key to verify.
- Set short expiry (5 minutes or less).
- Validate
audto ensure the token was meant for this service. - Validate
issto ensure it came from a trusted service.
OAuth 2.0 Client Credentials
Each service is an OAuth client. Services obtain an access token from an authorization server using their client credentials, then present that token to other services.
When to use:
- You already have an OAuth infrastructure (Auth0, Keycloak, Entra ID).
- You want centralized token issuance and revocation.
- You need fine-grained scopes managed centrally.
Flow:
Service A → Auth Server: POST /oauth/token
grant_type=client_credentials
client_id=order-service
client_secret=...
scope=inventory:read
Auth Server → Service A: { access_token: "...", expires_in: 300 }
Service A → Service B: GET /api/stock
Authorization: Bearer <access_token>
Service B: validates token (via introspection or local JWKS verification)
Provider examples:
- Auth0: Machine-to-Machine Applications
- Keycloak: Service Accounts
- Microsoft Entra ID: Client Credentials Flow
Comparison and Decision Framework
| Factor | mTLS | JWT | OAuth Client Credentials |
|---|---|---|---|
| Setup complexity | High | Low-Medium | Medium |
| Certificate/key management | Required (X.509) | Required (signing keys) | Managed by auth server |
| Claim-based access control | No (identity only) | Yes | Yes |
| Revocation speed | Potentially slow (CRL/OCSP often delayed) | Delayed (until expiry) | Fast (via auth server) |
| Works without auth server | Yes | Yes | No |
| Service mesh compatible | Native | Extra step | Extra step |
Choose mTLS when:
- You run a service mesh or zero-trust network.
- You need transport-layer identity, not just application-layer.
Choose JWT when:
- You want lightweight, self-contained tokens with claims.
- Services are in a trusted network and you do not need mTLS overhead.
Choose OAuth Client Credentials when:
- You want a centralized authority for token issuance and revocation.
- You are already using an identity provider.
Anti-Patterns
- Shared static API key between services. No identity, no rotation, no scoping.
- IP-based trust only. Breaks in dynamic environments (containers, auto-scaling).
- No expiry on service tokens. A leaked token is valid forever.
- Not validating
audon JWTs. A token meant for Service A works on Service B. - mTLS with a single CA and no service identity checks. Any service with a cert can talk to any other service.
Related Guides
Content is AI-assisted and reviewed by our team, but issues may be missed and best practices evolve rapidly, send corrections to [email protected]. Always consult official documentation and validate key implementation decisions before making design or security choices.
