Back to Advanced Topics
Q18Question 18 of 41

How do you configure mTLS, a JWT auth interceptor, deadlines, compression, and keep-alive for gRPC in Spring Boot?

Key Concepts
  • mTLS makes both client and server present X.509 certificates, encrypting the channel and proving identity before any application code runs.
  • A JWT auth interceptor reads the Bearer token from gRPC metadata, validates it, and closes the call with UNAUTHENTICATED if invalid.
  • Deadlines are set per call with withDeadlineAfter and propagate across hops so the entire chain respects one time budget.
  • Compression with gzip shrinks large payloads like account statements; enabled per call with withCompression.
  • Keep-alive pings detect dead connections via HTTP/2 PING frames; align client ping interval with server permitKeepAliveTime.
  • Every call in production should have a deadline to prevent stuck downstreams from exhausting threads.

Definition

mTLS proves both identities and encrypts, a JWT interceptor authorizes each call, deadlines bound call time, gzip shrinks payloads, and keep-alive pings detect dead connections.
  • mTLS – both client and server present X.509 certificates; a client without a valid cert is rejected at the TLS handshake.
  • JWT auth interceptor – a ServerInterceptor that reads the Authorization metadata key, validates the token, and rejects with UNAUTHENTICATED.
  • Deadlines – per-call time budgets set with withDeadlineAfter that propagate across hops and fail with DEADLINE_EXCEEDED.
  • Compression – gzip per call with withCompression("gzip") to shrink payloads.
  • Keep-alive – HTTP/2 PING frames on idle connections to detect dead peers; configure keepAliveTime and keepAliveTimeout.

Think of National Bank of India's armored-van route between branches. mTLS is both the driver and the branch checking each other's ID badges. The JWT interceptor is the vault guard checking every visitor's signed pass. The deadline is the rule that a request older than two seconds is abandoned. Compression folds the paperwork into a compact pouch. Keep-alive is the periodic radio check so a dead line is noticed quickly.

Understanding the Concepts

How do you configure mTLS for gRPC in Spring Boot?

With grpc-spring-boot-starter, TLS and mTLS are configured in application.yml. You give the server a certificate chain and key, and to require client certificates you set clientAuth: REQUIRE with a trust collection.

YAML
# application.yml
grpc:
  server:
    port: 9090
    security:
      enabled: true
      certificateChain: classpath:certs/server.crt
      privateKey: classpath:certs/server.key
      clientAuth: REQUIRE
      trustCertCollection: classpath:certs/ca.crt

  client:
    account-service:
      address: static://account-service:9090
      security:
        clientAuthEnabled: true
        certificateChain: classpath:certs/client.crt
        privateKey: classpath:certs/client.key
        trustCertCollection: classpath:certs/ca.crt

A client without a valid certificate is rejected at the TLS handshake, before any application code runs.

How do you write a JWT auth interceptor?

The JWT interceptor is a ServerInterceptor. It reads the Authorization metadata key, strips the Bearer prefix, validates the token, and closes the call with UNAUTHENTICATED if invalid.

JAVA
package com.bank.grpc;

import io.grpc.*;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import java.nio.charset.StandardCharsets;
import javax.crypto.SecretKey;

public class JwtAuthInterceptor implements ServerInterceptor {

    private static final Metadata.Key<String> AUTH_KEY =
        Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER);

    private static final Context.Key<String> USER_CTX =
        Context.key("authenticated-user");

    private final SecretKey signingKey;

    public JwtAuthInterceptor(String secret) {
        this.signingKey = Keys.hmacShaKeyFor(
            secret.getBytes(StandardCharsets.UTF_8));
    }

    @Override
    public <Req, Resp> ServerCall.Listener<Req> interceptCall(
            ServerCall<Req, Resp> call,
            Metadata headers,
            ServerCallHandler<Req, Resp> next) {

        String token = headers.get(AUTH_KEY);

        if (token == null || !token.startsWith("Bearer ")) {
            call.close(Status.UNAUTHENTICATED
                .withDescription("Missing or invalid token"),
                new Metadata());
            return new ServerCall.Listener<>() {};
        }

        try {
            Claims claims = Jwts.parser()
                .verifyWith(signingKey)
                .build()
                .parseSignedClaims(token.substring(7))
                .getPayload();

            String subject = claims.getSubject();
            Context ctx = Context.current().withValue(USER_CTX, subject);
            return Contexts.interceptCall(ctx, call, headers, next);

        } catch (Exception e) {
            call.close(Status.UNAUTHENTICATED
                .withDescription("Token validation failed: " + e.getMessage()),
                new Metadata());
            return new ServerCall.Listener<>() {};
        }
    }
}

How do deadlines work and why do they propagate?

Deadlines are set by the client with withDeadlineAfter. The deadline is an absolute time that travels in the call metadata. If it passes, the call fails with DEADLINE_EXCEEDED. Crucially, the deadline propagates to downstream gRPC calls through the context, so the entire chain respects one time budget.

JAVA
package com.bank.service;

import com.bank.grpc.AccountServiceGrpc;
import com.bank.grpc.BalanceRequest;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import java.util.concurrent.TimeUnit;

