Safeguard
Application Security

Microservices security: authentication between services

Service-to-service auth stops lateral movement between microservices. Learn how mTLS, OAuth2, and SPIFFE/SPIRE secure internal calls in production.

James
Principal Security Architect
7 min read

In a modern microservices architecture, a single user request can trigger calls across 20, 50, or 200 internal services before a response reaches the browser. Each of those calls crosses a trust boundary, and if a service accepts requests without verifying who actually sent them, an attacker who compromises one low-value service can pivot into services holding customer data, payment credentials, or infrastructure secrets. That pivot pattern is exactly what played out in Capital One's 2019 breach, where a misconfigured web application firewall role with excessive internal permissions let an attacker turn a single SSRF flaw into access to 106 million customer records. Service-to-service authentication -- proving that Service A is really Service A before Service B acts on its request -- is the control that stops that kind of lateral movement. This post breaks down how service-to-service authentication actually works in practice, where teams get it wrong, and how to roll it out across a large fleet of services without breaking your deployment pipeline.

What is service-to-service authentication, and how is it different from user authentication?

Service-to-service authentication is the process by which one workload proves its identity to another workload, independent of any human user in the request path. User authentication answers "which person is making this request," typically through a session cookie or an OAuth access token tied to a human identity, and it usually happens once at the edge, in an API gateway or a login flow. Service-to-service authentication answers a different question -- "which service is making this internal call" -- and it has to happen on every hop inside the system, not just at the perimeter. A checkout service calling an inventory service, a billing service calling a tax-calculation service, and a background worker calling an internal notifications API all need their own machine identity, separate from whatever user originally triggered the chain. NIST SP 800-204, published in 2019 specifically to address microservices security, recommends that every service-to-service call be authenticated and authorized on its own merits, because a request that already passed through one authenticated hop cannot be assumed safe at the next one.

Which protocols actually secure service-to-service calls in production?

Three approaches dominate production microservices deployments: mutual TLS (mTLS), OAuth 2.0 client credentials grant, and workload identity frameworks like SPIFFE/SPIRE. mTLS has both sides of a connection present X.509 certificates, so the TLS handshake itself proves identity before a single byte of application data moves; Istio has shipped mTLS support since 2018 and made STRICT mode -- rejecting any plaintext traffic between mesh sidecars -- the recommended default starting with Istio 1.5 in 2020. OAuth 2.0's client credentials grant, defined in RFC 6749 section 4.4, is the machine-to-machine equivalent of a login flow: a service authenticates to a token issuer with a client ID and secret (or a signed JWT assertion under RFC 7523), gets back a short-lived access token, and presents that token on each call. SPIFFE/SPIRE, which graduated as a CNCF project in November 2022, goes a step further by issuing cryptographically verifiable workload identities (SVIDs) automatically at pod or process startup, removing the need to provision and rotate static credentials by hand. Most mature platforms combine mTLS for transport-level identity with token-based authorization for fine-grained, per-call permissions.

What actually goes wrong when service authentication is implemented poorly?

The most common failure is trusting network location instead of cryptographic identity, and it has caused some of the largest breaches on record. In Uber's 2016 breach, attackers found AWS credentials hardcoded in a private GitHub repository used by one of Uber's internal services; those credentials granted broad access to backend data stores and exposed information on 57 million riders and drivers. The root problem was not the specific credential leak, it was that a static, long-lived secret -- rather than a short-lived, automatically rotated workload identity -- was the only thing standing between "any process with this string" and full data access. Kubernetes has had its own version of this failure: CVE-2018-1002105, a critical (CVSS 9.8) flaw disclosed in December 2018, let an unauthenticated attacker send crafted requests through the API server's aggregation layer and have them executed with the API server's own trusted identity against backend services, effectively forging service-to-service authentication at the cluster's most privileged layer. In both cases, a compromise or gap at one point in the trust chain was treated as a strong enough signal for every downstream service to grant access, which is exactly the assumption zero-trust service authentication is designed to remove.

Do internal, "trusted" networks still need authentication between services?

Yes -- an internal VPC or Kubernetes cluster network provides no cryptographic guarantee about which workload originated a request. Network-level trust (IP allowlists, VPC peering, "it's inside the firewall") predates the threat model microservices actually face: any compromised container, misconfigured sidecar, or over-permissioned service account sitting on that same network can originate traffic that looks legitimate to a receiving service that only checks source IP or network membership. NIST's zero trust architecture guidance, SP 800-207 (2020), states that no implicit trust should be granted based on network location, and that authentication and authorization should be discrete, enforced actions for every session. In practice this means a payments service should verify the cryptographic identity of a calling order service on every request, even if both run in the same private subnet and have never seen unauthorized traffic before -- because the first sign of a breach is usually traffic that looks exactly like normal internal traffic.

How do you roll out mTLS or workload identity across hundreds of services without breaking production?

Roll it out incrementally, in permissive mode, service by service, starting with non-critical traffic. Service meshes like Istio and Linkerd both support a "permissive" mTLS mode that accepts both encrypted and plaintext traffic simultaneously, which lets teams enable mTLS on a namespace without an instant outage if a handful of legacy clients haven't been upgraded yet; only after telemetry confirms all observed traffic to a service is already using mTLS should that namespace move to STRICT enforcement. Certificate and identity lifetimes matter here too -- SPIFFE SVIDs are commonly issued with lifetimes measured in hours, which limits the blast radius of a leaked credential far more than the multi-year TLS certificates many teams still use for east-west traffic. Teams should also inventory which services currently authenticate each other with static API keys or shared secrets before starting migration, since those are the integrations most likely to silently continue accepting unauthenticated calls after a mesh rollout if they bypass the sidecar proxy entirely -- for example, direct database connections or calls to a legacy service not yet mesh-enrolled.

How Safeguard Helps

Safeguard helps security teams find and close the exact gaps that undermine service-to-service authentication before they reach production. Griffin AI, Safeguard's reasoning engine, traces real call paths through your codebase and infrastructure-as-code to flag services that accept internal traffic without verifying caller identity, and reachability analysis confirms whether a vulnerable dependency or misconfigured auth check sits on a path actually exercised by production traffic -- so teams fix the flaws attackers can reach first, not the largest pile of findings. Safeguard's SBOM generation and ingest give you a live inventory of every service, library, and identity-handling component across your mesh, making it easier to spot services still relying on static credentials or unmeshed legacy connections. When Griffin AI confirms a fix, Safeguard opens an auto-fix pull request with the corrected authentication configuration or dependency bump already scoped to the affected service, so remediation ships as fast as the finding was identified.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.