Skip to content
Ashish.
All posts
Diagram illustrating the layered security model of OAuth2, JWT, and mTLS in a microservices architecture.

Microservices Security Architecture: OAuth2, JWT, and mTLS Patterns

Examines microservices security patterns including OAuth2, JWT propagation, and mTLS within service mesh architectures.

By Ashish SrivastavaPart 5 of Zero Trust & Modern Security Architecture Series

Microservices Security Architecture: OAuth2, JWT, and mTLS Patterns

In monolithic architectures, the perimeter was a network firewall; if you were inside the corporate network, you were trusted. In microservices, that perimeter dissolves. Every service instance can spin up anywhere, and the network is untrusted by default. The new security model is not a wall but a chain of trust links, where each link requires a specific mechanism to verify identity and secure the channel. This article examines the layered model where OAuth2 handles user identity at the perimeter, JWTs carry claims across services, and mTLS enforces zero-trust transport encryption between services. Confusing these distinct flows is the primary source of architectural security failures.

The Trust Boundary Shift

The first mechanism to understand is the separation of concerns between authentication (who are you?) and authorization (what can you do?), and where these checks happen. In a traditional setup, an API Gateway acts as the gatekeeper. It terminates the user's connection, validates credentials, and then forwards requests. However, simply forwarding the request isn't enough. The downstream services need to know who made the request originally, not just that someone authenticated.

Consider a user, Alice, accessing an "Orders" service. Alice logs in via a web browser. The browser sends a credential to the API Gateway. The Gateway validates this against an Identity Provider (IdP). This is the OAuth2 flow. Once validated, the Gateway issues a JSON Web Token (JWT). Alice includes this JWT in her request header. The "Orders" service receives the request. It does not know Alice's password. It only sees the token.

The critical shift in microservices is that the "Orders" service cannot rely on the network to know Alice is Alice. It must parse the token. But what about the traffic between the "Orders" service and a "Payment" service? Here, Alice is no longer involved. The "Orders" service is acting on behalf of Alice. The "Payment" service needs to trust that the "Orders" service is actually the "Orders" service, not an attacker spoofing the IP address. This is where mTLS enters the picture.

OAuth2 and JWT Propagation

OAuth2 is an authorization framework, but in modern architectures, we almost exclusively use OpenID Connect (OIDC) on top of it to handle authentication. The mechanism here is the issuance of a signed JWT. The Identity Provider (IdP) signs the token with a private key. The downstream services hold the corresponding public key (or a JWK set) to verify the signature.

When Alice makes a request, the Authorization header looks like this:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

The "Orders" service receives this. It performs a signature verification. If the signature is valid, the token is trusted. The service extracts the sub (subject) claim, which identifies Alice.

Now, the "Orders" service needs to call the "Payment" service to charge Alice. It must propagate Alice's identity. The mechanism is simple but often implemented incorrectly: pass the original JWT downstream.

If the "Orders" service generates a new token for the "Payment" service, it loses the context of the original user unless it explicitly maps claims. The most robust pattern is "pass-through." The "Orders" service takes the incoming Authorization header and injects it into the outbound request to "Payment."

# Pseudocode for service-to-service propagation
response = gateway.call("payment-service", {
  headers: {
    "Authorization": request.headers["Authorization"], // Pass original token
    "x-forwarded-for": request.headers["X-Forwarded-For"]
  }
})

Why is this important? Because the "Payment" service needs to enforce policies based on Alice's identity. If Alice is a "Premium" user, she gets a discount. The "Payment" service reads the sub and role claims from the original JWT. If the "Orders" service had issued a generic "service-token" without the user's specific claims, the "Payment" service would have to make a separate network call to the IdP to resolve the user, creating latency and a new attack surface.

However, there is a risk. If the "Orders" service is compromised, it can forward Alice's token to a malicious service. This is why we need mTLS.

mTLS in Service Meshes

While JWTs verify identity, they do not encrypt the traffic. An attacker on the network can intercept the JWT if the connection is not encrypted. Even if TLS 1.3 is used for user traffic, the traffic between internal microservices often lacks this protection in legacy setups. Mutual TLS (mTLS) solves this by requiring both the client and the server to present a certificate.

In a service mesh like Istio, this is handled transparently. A sidecar proxy (Envoy) sits next to every service pod. When the "Orders" service wants to talk to "Payment," it sends traffic to its local Envoy proxy. The Envoy proxy initiates a TLS handshake with the "Payment" service's Envoy proxy.

The mechanism involves a Certificate Authority (CA) managed by the control plane.

  1. Control Plane: The mesh control plane (e.g., Istiod) issues short-lived certificates to the "Orders" sidecar and the "Payment" sidecar. These certificates contain the service's identity (e.g., service=orders, namespace=production). While a CA compromise is critical, modern service meshes mitigate this via automated rotation protocols like SPIFFE/SPIRE and short TTLs, significantly reducing the exposure window compared to long-lived certificates.
  2. Handshake: When "Orders" connects to "Payment," the "Payment" sidecar (Server) sends its certificate first. The "Orders" sidecar (Client) verifies the signature against the mesh CA. Then, the "Orders" sidecar presents its certificate. The "Payment" sidecar verifies it.

This ensures that even if an attacker spoofs the IP address of the "Orders" service, they cannot complete the handshake without the private key corresponding to the mesh CA.

# Example: Envoy configuration for mTLS
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: mesh-default
  namespace: default