public class TransferClient {
    public static void main(String[] args) {
        ManagedChannel channel = ManagedChannelBuilder
            .forAddress("account-service", 9090)
            .usePlaintext()
            .build();

        var stub = AccountServiceGrpc.newBlockingStub(channel)
            .withDeadlineAfter(2, TimeUnit.SECONDS); // 2-second deadline

        var request = BalanceRequest.newBuilder()
            .setAccountId("RAJESH-001").build();

        // If the server takes longer than 2 seconds,
        // the call fails with DEADLINE_EXCEEDED
        var response = stub.getBalance(request);
        System.out.println("Balance: " + response.getBalancePaise());
    }
}

This is different from a client-only timeout because the server also learns the deadline and can stop work early.

How do you enable compression?

Compression is enabled per call with withCompression. For large responses like account statements, gzip can cut payload size dramatically.

JAVA
var stub = AccountServiceGrpc.newBlockingStub(channel)
    .withCompression("gzip")
    .withDeadlineAfter(5, TimeUnit.SECONDS);

// The server advertises decompression support
// Responses are also compressed if the server enables it

How do you configure keep-alive?

Keep-alive sends HTTP/2 PING frames on idle connections. If a ping is not answered within the timeout, the connection is closed and a new one is created.

JAVA
ManagedChannel channel = ManagedChannelBuilder
    .forAddress("account-service", 9090)
    .keepAliveTime(30, TimeUnit.SECONDS)    // Send ping every 30s idle
    .keepAliveTimeout(5, TimeUnit.SECONDS)  // Close if no pong in 5s
    .keepAliveWithoutCalls(true)            // Ping even with no active RPCs
    .build();

On the server side, align permitKeepAliveTime with the client's ping interval. If the server's allowed minimum is higher than the client's interval, the server sends GOAWAY and kills the connection.

JAVA
Server server = ServerBuilder.forPort(9090)
    .addService(new AccountServiceImpl())
    .permitKeepAliveTime(20, TimeUnit.SECONDS) // Allow pings >= 20s
    .keepAliveTime(60, TimeUnit.SECONDS)
    .build();

How do these concerns work together?

| Concern | Where configured | Effect |
|---------|-----------------|--------|
| mTLS | application.yml | Both sides prove identity; channel encrypted |
| JWT auth | ServerInterceptor | Per-request user authorization |
| Deadline | Client stub | Time budget propagates across hops |
| Compression | Client stub | Shrinks payloads |
| Keep-alive | Channel builder + server | Detects dead connections |

What are common mistakes?

The most common mistake is not setting deadlines. Without them, a stuck downstream can exhaust threads and connections.

JAVA
// WRONG: no deadline
var stub = AccountServiceGrpc.newBlockingStub(channel);

// CORRECT: always set a deadline in production
var stub = AccountServiceGrpc.newBlockingStub(channel)
    .withDeadlineAfter(2, TimeUnit.SECONDS);

Another mistake is misaligned keep-alive settings. If the client pings every 10 seconds but the server's permitKeepAliveTime is 30 seconds, the server sends GOAWAY.

What should a senior engineer know?

Every call in production should have a deadline. mTLS should be non-negotiable for service-to-service calls in banking. JWT tokens carry user identity for per-request authorization on top of mTLS channel authentication. Compression matters for large payloads but adds CPU overhead on both sides. Keep-alive alignment between client and server prevents silent connection drops.

Visual Overview

DIAGRAM
  gRPC Hardening in Spring Boot:

  Client                          Server
  ┌──────────────┐                ┌──────────────┐
  │ mTLS cert    │──TLS──────────▶│ mTLS verify  │
  │ JWT token    │──metadata─────▶│ JWT intercept│
  │ deadline(2s) │──propagates───▶│ respects 2s  │
  │ gzip         │──compressed───▶│ decompresses │
  │ keepAlive    │──PING─────────▶│ PONG         │
  └──────────────┘                └──────────────┘

  Timeline:
  1. TLS handshake (mTLS: both certs checked)
  2. JWT validated in ServerInterceptor
  3. Deadline countdown starts
  4. Request/response compressed with gzip
  5. Idle connection kept alive with PING/PONG

Key Takeaway

Hardening gRPC in Spring Boot is five independent concerns configured together. mTLS makes both client and server present X.509 certificates, so identity is proven and the channel encrypted at the TLS handshake, before any application code runs. The JWT interceptor reads the Authorization key from metadata, validates the token, and closes the call with UNAUTHENTICATED if invalid, providing per-request user authorization on top of mTLS channel authentication.

Deadlines are set by the client with withDeadlineAfter, and the key property is that the deadline is absolute and propagates across hops. The entire chain respects one time budget, preventing cascading hangs. Compression with gzip matters for large responses like account statements. Keep-alive pings detect dead connections, but the client's interval must align with the server's permitKeepAliveTime or the server sends GOAWAY.

At National Bank of India, Pankaj's transfer service used all five together. mTLS stopped impersonation, the JWT interceptor authorized each user request, every call carried a two-second deadline, statements were gzipped, and keep-alive kept the connection pool warm. Every call in production should have a deadline, because without one a stuck downstream can exhaust threads.

Remember one sentence: mTLS authenticates the channel, JWT authorizes the user, deadlines propagate a time budget, gzip compresses payloads, and keep-alive detects dead peers, and together they make gRPC production-ready.

---

Share
Use ← → arrow keys to navigate