mTLS Enforcement in Service Mesh with Java: Comprehensive Implementation Guide

mTLS (mutual TLS) provides two-way authentication where both client and server verify each other's certificates. In service mesh architectures, mTLS is enforced automatically between sidecar proxies to secure service-to-service communication.


Core Concepts

What is mTLS in Service Mesh?

  • Automatic certificate issuance and rotation between services
  • Identity-based authentication using X.509 certificates
  • Encryption of all service-to-service traffic
  • Zero-trust security model

Key Benefits:

  • Automatic Certificate Management: No manual certificate configuration needed
  • Service Identity: Each service gets a unique cryptographic identity
  • Traffic Encryption: All inter-service communication is encrypted
  • Policy Enforcement: Fine-grained access control based on service identity

Architecture Overview

Java App → Envoy Sidecar → mTLS → Envoy Sidecar → Java App
↓        (client)          (server)       ↓
Service A                              Service B
↓                                     ↓
Certificate                          Certificate
issued by                            issued by
Mesh CA                             Mesh CA

Dependencies and Setup

Maven Dependencies
<properties>
<spring-boot.version>3.1.0</spring-boot.version>
<istio-client.version>1.16.0</istio-client.version>
<spring-cloud.version>2022.0.3</spring-cloud.version>
</properties>
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- Spring Cloud Kubernetes -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes-fabric8</artifactId>
<version>${spring-cloud.version}</version>
</dependency>
<!-- Istio Java API -->
<dependency>
<groupId>io.istio</groupId>
<artifactId>istio-client</artifactId>
<version>${istio-client.version}</version>
</dependency>
<!-- Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- Resilience -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
<version>${spring-cloud.version}</version>
</dependency>
<!-- Monitoring -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
</dependencies>
Docker Compose for Local mTLS Testing
# docker-compose.mtls.yml
version: '3.8'
services:
# Consul with mTLS enabled
consul-server:
image: consul:1.15
container_name: consul-server
ports:
- "8500:8500"
- "8600:8600/udp"
command: >
agent -server -ui -node=consul-server-1
-bootstrap-expect=1 -client=0.0.0.0
-config-file=/consul/config/mtls.hcl
volumes:
- ./consul-config/mtls.hcl:/consul/config/mtls.hcl
environment:
- CONSUL_BIND_INTERFACE=eth0
# Istio for advanced mTLS features
istio-pilot:
image: istio/pilot:1.18.0
container_name: istio-pilot
ports:
- "15010:15010"
- "15017:15017"
command: [
"discovery",
"--monitoringAddr=:15014",
"--log_output_level=default:info",
"--domain", "cluster.local",
"--keepaliveMaxServerConnectionAge", "30m"
]
# mTLS-enabled services
frontend-service:
build: ./frontend-service
container_name: frontend-service
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=mtls,kubernetes
- MTLS_ENABLED=true
labels:
- "service-mesh=enabled"
- "mtls-strict=true"
backend-service:
build: ./backend-service
container_name: backend-service
ports:
- "8081:8081"
environment:
- SPRING_PROFILES_ACTIVE=mtls,kubernetes
- MTLS_ENABLED=true
labels:
- "service-mesh=enabled"
- "mtls-strict=true"
database-service:
build: ./database-service
container_name: database-service
ports:
- "8082:8082"
environment:
- SPRING_PROFILES_ACTIVE=mtls,kubernetes
- MTLS_ENABLED=true
labels:
- "service-mesh=enabled"
- "mtls-strict=true"

Spring Boot Configuration for mTLS