spec:
  mtls:
    mode: STRICT # Enforces mTLS for all traffic

With mTLS active, the transport layer is secure. The "Payment" service knows the request came from a legitimate "Orders" pod. But mTLS does not tell the "Payment" service who Alice is. It only tells it that the "Orders" service is talking to it. This is the distinction: mTLS authenticates the machine, JWT authenticates the user.

The Composition Pattern

The most common architectural error is treating mTLS as a replacement for JWT validation. Some teams assume that because traffic is mutually authenticated, they can skip checking the JWT. This is a fatal flaw.

Imagine the "Orders" service is compromised. The attacker gains access to the pod. The attacker's code can now send requests to the "Payment" service. The "Payment" service's sidecar sees a valid certificate from the "Orders" service. It accepts the traffic because the machine identity is valid. If the "Payment" service relies solely on mTLS, it has no way to know that this request is not coming from the legitimate "Orders" application logic, but from a malicious script running inside the pod.

The correct pattern is defense in depth.

  1. mTLS ensures the request comes from a trusted service within the mesh (confidentiality and integrity).
  2. JWT ensures the request is authorized for the specific user (authorization).

The "Payment" service must perform both checks. It verifies the mTLS handshake first (enforced by the sidecar). Then, it parses the JWT in the Authorization header. It verifies the signature using the IdP's public key. It checks the claims.

If the JWT is missing, the "Payment" service rejects the request, even if the mTLS handshake succeeded. This prevents an attacker inside the "Orders" pod from making unauthorized actions on behalf of users they shouldn't access.

Conversely, if the JWT is valid but the mTLS handshake fails (e.g., the request comes from outside the mesh), the sidecar drops the connection before the application logic ever sees it.

Operational Realities and Tradeoffs

Implementing this stack introduces complexity. Managing the lifecycle of certificates in mTLS requires a robust PKI. If the CA private key is leaked, the entire mesh is compromised. This is why modern meshes use short-lived certificates (TTL of hours or minutes) and automate rotation.

Regarding JWTs, the tradeoff is between stateless validation and revocation. Since JWTs are self-contained, the "Payment" service does not need to query the IdP for every request, which is efficient. However, true stateless revocation is impossible. To mitigate theft, the solution involves a hybrid approach: maintaining a lightweight deny-list cache or caching JWK sets with short TTLs. This acknowledges the inherent trade-off between the performance of stateless validation and the security requirement of immediate revocation.

Another consideration is the "chaining" of tokens. In complex workflows, a service might need to call another service, which calls a third. The original JWT must be propagated through the entire chain. If any service in the middle strips the Authorization header and replaces it with a generic service token, the downstream services lose the audit trail of the original user.

The pattern is not just about tools; it is about the flow of data. The identity of the user (Alice) must travel with the request, encrypted by the transport (mTLS), and signed by the issuer (JWT). Any break in this chain creates a blind spot where an attacker can operate.

Conclusion

In summary, a solid microservices security architecture treats the network as hostile. It uses mTLS to ensure that only authorized services can talk to each other, and it uses OAuth2/JWT to ensure that only authorized users can trigger actions within those services. The two mechanisms are complementary, not interchangeable. The "Orders" service is trusted by the "Payment" service because of mTLS, but the "Payment" service only processes the transaction because the JWT proves Alice has permission. By correctly composing these patterns, organizations can achieve a true zero-trust posture where trust is never assumed based on network location alone.

FAQ

Can I use mTLS instead of JWT? No. mTLS authenticates the machine (the service pod), ensuring the request comes from a valid service. It does not identify the human user. You still need JWTs to authorize the specific user's actions (e.g., "Alice is allowed to delete this order").

How do I revoke a stolen JWT? Since JWTs are stateless, they cannot be revoked immediately on the server side without external help. The standard mitigation is to use short expiration times (short TTL) combined with a lightweight deny-list cache. If a token is reported stolen, it is added to the deny-list, which the service checks periodically before accepting the token.

What happens if the CA key is compromised? If the Certificate Authority private key is leaked, an attacker could issue fake certificates for any service. Modern service meshes mitigate this risk by using short-lived certificates (minutes or hours) and automated rotation (e.g., SPIFFE/SPIRE). This limits the window of opportunity for an attacker to misuse a compromised key compared to long-lived certificates.

Common Pitfalls

  1. Relying solely on mTLS for user authorization: Assuming that because a request comes from a trusted service (verified by mTLS), it is automatically authorized to perform user-specific actions. This ignores the identity of the end-user.
  2. Stripping JWT headers in intermediate services: When an intermediate service generates a new token or strips the Authorization header, the downstream services lose the context of the original user, breaking the audit trail and preventing fine-grained authorization.
  3. Using long-lived certificates without rotation: Deploying certificates with long expiration dates creates a massive exposure window if the private key is ever compromised, whereas short-lived certificates rotated automatically limit the damage.

Practical Takeaways

  • Implement Defense in Depth: Never rely on a single mechanism. Use mTLS to secure the transport and authenticate the service, and use JWTs to authenticate the user and enforce authorization policies.
  • Adopt Short-Lived Certificates: Configure your service mesh to issue certificates with short TTLs (e.g., minutes or hours) and automate rotation to minimize the risk associated with key compromise.
  • Propagate JWTs Correctly: Ensure that the original JWT is passed through the entire service chain without modification or replacement, preserving the user's identity and claims for final authorization checks.

Related posts