Back to guides

Service-to-Service Authentication Best Practices

By Davy Rogers

When services talk to each other, they need to prove identity. Below: the three main approaches, when to use each, and how to avoid the usual mistakes.

The Three Options

ApproachIdentity ProofWorks Across NetworksComplexity
mTLSX.509 certificatesYesHigh (cert management)
JWT (signed)Signed token with claimsYesMedium
OAuth 2.0 Client CredentialsAccess token from auth serverYesMedium-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:

  1. Each service has a certificate signed by an internal Certificate Authority (CA).
  2. On connection, both sides present their cert.
  3. 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:

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 aud to ensure the token was meant for this service.
  • Validate iss to 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:

Comparison and Decision Framework

FactormTLSJWTOAuth Client Credentials
Setup complexityHighLow-MediumMedium
Certificate/key managementRequired (X.509)Required (signing keys)Managed by auth server
Claim-based access controlNo (identity only)YesYes
Revocation speedPotentially slow (CRL/OCSP often delayed)Delayed (until expiry)Fast (via auth server)
Works without auth serverYesYesNo
Service mesh compatibleNativeExtra stepExtra 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 aud on 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

Published 04 Mar 2026

Frequently asked questions

Whats the simplest credible service-to-service pattern?
Short-lived bearer tokens issued by your identity provider via the client_credentials grant, validated at the receiver. Rotate the underlying secret using your secrets manager.
Where does mutual TLS fit?
Inside a trust domain you control - typically a service mesh (Istio, Linkerd, Consul). It gives strong transport identity but does not by itself convey caller-specific claims.
Can I use long-lived API keys between services?
You can; you should not. Long-lived shared secrets are the most-leaked credential type in production. Move to short-lived tokens or workload identity (OIDC, SPIFFE) wherever possible.
How do I authorise actions between services?
Carry the original user identity (an on-behalf-of claim) and the calling services identity in the token, then authorise on both. Authentication answers who; authorisation answers what they can do.

Related

Want a professional to look at it?Get an AppSec Health Check.