1. Application Properties
# application-mtls.yml
spring:
application:
name: frontend-service
cloud:
kubernetes:
enabled: true
reload:
enabled: true
config:
sources:
- name: ${spring.application.name}
namespace: default
discovery:
all-namespaces: false
# mTLS Configuration
app:
mtls:
enabled: true
mode: STRICT  # STRICT, PERMISSIVE, DISABLED
ca-cert-path: /var/run/secrets/istio/root-cert.pem
cert-chain-path: /var/run/secrets/istio/cert-chain.pem
key-path: /var/run/secrets/istio/key.pem
verify-hostname: true
allowed-services:
- backend-service
- database-service
# Security
server:
ssl:
enabled: true
client-auth: need
key-store: /var/run/secrets/istio/keystore.p12
key-store-password: changeit
key-store-type: PKCS12
trust-store: /var/run/secrets/istio/truststore.p12
trust-store-password: changeit
trust-store-type: PKCS12
port: 8443
management:
endpoints:
web:
exposure:
include: health,info,metrics,mtls
endpoint:
mtls:
enabled: true
server:
port: 8444
ssl:
enabled: true
2. mTLS Configuration Class
@Configuration
@ConfigurationProperties(prefix = "app.mtls")
@Validated
public class MtlsConfig {
private boolean enabled = false;
private MtlsMode mode = MtlsMode.PERMISSIVE;
private String caCertPath;
private String certChainPath;
private String keyPath;
private boolean verifyHostname = true;
private Set<String> allowedServices = new HashSet<>();
public enum MtlsMode {
STRICT, PERMISSIVE, DISABLED
}
// Getters and setters
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public MtlsMode getMode() { return mode; }
public void setMode(MtlsMode mode) { this.mode = mode; }
public String getCaCertPath() { return caCertPath; }
public void setCaCertPath(String caCertPath) { this.caCertPath = caCertPath; }
public String getCertChainPath() { return certChainPath; }
public void setCertChainPath(String certChainPath) { this.certChainPath = certChainPath; }
public String getKeyPath() { return keyPath; }
public void setKeyPath(String keyPath) { this.keyPath = keyPath; }
public boolean isVerifyHostname() { return verifyHostname; }
public void setVerifyHostname(boolean verifyHostname) { this.verifyHostname = verifyHostname; }
public Set<String> getAllowedServices() { return allowedServices; }
public void setAllowedServices(Set<String> allowedServices) { this.allowedServices = allowedServices; }
}
3. mTLS Certificate Manager
@Component
@Slf4j
public class MtlsCertificateManager {
private final MtlsConfig mtlsConfig;
private SSLContext sslContext;
private X509Certificate clientCertificate;
public MtlsCertificateManager(MtlsConfig mtlsConfig) {
this.mtlsConfig = mtlsConfig;
if (mtlsConfig.isEnabled()) {
initializeCertificates();
}
}
@PostConstruct
public void initializeCertificates() {
if (!mtlsConfig.isEnabled()) {
log.info("mTLS is disabled");
return;
}
try {
// Load certificates from mesh-injected paths
CertificateFactory cf = CertificateFactory.getInstance("X.509");
// Load CA certificate
X509Certificate caCert = loadCertificate(mtlsConfig.getCaCertPath());
// Load client certificate chain
X509Certificate clientCert = loadCertificate(mtlsConfig.getCertChainPath());
// Load private key
PrivateKey privateKey = loadPrivateKey(mtlsConfig.getKeyPath());
// Create KeyStore
KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(null, null);
keyStore.setKeyEntry("client", privateKey, "changeit".toCharArray(), 
new Certificate[]{clientCert});
// Create TrustStore
KeyStore trustStore = KeyStore.getInstance("PKCS12");
trustStore.load(null, null);
trustStore.setCertificateEntry("ca", caCert);
// Initialize SSLContext
SSLContextBuilder sslContextBuilder = SSLContexts.custom()
.loadKeyMaterial(keyStore, "changeit".toCharArray())
.loadTrustMaterial(trustStore, null);
if (mtlsConfig.isVerifyHostname()) {
sslContextBuilder = sslContextBuilder.setKeyStoreType("PKCS12");
}
this.sslContext = sslContextBuilder.build();
this.clientCertificate = clientCert;
log.info("mTLS certificates initialized successfully for mode: {}", mtlsConfig.getMode());
logCertificateInfo(clientCert);
} catch (Exception e) {
log.error("Failed to initialize mTLS certificates", e);
throw new IllegalStateException("mTLS initialization failed", e);
}
}
private X509Certificate loadCertificate(String path) throws Exception {
try (InputStream is = Files.newInputStream(Paths.get(path))) {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
return (X509Certificate) cf.generateCertificate(is);
}
}
private PrivateKey loadPrivateKey(String path) throws Exception {
// This is simplified - in practice, keys might be in different formats
byte[] keyBytes = Files.readAllBytes(Paths.get(path));
String key = new String(keyBytes);
if (key.contains("PRIVATE KEY")) {
// PEM format
return loadPemPrivateKey(key);
} else {
// Assume DER format
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePrivate(spec);
}
}
private PrivateKey loadPemPrivateKey(String pem) throws Exception {
String privateKeyPEM = pem
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s", "");
byte[] encoded = Base64.getDecoder().decode(privateKeyPEM);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encoded);
return keyFactory.generatePrivate(keySpec);
}
private void logCertificateInfo(X509Certificate cert) {
log.info("Client Certificate Details:");
log.info("  Subject: {}", cert.getSubjectX500Principal());
log.info("  Issuer: {}", cert.getIssuerX500Principal());
log.info("  Valid From: {}", cert.getNotBefore());
log.info("  Valid Until: {}", cert.getNotAfter());
log.info("  Serial Number: {}", cert.getSerialNumber());
}
public SSLContext getSslContext() {
return sslContext;
}
public X509Certificate getClientCertificate() {
return clientCertificate;
}
public String getServiceIdentity() {
if (clientCertificate == null) {
return "unknown";
}
String subject = clientCertificate.getSubjectX500Principal().getName();
// Extract service name from certificate subject
// Format: CN=frontend-service.default.svc.cluster.local
Pattern pattern = Pattern.compile("CN=([^.]+)");
Matcher matcher = pattern.matcher(subject);
if (matcher.find()) {
return matcher.group(1);
}
return subject;
}
public boolean isCertificateValid() {
if (clientCertificate == null) {
return false;
}
try {
clientCertificate.checkValidity();
return true;
} catch (CertificateExpiredException | CertificateNotYetValidException e) {
log.warn("Certificate is not valid: {}", e.getMessage());
return false;
}
}
}
4. mTLS-Aware RestTemplate
@Configuration
public class MtlsRestTemplateConfig {
@Bean
@ConditionalOnProperty(name = "app.mtls.enabled", havingValue = "true")
public RestTemplate mtlsRestTemplate(MtlsCertificateManager certificateManager,
MtlsConfig mtlsConfig) {
try {
SSLContext sslContext = certificateManager.getSslContext();
SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(
sslContext,
new String[]{"TLSv1.2", "TLSv1.3"},
null,
mtlsConfig.isVerifyHostname() ? 
SSLConnectionSocketFactory.getDefaultHostnameVerifier() : 
NoopHostnameVerifier.INSTANCE
);
HttpClientConnectionManager connectionManager = 
PoolingHttpClientConnectionManagerBuilder.create()
.setSSLSocketFactory(socketFactory)
.build();
HttpClient httpClient = HttpClients.custom()
.setConnectionManager(connectionManager)
.setSSLContext(sslContext)
.addInterceptorFirst(new HttpComponentsMessageSender.RemoveSoapHeadersInterceptor())
.build();
HttpComponentsClientHttpRequestFactory requestFactory = 
new HttpComponentsClientHttpRequestFactory(httpClient);
return new RestTemplate(requestFactory);
} catch (Exception e) {
throw new IllegalStateException("Failed to create mTLS RestTemplate", e);
}
}
@Bean
@ConditionalOnProperty(name = "app.mtls.enabled", havingValue = "false")
public RestTemplate standardRestTemplate() {
return new RestTemplate();
}
}

