google-site-verification: google61fe8ba583a51912.html
Istio mTLS Policy Enforcement in Java

Overview

Istio mTLS (mutual TLS) provides service-to-service authentication and encryption. Java applications can leverage Istio's mTLS capabilities while implementing additional security checks and policy enforcement.

1. Basic mTLS Configuration Detection

import javax.net.ssl.*; import java.security.cert.X509Certificate; import java.util.Enumeration; public class IstioMTLSDetector { public static void checkMTLSConnection() { try { SSLSession session = ((SSLSocket) SSLSocketFactory.getDefault().createSocket()).getSession(); System.out.println("=== mTLS Connection Details ==="); System.out.println("Protocol: " + session.getProtocol()); System.out.println("Cipher Suite: " + session.getCipherSuite()); // Check if client certificate is present (mTLS) Certificate[] clientCerts = session.getPeerCertificates(); if (clientCerts != null && clientCerts.length > 0) { System.out.println("✓ mTLS Enabled: Client certificate present"); analyzeClientCertificate((X509Certificate) clientCerts[0]); } else { System.out.println("✗ mTLS Not Active: No client certificate"); } } catch (Exception e) { System.err.println("Error checking mTLS: " + e.getMessage()); } } private static void analyzeClientCertificate(X509Certificate cert) { try { System.out.println("Client Certificate Details:"); System.out.println(" Subject: " + cert.getSubjectX500Principal()); System.out.println(" Issuer: " + cert.getIssuerX500Principal()); System.out.println(" Serial: " + cert.getSerialNumber()); System.out.println(" Valid Until: " + cert.getNotAfter()); // Check SPIFFE ID for service identity String spiffeId = extractSpiffeId(cert); if (spiffeId != null) { System.out.println(" SPIFFE ID: " + spiffeId); } } catch (Exception e) { System.err.println("Error analyzing certificate: " + e.getMessage()); } } private static String extractSpiffeId(X509Certificate cert) { // SPIFFE ID is typically in the URI SAN extension try { Collection<List<?>> sans = cert.getSubjectAlternativeNames(); if (sans != null) { for (List<?> san : sans) { if (san.size() >= 2) { Integer type = (Integer) san.get(0); if (type == 6) { // URI type String uri = (String) san.get(1); if (uri.startsWith("spiffe://")) { return uri; } } } } } } catch (Exception e) { // Ignore extraction errors } return null; } }

2. Spring Boot mTLS Configuration

Application Properties

# application.yml server: port: 8443 ssl: key-store: classpath:keystore.p12 key-store-password: changeit key-store-type: PKCS12 key-alias: server client-auth: need trust-store: classpath:truststore.p12 trust-store-password: changeit trust-store-type: PKCS12 spring: application: name: secure-service security: require-ssl: true # Custom mTLS properties mtls: enforcement: enabled: true require-spiffe: true allowed-spiffe-patterns: - "spiffe://cluster.local/ns/.*/sa/.*" require-specific-issuer: "spiffe://root.example.com"

Spring Security mTLS Configuration

import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter; import javax.servlet.http.HttpServletRequest; import java.security.cert.X509Certificate; import java.util.regex.Pattern; @Configuration @EnableWebSecurity public class MTlsSecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authz -> authz .requestMatchers("/public/**").permitAll() .requestMatchers("/admin/**").hasRole("ADMIN") .anyRequest().authenticated() ) .x509(x509 -> x509 .x509AuthenticationFilter(x509AuthenticationFilter()) .subjectPrincipalRegex("CN=(.*?)(?:,|$)") ) .ssl(ssl -> ssl.requireSecure(true)); return http.build(); } @Bean public X509AuthenticationFilter x509AuthenticationFilter() { X509AuthenticationFilter filter = new X509AuthenticationFilter(); filter.setAuthenticationManager(authentication -> { if (authentication.getCredentials() instanceof X509Certificate) { X509Certificate cert = (X509Certificate) authentication.getCredentials(); return authenticateWithCertificate(cert); } throw new RuntimeException("Invalid authentication"); }); return filter; } private Authentication authenticateWithCertificate(X509Certificate cert) { try { // Extract SPIFFE ID String spiffeId = extractSpiffeId(cert); // Validate SPIFFE ID if (!isValidSpiffeId(spiffeId)) { throw new RuntimeException("Invalid SPIFFE ID: " + spiffeId); } // Extract service account from SPIFFE ID String serviceAccount = extractServiceAccount(spiffeId); String role = determineRole(serviceAccount); User user = new User(serviceAccount, "", AuthorityUtils.createAuthorityList("ROLE_" + role)); return new PreAuthenticatedAuthenticationToken(user, cert, AuthorityUtils.createAuthorityList("ROLE_" + role)); } catch (Exception e) { throw new RuntimeException("Certificate authentication failed", e); } } private boolean isValidSpiffeId(String spiffeId) { if (spiffeId == null) return false; Pattern pattern = Pattern.compile("^spiffe://cluster\\.local/ns/[^/]+/sa/[^/]+$"); return pattern.matcher(spiffeId).matches(); } private String extractServiceAccount(String spiffeId) { // spiffe://cluster.local/ns/default/sa/my-service String[] parts = spiffeId.split("/"); return parts[parts.length - 1]; // Returns "my-service" } private String determineRole(String serviceAccount) { // Map service accounts to roles if (serviceAccount.endsWith("-admin")) { return "ADMIN"; } else if (serviceAccount.endsWith("-reader")) { return "READER"; } return "USER"; } }

3. Advanced mTLS Policy Enforcement

import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.security.cert.X509Certificate; import java.util.*; @Component public class MTlsPolicyEnforcementFilter extends OncePerRequestFilter { private final PolicyEngine policyEngine; private final CertificateValidator certificateValidator; public MTlsPolicyEnforcementFilter(PolicyEngine policyEngine, CertificateValidator certificateValidator) { this.policyEngine = policyEngine; this.certificateValidator = certificateValidator; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // Extract client certificate from request X509Certificate[] certificates = (X509Certificate[]) request.getAttribute("javax.servlet.request.X509Certificate"); if (certificates == null || certificates.length == 0) { sendError(response, "No client certificate provided", HttpServletResponse.SC_UNAUTHORIZED); return; } X509Certificate clientCert = certificates[0]; try { // Validate certificate CertificateValidationResult validationResult = certificateValidator.validateCertificate(clientCert); if (!validationResult.isValid()) { sendError(response, "Invalid client certificate: " + validationResult.getErrorMessage(), HttpServletResponse.SC_UNAUTHORIZED); return; } // Extract identity information ServiceIdentity identity = extractServiceIdentity(clientCert); // Apply policies PolicyDecision decision = policyEngine.evaluate(request, identity); if (!decision.isAllowed()) { sendError(response, "Access denied: " + decision.getReason(), HttpServletResponse.SC_FORBIDDEN); return; } // Add identity to request for downstream use request.setAttribute("serviceIdentity", identity); filterChain.doFilter(request, response); } catch (Exception e) { sendError(response, "Internal server error", HttpServletResponse.SC_INTERNAL_SERVER_ERROR); } } private ServiceIdentity extractServiceIdentity(X509Certificate cert) { String spiffeId = extractSpiffeId(cert); String namespace = extractNamespace(spiffeId); String serviceAccount = extractServiceAccount(spiffeId); return new ServiceIdentity(spiffeId, namespace, serviceAccount, cert.getSubjectX500Principal().getName()); } private void sendError(HttpServletResponse response, String message, int status) throws IOException { response.setStatus(status); response.setContentType("application/json"); response.getWriter().write(String.format( "{\"error\": \"%s\", \"status\": %d}", message, status)); } } // Supporting classes class ServiceIdentity { private final String spiffeId; private final String namespace; private final String serviceAccount; private final String subjectDN; public ServiceIdentity(String spiffeId, String namespace, String serviceAccount, String subjectDN) { this.spiffeId = spiffeId; this.namespace = namespace; this.serviceAccount = serviceAccount; this.subjectDN = subjectDN; } // Getters public String getSpiffeId() { return spiffeId; } public String getNamespace() { return namespace; } public String getServiceAccount() { return serviceAccount; } public String getSubjectDN() { return subjectDN; } } class CertificateValidationResult { private final boolean valid; private final String errorMessage; public CertificateValidationResult(boolean valid, String errorMessage) { this.valid = valid; this.errorMessage = errorMessage; } public boolean isValid() { return valid; } public String getErrorMessage() { return errorMessage; } } class PolicyDecision { private final boolean allowed; private final String reason; public PolicyDecision(boolean allowed, String reason) { this.allowed = allowed; this.reason = reason; } public boolean isAllowed() { return allowed; } public String getReason() { return reason; } }

4. Policy Engine Implementation

import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import java.util.*; import java.util.regex.Pattern; @Component public class PolicyEngine { private final List<AccessPolicy> policies; public PolicyEngine() { this.policies = Arrays.asList( new NamespaceIsolationPolicy(), new ServiceAccountPolicy(), new TimeBasedPolicy(), new ResourceAccessPolicy() ); } public PolicyDecision evaluate(HttpServletRequest request, ServiceIdentity identity) { List<String> denialReasons = new ArrayList<>(); for (AccessPolicy policy : policies) { PolicyDecision decision = policy.evaluate(request, identity); if (!decision.isAllowed()) { denialReasons.add(decision.getReason()); } } if (denialReasons.isEmpty()) { return new PolicyDecision(true, "Access granted"); } else { return new PolicyDecision(false, String.join("; ", denialReasons)); } } } interface AccessPolicy { PolicyDecision evaluate(HttpServletRequest request, ServiceIdentity identity); } class NamespaceIsolationPolicy implements AccessPolicy { @Override public PolicyDecision evaluate(HttpServletRequest request, ServiceIdentity identity) { String requestPath = request.getRequestURI(); String callerNamespace = identity.getNamespace(); // Extract target namespace from request path or headers String targetNamespace = extractTargetNamespace(request); if (targetNamespace != null && !callerNamespace.equals(targetNamespace)) { // Allow cross-namespace access only for specific endpoints if (!isCrossNamespaceAllowed(requestPath)) { return new PolicyDecision(false, "Cross-namespace access not allowed to " + requestPath); } } return new PolicyDecision(true, "Namespace check passed"); } private String extractTargetNamespace(HttpServletRequest request) { // Extract from headers (Istio typically adds these) String namespace = request.getHeader("x-forwarded-namespace"); if (namespace != null) return namespace; // Extract from path pattern: /api/v1/namespaces/{namespace}/... String path = request.getRequestURI(); Pattern pattern = Pattern.compile("/api/v1/namespaces/([^/]+)/"); java.util.regex.Matcher matcher = pattern.matcher(path); if (matcher.find()) { return matcher.group(1); } return null; } private boolean isCrossNamespaceAllowed(String path) { // Define paths that allow cross-namespace access return path.startsWith("/api/public") || path.startsWith("/health") || path.startsWith("/metrics"); } } class ServiceAccountPolicy implements AccessPolicy { private final Set<String> privilegedAccounts = Set.of( "istio-system", "kube-system", "cluster-admin" ); @Override public PolicyDecision evaluate(HttpServletRequest request, ServiceIdentity identity) { String serviceAccount = identity.getServiceAccount(); String path = request.getRequestURI(); // Check if privileged service account is accessing sensitive endpoints if (isSensitiveEndpoint(path) && isPrivilegedAccount(serviceAccount)) { return new PolicyDecision(false, "Privileged service account cannot access sensitive endpoints"); } // Check service account specific restrictions if (hasServiceAccountRestriction(serviceAccount, path)) { return new PolicyDecision(false, "Service account restricted from accessing " + path); } return new PolicyDecision(true, "Service account check passed"); } private boolean isSensitiveEndpoint(String path) { return path.contains("/admin/") || path.contains("/config/") || path.contains("/secret/"); } private boolean isPrivilegedAccount(String serviceAccount) { return privilegedAccounts.stream() .anyMatch(serviceAccount::contains); } private boolean hasServiceAccountRestriction(String serviceAccount, String path) { // Implement service account specific restrictions Map<String, Set<String>> restrictions = Map.of( "readonly-sa", Set.of("/api/write", "/api/delete"), "external-sa", Set.of("/api/internal", "/admin") ); Set<String> restrictedPaths = restrictions.get(serviceAccount); return restrictedPaths != null && restrictedPaths.stream() .anyMatch(path::startsWith); } } class TimeBasedPolicy implements AccessPolicy { @Override public PolicyDecision evaluate(HttpServletRequest request, ServiceIdentity identity) { Calendar cal = Calendar.getInstance(); int hour = cal.get(Calendar.HOUR_OF_DAY); // Restrict access during maintenance window (2 AM - 4 AM) if (hour >= 2 && hour < 4) { String path = request.getRequestURI(); if (!isMaintenanceAllowed(path)) { return new PolicyDecision(false, "Access restricted during maintenance window (2AM-4AM)"); } } return new PolicyDecision(true, "Time-based check passed"); } private boolean isMaintenanceAllowed(String path) { return path.startsWith("/health") || path.startsWith("/maintenance") || path.startsWith("/metrics"); } } class ResourceAccessPolicy implements AccessPolicy { @Override public PolicyDecision evaluate(HttpServletRequest request, ServiceIdentity identity) { String method = request.getMethod(); String path = request.getRequestURI(); // RBAC based on service identity and resource if (method.equals("DELETE") && !canDelete(identity, path)) { return new PolicyDecision(false, "Delete operation not permitted"); } if (method.equals("POST") && path.contains("/admin") && !isAdmin(identity)) { return new PolicyDecision(false, "Admin access required"); } return new PolicyDecision(true, "Resource access check passed"); } private boolean canDelete(ServiceIdentity identity, String path) { return identity.getServiceAccount().endsWith("-admin") || path.startsWith("/api/user/") && path.endsWith(identity.getServiceAccount()); } private boolean isAdmin(ServiceIdentity identity) { return identity.getServiceAccount().endsWith("-admin") || identity.getNamespace().equals("admin"); } }

5. Certificate Validator

import org.springframework.stereotype.Component; import java.security.cert.X509Certificate; import java.util.Date; import java.util.Set; @Component public class CertificateValidator { private final Set<String> trustedIssuers = Set.of( "CN=istio-ca, O=Istio", "CN=cluster.local", "OU=spiffe://cluster.local" ); private final CertificateRevocationChecker revocationChecker; private final CertificateCache certificateCache; public CertificateValidator(CertificateRevocationChecker revocationChecker, CertificateCache certificateCache) { this.revocationChecker = revocationChecker; this.certificateCache = certificateCache; } public CertificateValidationResult validateCertificate(X509Certificate cert) { try { // Check cache first String certFingerprint = generateFingerprint(cert); if (certificateCache.isBlacklisted(certFingerprint)) { return new CertificateValidationResult(false, "Certificate is blacklisted"); } // Basic validity check cert.checkValidity(new Date()); // Check issuer if (!isTrustedIssuer(cert)) { return new CertificateValidationResult(false, "Untrusted certificate issuer"); } // Check SPIFFE ID format String spiffeId = extractSpiffeId(cert); if (spiffeId == null || !isValidSpiffeFormat(spiffeId)) { return new CertificateValidationResult(false, "Invalid SPIFFE ID format"); } // Check revocation if (revocationChecker.isRevoked(cert)) { certificateCache.blacklist(certFingerprint); return new CertificateValidationResult(false, "Certificate is revoked"); } // Check certificate chain (if available) if (!isValidCertificateChain(cert)) { return new CertificateValidationResult(false, "Invalid certificate chain"); } return new CertificateValidationResult(true, null); } catch (Exception e) { return new CertificateValidationResult(false, "Certificate validation failed: " + e.getMessage()); } } private boolean isTrustedIssuer(X509Certificate cert) { String issuerDN = cert.getIssuerX500Principal().getName(); return trustedIssuers.stream() .anyMatch(issuerDN::contains); } private boolean isValidSpiffeFormat(String spiffeId) { return spiffeId.matches("^spiffe://[^/]+/ns/[^/]+/sa/[^/]+$"); } private boolean isValidCertificateChain(X509Certificate cert) { // In production, this would validate the entire certificate chain // against the Istio root CA return true; // Simplified for example } private String generateFingerprint(X509Certificate cert) throws Exception { // Generate SHA-256 fingerprint of certificate java.security.MessageDigest md = java.security.MessageDigest.getInstance("SHA-256"); byte[] digest = md.digest(cert.getEncoded()); return bytesToHex(digest); } private String bytesToHex(byte[] bytes) { StringBuilder result = new StringBuilder(); for (byte b : bytes) { result.append(String.format("%02x", b)); } return result.toString(); } } @Component class CertificateRevocationChecker { public boolean isRevoked(X509Certificate cert) { // Implement CRL or OCSP checking // For Istio, typically certificates are short-lived so revocation is less critical return false; } } @Component class CertificateCache { private final Set<String> blacklistedCertificates = Collections.synchronizedSet(new HashSet<>()); public void blacklist(String certificateFingerprint) { blacklistedCertificates.add(certificateFingerprint); } public boolean isBlacklisted(String certificateFingerprint) { return blacklistedCertificates.contains(certificateFingerprint); } }

6. mTLS-aware REST Client

import org.springframework.http.*; import org.springframework.web.client.RestTemplate; import javax.net.ssl.*; import java.security.KeyStore; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; @Component public class MTlsRestClient { private final RestTemplate restTemplate; private final ServiceIdentityProvider identityProvider; public MTlsRestClient(ServiceIdentityProvider identityProvider) throws Exception { this.identityProvider = identityProvider; this.restTemplate = createMTlsRestTemplate(); } private RestTemplate createMTlsRestTemplate() throws Exception { // Load client certificate and key KeyStore keyStore = loadKeyStore(); KeyStore trustStore = loadTrustStore(); SSLContext sslContext = SSLContexts.custom() .loadKeyMaterial(keyStore, "changeit".toCharArray()) .loadTrustMaterial(trustStore, null) .build(); SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory( sslContext, new String[]{"TLSv1.2", "TLSv1.3"}, null, new DefaultHostnameVerifier()); HttpClient httpClient = HttpClients.custom() .setSSLSocketFactory(socketFactory) .build(); HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient); return new RestTemplate(factory); } public <T> ResponseEntity<T> executeWithMTls(String url, HttpMethod method, HttpEntity<?> requestEntity, Class<T> responseType) { // Add mTLS specific headers HttpHeaders headers = new HttpHeaders(); if (requestEntity.getHeaders() != null) { headers.putAll(requestEntity.getHeaders()); } // Add service identity headers ServiceIdentity identity = identityProvider.getCurrentIdentity(); headers.add("X-Service-Identity", identity.getSpiffeId()); headers.add("X-Service-Namespace", identity.getNamespace()); headers.add("X-Service-Account", identity.getServiceAccount()); HttpEntity<?> enhancedEntity = new HttpEntity<>( requestEntity.getBody(), headers); return restTemplate.exchange(url, method, enhancedEntity, responseType); } private KeyStore loadKeyStore() throws Exception { // Load client certificate and private key KeyStore keyStore = KeyStore.getInstance("PKCS12"); keyStore.load(getClass().getResourceAsStream("/client-keystore.p12"), "changeit".toCharArray()); return keyStore; } private KeyStore loadTrustStore() throws Exception { // Load CA certificates KeyStore trustStore = KeyStore.getInstance("JKS"); trustStore.load(getClass().getResourceAsStream("/truststore.jks"), "changeit".toCharArray()); return trustStore; } } @Component class ServiceIdentityProvider { public ServiceIdentity getCurrentIdentity() { // In a real implementation, this would extract identity from security context // or from the current mTLS certificate return new ServiceIdentity( "spiffe://cluster.local/ns/default/sa/my-service", "default", "my-service", "CN=my-service.default.cluster.local" ); } }

7. Monitoring and Logging

import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.security.cert.X509Certificate; @Component public class MTlsAuditInterceptor implements HandlerInterceptor { private final AuditLogger auditLogger; public MTlsAuditInterceptor(AuditLogger auditLogger) { this.auditLogger = auditLogger; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { try { X509Certificate[] certificates = (X509Certificate[]) request.getAttribute("javax.servlet.request.X509Certificate"); ServiceIdentity identity = (ServiceIdentity) request.getAttribute("serviceIdentity"); if (identity != null) { auditLogger.logAccessAttempt( identity, request.getMethod(), request.getRequestURI(), request.getRemoteAddr(), certificates != null && certificates.length > 0 ); } } catch (Exception e) { // Don't break the request for logging errors System.err.println("Audit logging failed: " + e.getMessage()); } return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { try { ServiceIdentity identity = (ServiceIdentity) request.getAttribute("serviceIdentity"); if (identity != null) { auditLogger.logAccessCompletion( identity, request.getMethod(), request.getRequestURI(), response.getStatus(), ex != null ? ex.getMessage() : null ); } } catch (Exception e) { System.err.println("Audit logging failed: " + e.getMessage()); } } } @Component class AuditLogger { public void logAccessAttempt(ServiceIdentity identity, String method, String path, String remoteAddr, boolean mTlsEnabled) { String logMessage = String.format( "ACCESS_ATTEMPT|spiffe_id=%s|method=%s|path=%s|remote_addr=%s|mtls=%s", identity.getSpiffeId(), method, path, remoteAddr, mTlsEnabled ); System.out.println(logMessage); } public void logAccessCompletion(ServiceIdentity identity, String method, String path, int status, String error) { String logMessage = String.format( "ACCESS_COMPLETE|spiffe_id=%s|method=%s|path=%s|status=%d|error=%s", identity.getSpiffeId(), method, path, status, error != null ? error : "none" ); System.out.println(logMessage); } }

Key Features

  1. mTLS Detection: Automatically detect and validate mTLS connections
  2. SPIFFE Identity Extraction: Parse SPIFFE IDs from client certificates
  3. Policy Enforcement: Implement fine-grained access control based on service identity
  4. Certificate Validation: Comprehensive certificate validation including revocation checking
  5. Security Integration: Seamless integration with Spring Security
  6. Audit Logging: Comprehensive logging of mTLS authentication and authorization events
  7. REST Client: mTLS-aware HTTP client for service-to-service communication

This implementation provides a robust foundation for enforcing mTLS policies in Java applications within an Istio service mesh, ensuring secure service-to-service communication with proper identity-based access control.

Leave a Reply

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


Macro Nepal Helper