Service Implementation with mTLS

1. mTLS Service Client
@Service
@Slf4j
public class MtlsAwareServiceClient {
private final RestTemplate restTemplate;
private final MtlsCertificateManager certificateManager;
private final MtlsConfig mtlsConfig;
private final CircuitBreaker circuitBreaker;
public MtlsAwareServiceClient(@Qualifier("mtlsRestTemplate") RestTemplate restTemplate,
MtlsCertificateManager certificateManager,
MtlsConfig mtlsConfig,
CircuitBreakerRegistry circuitBreakerRegistry) {
this.restTemplate = restTemplate;
this.certificateManager = certificateManager;
this.mtlsConfig = mtlsConfig;
this.circuitBreaker = circuitBreakerRegistry.circuitBreaker("mtlsServiceClient");
}
public <T> T callServiceWithMtls(String serviceUrl, String path, Class<T> responseType) {
validateMtlsStatus();
validateServiceAccess(serviceUrl);
return circuitBreaker.executeSupplier(() -> {
try {
String fullUrl = buildServiceUrl(serviceUrl, path);
log.debug("Making mTLS request to: {}", fullUrl);
ResponseEntity<T> response = restTemplate.getForEntity(fullUrl, responseType);
if (response.getStatusCode().is2xxSuccessful()) {
log.debug("mTLS request successful to: {}", serviceUrl);
return response.getBody();
} else {
throw new MtlsServiceException(
"Service returned non-2xx status: " + response.getStatusCode());
}
} catch (ResourceAccessException e) {
log.error("mTLS connection failed to: {}", serviceUrl, e);
throw new MtlsConnectionException("mTLS connection failed", e);
}
});
}
public <T> T postWithMtls(String serviceUrl, String path, Object request, Class<T> responseType) {
validateMtlsStatus();
validateServiceAccess(serviceUrl);
return circuitBreaker.executeSupplier(() -> {
try {
String fullUrl = buildServiceUrl(serviceUrl, path);
log.debug("Making mTLS POST request to: {}", fullUrl);
ResponseEntity<T> response = restTemplate.postForEntity(fullUrl, request, responseType);
if (response.getStatusCode().is2xxSuccessful()) {
log.debug("mTLS POST request successful to: {}", serviceUrl);
return response.getBody();
} else {
throw new MtlsServiceException(
"Service returned non-2xx status: " + response.getStatusCode());
}
} catch (ResourceAccessException e) {
log.error("mTLS POST connection failed to: {}", serviceUrl, e);
throw new MtlsConnectionException("mTLS connection failed", e);
}
});
}
private void validateMtlsStatus() {
if (!mtlsConfig.isEnabled()) {
throw new IllegalStateException("mTLS is not enabled");
}
if (!certificateManager.isCertificateValid()) {
throw new MtlsSecurityException("Client certificate is not valid");
}
if (mtlsConfig.getMode() == MtlsConfig.MtlsMode.STRICT && 
!certificateManager.isCertificateValid()) {
throw new MtlsSecurityException("STRICT mode requires valid certificate");
}
}
private void validateServiceAccess(String serviceUrl) {
String serviceName = extractServiceName(serviceUrl);
if (!mtlsConfig.getAllowedServices().contains(serviceName)) {
throw new MtlsSecurityException(
"Service " + serviceName + " is not in allowed services list");
}
}
private String buildServiceUrl(String serviceUrl, String path) {
// For service mesh, use the service name with mTLS port
if (serviceUrl.contains(".")) {
// Full domain - use HTTPS
return "https://" + serviceUrl + ":8443" + path;
} else {
// Service name only - use mesh internal communication
return "https://" + serviceUrl + ".default.svc.cluster.local:8443" + path;
}
}
private String extractServiceName(String serviceUrl) {
// Extract service name from URL
// Format: service-name.namespace.svc.cluster.local
return serviceUrl.split("\\.")[0];
}
public MtlsConnectionInfo getConnectionInfo() {
return new MtlsConnectionInfo(
certificateManager.getServiceIdentity(),
certificateManager.isCertificateValid(),
mtlsConfig.getMode(),
mtlsConfig.getAllowedServices()
);
}
}
2. mTLS Security Service
@Service
@Slf4j
public class MtlsSecurityService {
private final MtlsCertificateManager certificateManager;
private final MtlsConfig mtlsConfig;
public MtlsSecurityService(MtlsCertificateManager certificateManager, 
MtlsConfig mtlsConfig) {
this.certificateManager = certificateManager;
this.mtlsConfig = mtlsConfig;
}
public boolean authenticateService(HttpServletRequest request) {
if (!mtlsConfig.isEnabled()) {
return true; // mTLS disabled, allow all
}
try {
X509Certificate[] certificates = (X509Certificate[]) 
request.getAttribute("javax.servlet.request.X509Certificate");
if (certificates == null || certificates.length == 0) {
log.warn("No client certificate presented in request");
return mtlsConfig.getMode() != MtlsConfig.MtlsMode.STRICT;
}
X509Certificate clientCert = certificates[0];
return validateCertificate(clientCert);
} catch (Exception e) {
log.error("Certificate authentication failed", e);
return false;
}
}
private boolean validateCertificate(X509Certificate certificate) {
try {
// Check certificate validity
certificate.checkValidity();
// Verify certificate chain (simplified)
String subject = certificate.getSubjectX500Principal().getName();
String issuer = certificate.getIssuerX500Principal().getName();
log.debug("Validating certificate - Subject: {}, Issuer: {}", subject, issuer);
// Extract service name from subject
String serviceName = extractServiceName(subject);
// Check if service is allowed to communicate with this service
return isServiceAllowed(serviceName);
} catch (CertificateException e) {
log.warn("Certificate validation failed: {}", e.getMessage());
return false;
}
}
private String extractServiceName(String subject) {
// Certificate subject format: CN=service-name.namespace.svc.cluster.local
Pattern pattern = Pattern.compile("CN=([^.]+)");
Matcher matcher = pattern.matcher(subject);
if (matcher.find()) {
return matcher.group(1);
}
return "unknown";
}
private boolean isServiceAllowed(String serviceName) {
return mtlsConfig.getAllowedServices().contains(serviceName);
}
public ServiceIdentity getCallerIdentity(HttpServletRequest request) {
try {
X509Certificate[] certificates = (X509Certificate[]) 
request.getAttribute("javax.servlet.request.X509Certificate");
if (certificates != null && certificates.length > 0) {
X509Certificate cert = certificates[0];
String subject = cert.getSubjectX500Principal().getName();
String serviceName = extractServiceName(subject);
return new ServiceIdentity(
serviceName,
subject,
cert.getIssuerX500Principal().getName(),
cert.getNotBefore(),
cert.getNotAfter()
);
}
} catch (Exception e) {
log.error("Failed to extract caller identity", e);
}
return ServiceIdentity.UNKNOWN;
}
}
3. mTLS Security Filter
@Component
@Slf4j
public class MtlsSecurityFilter implements Filter {
private final MtlsSecurityService mtlsSecurityService;
private final MtlsConfig mtlsConfig;
public MtlsSecurityFilter(MtlsSecurityService mtlsSecurityService, 
MtlsConfig mtlsConfig) {
this.mtlsSecurityService = mtlsSecurityService;
this.mtlsConfig = mtlsConfig;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, 
FilterChain chain) throws IOException, ServletException {
if (!mtlsConfig.isEnabled()) {
chain.doFilter(request, response);
return;
}
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// Skip mTLS check for health endpoints
if (shouldSkipMtlsCheck(httpRequest)) {
chain.doFilter(request, response);
return;
}
// Authenticate service using mTLS
if (mtlsSecurityService.authenticateService(httpRequest)) {
ServiceIdentity caller = mtlsSecurityService.getCallerIdentity(httpRequest);
log.debug("mTLS authentication successful for service: {}", caller.getServiceName());
// Add caller identity to request for business logic
request.setAttribute("callerServiceIdentity", caller);
chain.doFilter(request, response);
} else {
log.warn("mTLS authentication failed for request: {}", httpRequest.getRequestURI());
httpResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
httpResponse.getWriter().write("mTLS authentication failed");
}
}
private boolean shouldSkipMtlsCheck(HttpServletRequest request) {
String path = request.getRequestURI();
return path.startsWith("/actuator/health") || 
path.startsWith("/actuator/info") ||
path.startsWith("/actuator/mtls");
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("mTLS Security Filter initialized with mode: {}", mtlsConfig.getMode());
}
@Override
public void destroy() {
log.info("mTLS Security Filter destroyed");
}
}
4. Frontend Service with mTLS
@RestController
@RequestMapping("/api/frontend")
@Slf4j
public class FrontendController {
private final MtlsAwareServiceClient mtlsClient;
private final MtlsSecurityService mtlsSecurityService;
public FrontendController(MtlsAwareServiceClient mtlsClient,
MtlsSecurityService mtlsSecurityService) {
this.mtlsClient = mtlsClient;
this.mtlsSecurityService = mtlsSecurityService;
}
@GetMapping("/user/{userId}/orders")
public ResponseEntity<UserOrdersResponse> getUserOrders(@PathVariable String userId,
HttpServletRequest request) {
try {
// Get caller identity for audit
ServiceIdentity caller = mtlsSecurityService.getCallerIdentity(request);
log.info("Processing request from service: {} for user: {}", 
caller.getServiceName(), userId);
// Call backend service with mTLS
String backendUrl = "backend-service.default.svc.cluster.local";
List<Order> orders = mtlsClient.callServiceWithMtls(
backendUrl, "/api/orders/user/" + userId, List.class);
// Call user service with mTLS
String userServiceUrl = "user-service.default.svc.cluster.local";
User user = mtlsClient.callServiceWithMtls(
userServiceUrl, "/api/users/" + userId, User.class);
UserOrdersResponse response = new UserOrdersResponse(user, orders);
return ResponseEntity.ok(response);
} catch (MtlsSecurityException e) {
log.error("mTLS security violation", e);
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
} catch (MtlsConnectionException e) {
log.error("mTLS connection failed", e);
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).build();
}
}
@PostMapping("/orders")
public ResponseEntity<Order> createOrder(@RequestBody CreateOrderRequest request,
HttpServletRequest httpRequest) {
try {
// Validate caller has permission to create orders
ServiceIdentity caller = (ServiceIdentity) 
httpRequest.getAttribute("callerServiceIdentity");
if (!isAllowedToCreateOrders(caller)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
// Call backend service with mTLS
String backendUrl = "backend-service.default.svc.cluster.local";
Order order = mtlsClient.postWithMtls(
backendUrl, "/api/orders", request, Order.class);
return ResponseEntity.status(HttpStatus.CREATED).body(order);
} catch (MtlsSecurityException e) {
log.error("mTLS security violation", e);
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}
private boolean isAllowedToCreateOrders(ServiceIdentity caller) {
// Business logic for authorization
return "web-gateway".equals(caller.getServiceName()) ||
"mobile-gateway".equals(caller.getServiceName());
}
@GetMapping("/mtls-info")
public ResponseEntity<MtlsConnectionInfo> getMtlsInfo() {
MtlsConnectionInfo info = mtlsClient.getConnectionInfo();
return ResponseEntity.ok(info);
}
}
5. mTLS Actuator Endpoint
@Component
@Endpoint(id = "mtls")
@Slf4j
public class MtlsActuatorEndpoint {
private final MtlsCertificateManager certificateManager;
private final MtlsConfig mtlsConfig;
private final MtlsSecurityService mtlsSecurityService;
public MtlsActuatorEndpoint(MtlsCertificateManager certificateManager,
MtlsConfig mtlsConfig,
MtlsSecurityService mtlsSecurityService) {
this.certificateManager = certificateManager;
this.mtlsConfig = mtlsConfig;
this.mtlsSecurityService = mtlsSecurityService;
}
@ReadOperation
public MtlsStatus getMtlsStatus() {
return new MtlsStatus(
mtlsConfig.isEnabled(),
mtlsConfig.getMode().toString(),
certificateManager.isCertificateValid(),
certificateManager.getServiceIdentity(),
mtlsConfig.getAllowedServices()
);
}
@ReadOperation
public CertificateInfo getCertificateInfo() {
X509Certificate cert = certificateManager.getClientCertificate();
if (cert == null) {
return new CertificateInfo("No certificate available", null, null, null, null);
}
return new CertificateInfo(
cert.getSubjectX500Principal().getName(),
cert.getIssuerX500Principal().getName(),
cert.getNotBefore(),
cert.getNotAfter(),
cert.getSerialNumber().toString()
);
}
public static class MtlsStatus {
private final boolean enabled;
private final String mode;
private final boolean certificateValid;
private final String serviceIdentity;
private final Set<String> allowedServices;
// Constructor, getters...
}
public static class CertificateInfo {
private final String subject;
private final String issuer;
private final Date validFrom;
private final Date validUntil;
private final String serialNumber;
// Constructor, getters...
}
}

Kubernetes Deployment with mTLS

1. Istio mTLS Configuration
# k8s/istio-mtls.yaml
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default
namespace: default
spec:
mtls:
mode: STRICT
---
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: frontend-service
namespace: default
spec:
selector:
matchLabels:
app: frontend-service
mtls:
mode: STRICT
---
apiVersion: security.istio.io/v1beta1
kind: DestinationRule
metadata:
name: enable-mtls
namespace: default
spec:
host: "*.svc.cluster.local"
trafficPolicy:
tls:
mode: ISTIO_MUTUAL
2. Frontend Service Deployment
# k8s/frontend-service.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend-service
labels:
app: frontend-service
version: v1
spec:
replicas: 3
selector:
matchLabels:
app: frontend-service
version: v1
template:
metadata:
labels:
app: frontend-service
version: v1
annotations:
sidecar.istio.io/inject: "true"
proxy.istio.io/config: |
tracing:
zipkin:
address: zipkin:9411
spec:
serviceAccountName: frontend-service-account
containers:
- name: frontend-service
image: frontend-service:latest
ports:
- containerPort: 8443
name: https
- containerPort: 8444
name: management
env:
- name: SPRING_PROFILES_ACTIVE
value: "kubernetes,mtls"
- name: APP_MTLS_ENABLED
value: "true"
- name: APP_MTLS_MODE
value: "STRICT"
resources:
requests:
memory: "512Mi"
cpu: "200m"
limits:
memory: "1Gi"
cpu: "500m"
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8444
scheme: HTTPS
initialDelaySeconds: 30
periodSeconds: 10
livenessProbe:
httpGet:
path: /actuator/health
port: 8444
scheme: HTTPS
initialDelaySeconds: 60
periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: frontend-service
labels:
app: frontend-service
spec:
selector:
app: frontend-service
ports:
- name: https
port: 8443
targetPort: 8443
- name: management
port: 8444
targetPort: 8444
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: frontend-service-account
3. Service Role for mTLS
# k8s/service-roles.yaml
apiVersion: rbac.istio.io/v1alpha1
kind: ServiceRole
metadata:
name: frontend-service-role
namespace: default
spec:
rules:
- services: ["frontend-service.default.svc.cluster.local"]
methods: ["*"]
paths: ["*"]
---
apiVersion: rbac.istio.io/v1alpha1
kind: ServiceRoleBinding
metadata:
name: frontend-service-binding
namespace: default
spec:
subjects:
- properties:
source.namespace: "default"
roleRef:
kind: ServiceRole
name: frontend-service-role

Testing and Monitoring

1. mTLS Integration Tests
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
@ActiveProfiles("test")
class MtlsIntegrationTest {
@Container
static GenericContainer<?> consulContainer = new GenericContainer<>("consul:1.15")
.withExposedPorts(8500)
.withCommand("agent", "-dev", "-client", "0.0.0.0");
@Autowired
private TestRestTemplate testRestTemplate;
@Autowired
private MtlsCertificateManager certificateManager;
@Test
void testMtlsEnabled() {
MtlsConfig mtlsConfig = certificateManager.getMtlsConfig();
assertThat(mtlsConfig.isEnabled()).isTrue();
}
@Test
void testCertificateValidation() {
assertThat(certificateManager.isCertificateValid()).isTrue();
}
@Test
void testMtlsActuatorEndpoint() {
ResponseEntity<MtlsActuatorEndpoint.MtlsStatus> response = testRestTemplate.getForEntity(
"/actuator/mtls", MtlsActuatorEndpoint.MtlsStatus.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody().isEnabled()).isTrue();
}
}
2. mTLS Health Indicator
@Component
public class MtlsHealthIndicator implements HealthIndicator {
private final MtlsCertificateManager certificateManager;
private final MtlsConfig mtlsConfig;
public MtlsHealthIndicator(MtlsCertificateManager certificateManager, 
MtlsConfig mtlsConfig) {
this.certificateManager = certificateManager;
this.mtlsConfig = mtlsConfig;
}
@Override
public Health health() {
if (!mtlsConfig.isEnabled()) {
return Health.unknown()
.withDetail("mTLS", "disabled")
.build();
}
try {
boolean certificateValid = certificateManager.isCertificateValid();
String serviceIdentity = certificateManager.getServiceIdentity();
if (certificateValid) {
return Health.up()
.withDetail("mTLS", "enabled")
.withDetail("mode", mtlsConfig.getMode())
.withDetail("serviceIdentity", serviceIdentity)
.withDetail("certificateValid", true)
.build();
} else {
return Health.down()
.withDetail("mTLS", "enabled")
.withDetail("mode", mtlsConfig.getMode())
.withDetail("serviceIdentity", serviceIdentity)
.withDetail("certificateValid", false)
.withDetail("error", "Certificate is not valid")
.build();
}
} catch (Exception e) {
return Health.down()
.withDetail("mTLS", "error")
.withDetail("error", e.getMessage())
.build();
}
}
}

Best Practices

  1. Start with PERMISSIVE mode: Gradually move to STRICT mode
  2. Monitor certificate rotation: Ensure automatic rotation works correctly
  3. Implement proper error handling: Handle mTLS failures gracefully
  4. Use circuit breakers: Prevent cascading failures
  5. Monitor mTLS metrics: Track success/failure rates
  6. Test across service boundaries: Ensure end-to-end mTLS works
// Example of gradual mTLS enforcement
public class GradualMtlsEnforcement {
public void enforceMtls() {
// Phase 1: Monitor without enforcement
// Phase 2: PERMISSIVE mode (allow non-mTLS)
// Phase 3: STRICT mode (enforce mTLS)
// Phase 4: Monitor and optimize
}
}

Conclusion

mTLS enforcement in service mesh provides:

  • Automatic certificate management with zero manual intervention
  • Strong service identity for authentication and authorization
  • End-to-end encryption for all service communication
  • Zero-trust security model implementation

By implementing the patterns shown above, Java applications can seamlessly integrate with service mesh mTLS capabilities, providing robust security while maintaining development velocity. The combination of automatic certificate management, service identity, and fine-grained access control creates a secure foundation for microservices communication.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper