Introduction
SPIFFE (Secure Production Identity Framework For Everyone) and SPIRE (SPIFFE Runtime Environment) provide a universal identity framework for distributed systems. This guide covers SPIFFE/SPIRE integration in Java for secure service-to-service authentication and identity management.
Core SPIFFE Concepts
SPIFFE ID and SVID
package com.spiffe.core; import java.net.URI; import java.security.cert.X509Certificate; import java.util.List; public class SpiffeId { private final URI spiffeId; private final String trustDomain; private final String path; public SpiffeId(String spiffeId) { this.spiffeId = URI.create(spiffeId); validateSpiffeId(this.spiffeId); this.trustDomain = this.spiffeId.getHost(); this.path = this.spiffeId.getPath(); } public SpiffeId(String trustDomain, String path) { this.trustDomain = trustDomain; this.path = path != null ? path : ""; this.spiffeId = URI.create("spiffe://" + trustDomain + this.path); validateSpiffeId(this.spiffeId); } private void validateSpiffeId(URI id) { if (!"spiffe".equals(id.getScheme())) { throw new IllegalArgumentException("SPIFFE ID must use 'spiffe' scheme"); } if (id.getHost() == null || id.getHost().isEmpty()) { throw new IllegalArgumentException("SPIFFE ID must contain a trust domain"); } } public String getTrustDomain() { return trustDomain; } public String getPath() { return path; } public URI toUri() { return spiffeId; } @Override public String toString() { return spiffeId.toString(); } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; SpiffeId other = (SpiffeId) obj; return spiffeId.equals(other.spiffeId); } @Override public int hashCode() { return spiffeId.hashCode(); } } public class SpiffeSvid { private final SpiffeId spiffeId; private final X509Certificate certificate; private final List<X509Certificate> certificateChain; private final byte[] privateKey; // In production, use proper key storage private final long expiresAt; public SpiffeSvid(SpiffeId spiffeId, X509Certificate certificate, List<X509Certificate> certificateChain, byte[] privateKey, long expiresAt) { this.spiffeId = spiffeId; this.certificate = certificate; this.certificateChain = List.copyOf(certificateChain); this.privateKey = privateKey.clone(); this.expiresAt = expiresAt; } public boolean isExpired() { return System.currentTimeMillis() > expiresAt; } public boolean expiresSoon(long thresholdMs) { return (expiresAt - System.currentTimeMillis()) < thresholdMs; } // Getters public SpiffeId getSpiffeId() { return spiffeId; } public X509Certificate getCertificate() { return certificate; } public List<X509Certificate> getCertificateChain() { return certificateChain; } public byte[] getPrivateKey() { return privateKey.clone(); } public long getExpiresAt() { return expiresAt; } } SPIRE Agent Integration
Workload API Client
package com.spiffe.workload; import io.grpc.*; import com.google.protobuf.ByteString; import spire.api.workload.WorkloadOuterClass; import spire.api.workload.WorkloadGrpc; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; public class WorkloadApiClient implements AutoCloseable { private static final String DEFAULT_SOCKET_PATH = "/tmp/spire-agent/public/api.sock"; private static final long REFRESH_INTERVAL_MS = 300000; // 5 minutes private final ManagedChannel channel; private final WorkloadGrpc.WorkloadBlockingStub blockingStub; private final String socketPath; public WorkloadApiClient() { this(DEFAULT_SOCKET_PATH); } public WorkloadApiClient(String socketPath) { this.socketPath = socketPath; this.channel = createChannel(); this.blockingStub = WorkloadGrpc.newBlockingStub(channel); } private ManagedChannel createChannel() { return NettyChannelBuilder.forAddress("unix://" + socketPath) .channelType(io.netty.channel.epoll.EpollDomainSocketChannel.class) .eventLoopGroup(new io.netty.channel.epoll.EpollEventLoopGroup()) .usePlaintext() .build(); } public SpiffeSvid fetchX509Svid(String spiffeId) throws Exception { WorkloadOuterClass.X509SVIDRequest request = WorkloadOuterClass.X509SVIDRequest.newBuilder().build(); WorkloadOuterClass.X509SVIDResponse response = blockingStub.fetchX509SVID(request); for (WorkloadOuterClass.X509SVID svid : response.getSvidsList()) { if (spiffeId == null || spiffeId.equals(svid.getSpiffeId())) { return parseX509Svid(svid); } } throw new RuntimeException("No SVID found for SPIFFE ID: " + spiffeId); } public List<SpiffeSvid> fetchAllX509Svids() throws Exception { WorkloadOuterClass.X509SVIDRequest request = WorkloadOuterClass.X509SVIDRequest.newBuilder().build(); WorkloadOuterClass.X509SVIDResponse response = blockingStub.fetchX509SVID(request); List<SpiffeSvid> svids = new ArrayList<>(); for (WorkloadOuterClass.X509SVID svid : response.getSvidsList()) { svids.add(parseX509Svid(svid)); } return svids; } public List<X509Certificate> fetchX509Bundle(String trustDomain) throws Exception { WorkloadOuterClass.X509BundlesRequest request = WorkloadOuterClass.X509BundlesRequest.newBuilder().build(); WorkloadOuterClass.X509BundlesResponse response = blockingStub.fetchX509Bundles(request); String bundleKey = "spiffe://" + trustDomain; ByteString bundleData = response.getBundlesMap().get(bundleKey); if (bundleData == null) { throw new RuntimeException("No bundle found for trust domain: " + trustDomain); } return parseCertificates(bundleData.toByteArray()); } private SpiffeSvid parseX509Svid(WorkloadOuterClass.X509SVID svid) throws CertificateException { SpiffeId spiffeId = new SpiffeId(svid.getSpiffeId()); // Parse certificate byte[] certData = svid.getX509Svid().toByteArray(); X509Certificate certificate = parseCertificate(certData); // Parse certificate chain List<X509Certificate> chain = new ArrayList<>(); chain.add(certificate); for (ByteString chainCert : svid.getBundleList()) { chain.add(parseCertificate(chainCert.toByteArray())); } // Parse private key (this would be handled differently in production) byte[] privateKey = svid.getX509SvidKey().toByteArray(); long expiresAt = certificate.getNotAfter().getTime(); return new SpiffeSvid(spiffeId, certificate, chain, privateKey, expiresAt); } private X509Certificate parseCertificate(byte[] certData) throws CertificateException { CertificateFactory cf = CertificateFactory.getInstance("X.509"); return (X509Certificate) cf.generateCertificate( new java.io.ByteArrayInputStream(certData)); } private List<X509Certificate> parseCertificates(byte[] bundleData) throws CertificateException { CertificateFactory cf = CertificateFactory.getInstance("X.509"); java.io.ByteArrayInputStream bis = new java.io.ByteArrayInputStream(bundleData); List<X509Certificate> certificates = new ArrayList<>(); while (bis.available() > 0) { X509Certificate cert = (X509Certificate) cf.generateCertificate(bis); certificates.add(cert); } return certificates; } @Override public void close() { try { channel.shutdown().awaitTermination(5, TimeUnit.SECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); channel.shutdownNow(); } } } SVID Manager with Automatic Renewal
Automatic SVID Management
package com.spiffe.management; import com.spiffe.core.SpiffeSvid; import com.spiffe.workload.WorkloadApiClient; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; public class SvidManager implements AutoCloseable { private final WorkloadApiClient workloadClient; private final String spiffeId; private final long refreshIntervalMs; private final long expiryThresholdMs; private final AtomicReference<SpiffeSvid> currentSvid; private final ScheduledExecutorService scheduler; private final Consumer<SpiffeSvid> onSvidUpdated; private volatile boolean running = false; private ScheduledFuture<?> refreshTask; public SvidManager(String spiffeId, WorkloadApiClient workloadClient) { this(spiffeId, workloadClient, 300000, 60000, null); // 5 min refresh, 1 min threshold } public SvidManager(String spiffeId, WorkloadApiClient workloadClient, long refreshIntervalMs, long expiryThresholdMs, Consumer<SpiffeSvid> onSvidUpdated) { this.spiffeId = spiffeId; this.workloadClient = workloadClient; this.refreshIntervalMs = refreshIntervalMs; this.expiryThresholdMs = expiryThresholdMs; this.onSvidUpdated = onSvidUpdated; this.currentSvid = new AtomicReference<>(); this.scheduler = Executors.newSingleThreadScheduledExecutor( r -> new Thread(r, "spiffe-svid-manager")); } public void start() { if (running) { return; } running = true; // Initial SVID fetch refreshSvid(); // Schedule periodic refresh refreshTask = scheduler.scheduleAtFixedRate( this::refreshSvid, refreshIntervalMs, refreshIntervalMs, TimeUnit.MILLISECONDS ); // Schedule expiry check (more frequent) scheduler.scheduleAtFixedRate( this::checkExpiry, expiryThresholdMs / 2, expiryThresholdMs / 2, TimeUnit.MILLISECONDS ); } private void refreshSvid() { try { SpiffeSvid newSvid = workloadClient.fetchX509Svid(spiffeId); SpiffeSvid oldSvid = currentSvid.getAndSet(newSvid); // Notify listener if SVID changed if (onSvidUpdated != null && !newSvid.equals(oldSvid)) { onSvidUpdated.accept(newSvid); } System.out.println("Refreshed SVID for: " + spiffeId); } catch (Exception e) { System.err.println("Failed to refresh SVID: " + e.getMessage()); // In production, implement retry logic with backoff } } private void checkExpiry() { SpiffeSvid svid = currentSvid.get(); if (svid != null && svid.expiresSoon(expiryThresholdMs)) { System.out.println("SVID expiring soon, refreshing immediately"); refreshSvid(); } } public SpiffeSvid getCurrentSvid() { SpiffeSvid svid = currentSvid.get(); if (svid == null) { throw new IllegalStateException("SVID not available. Call start() first."); } return svid; } public boolean isHealthy() { SpiffeSvid svid = currentSvid.get(); return svid != null && !svid.isExpired(); } @Override public void close() { running = false; if (refreshTask != null) { refreshTask.cancel(true); } scheduler.shutdown(); try { if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) { scheduler.shutdownNow(); } } catch (InterruptedException e) { scheduler.shutdownNow(); Thread.currentThread().interrupt(); } workloadClient.close(); } } gRPC with SPIFFE Authentication
SPIFFE-aware gRPC Interceptors
package com.spiffe.grpc; import io.grpc.*; import com.spiffe.core.SpiffeSvid; import com.spiffe.management.SvidManager; import java.security.cert.X509Certificate; import java.util.concurrent.Executor; public class SpiffeClientInterceptor implements ClientInterceptor { private final SvidManager svidManager; public SpiffeClientInterceptor(SvidManager svidManager) { this.svidManager = svidManager; } @Override public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall( MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) { return new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>( next.newCall(method, callOptions)) { @Override public void start(Listener<RespT> responseListener, Metadata headers) { // Add SPIFFE SVID to headers addSpiffeHeaders(headers); super.start(responseListener, headers); } }; } private void addSpiffeHeaders(Metadata headers) { try { SpiffeSvid svid = svidManager.getCurrentSvid(); // Add certificate chain as headers int certIndex = 0; for (X509Certificate cert : svid.getCertificateChain()) { headers.put(Metadata.Key.of( "x-spiffe-cert-" + certIndex, Metadata.ASCII_STRING_MARSHALLER), cert.toString()); certIndex++; } // Add SPIFFE ID header headers.put(Metadata.Key.of( "x-spiffe-id", Metadata.ASCII_STRING_MARSHALLER), svid.getSpiffeId().toString()); } catch (Exception e) { System.err.println("Failed to add SPIFFE headers: " + e.getMessage()); } } } public class SpiffeServerInterceptor implements ServerInterceptor { private final String expectedTrustDomain; public SpiffeServerInterceptor(String expectedTrustDomain) { this.expectedTrustDomain = expectedTrustDomain; } @Override public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall( ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next) { try { // Extract and verify SPIFFE identity from headers SpiffeId clientId = extractAndVerifySpiffeId(headers); // Create context with SPIFFE identity Context context = Context.current().withValue( SpiffeContextKey.SPIFFE_ID, clientId); return Contexts.interceptCall(context, call, headers, next); } catch (SecurityException e) { call.close(Status.UNAUTHENTICATED.withDescription(e.getMessage()), new Metadata()); return new ServerCall.Listener<ReqT>() {}; } } private SpiffeId extractAndVerifySpiffeId(Metadata headers) { // Extract SPIFFE ID from headers String spiffeIdStr = headers.get(Metadata.Key.of( "x-spiffe-id", Metadata.ASCII_STRING_MARSHALLER)); if (spiffeIdStr == null) { throw new SecurityException("Missing SPIFFE ID header"); } SpiffeId spiffeId = new SpiffeId(spiffeIdStr); // Verify trust domain if (!expectedTrustDomain.equals(spiffeId.getTrustDomain())) { throw new SecurityException( "Untrusted SPIFFE ID trust domain: " + spiffeId.getTrustDomain()); } // Extract and verify certificates (simplified) verifyCertificates(headers, spiffeId); return spiffeId; } private void verifyCertificates(Metadata headers, SpiffeId expectedId) { // In production, this would verify the certificate chain against the trust bundle // and ensure the certificate contains the expected SPIFFE ID // Simplified implementation for (int i = 0; headers.containsKey( Metadata.Key.of("x-spiffe-cert-" + i, Metadata.ASCII_STRING_MARSHALLER)); i++) { String certStr = headers.get(Metadata.Key.of( "x-spiffe-cert-" + i, Metadata.ASCII_STRING_MARSHALLER)); // Verify certificate (implementation details would go here) System.out.println("Verifying certificate: " + certStr); } } } // Context key for accessing SPIFFE ID in gRPC services public class SpiffeContextKey { public static final Context.Key<SpiffeId> SPIFFE_ID = Context.key("spiffe-id"); } Spring Boot Integration
SPIFFE Spring Boot Starter
package com.spiffe.spring; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import com.spiffe.management.SvidManager; import com.spiffe.workload.WorkloadApiClient; @Configuration @ConfigurationProperties(prefix = "spiffe") public class SpiffeAutoConfiguration { private String spiffeId; private String socketPath = "/tmp/spire-agent/public/api.sock"; private long refreshIntervalMs = 300000; private long expiryThresholdMs = 60000; @Bean public WorkloadApiClient workloadApiClient() { return new WorkloadApiClient(socketPath); } @Bean public SvidManager svidManager(WorkloadApiClient workloadApiClient) { SvidManager manager = new SvidManager( spiffeId, workloadApiClient, refreshIntervalMs, expiryThresholdMs, this::onSvidUpdated); manager.start(); return manager; } @Bean public SpiffeClientInterceptor spiffeClientInterceptor(SvidManager svidManager) { return new SpiffeClientInterceptor(svidManager); } @Bean public SpiffeServerInterceptor spiffeServerInterceptor() { // Extract trust domain from SPIFFE ID String trustDomain = new SpiffeId(spiffeId).getTrustDomain(); return new SpiffeServerInterceptor(trustDomain); } private void onSvidUpdated(SpiffeSvid newSvid) { System.out.println("SVID updated for: " + newSvid.getSpiffeId()); // In production, you might want to update TLS contexts or other security configurations } // Getters and setters for configuration properties public String getSpiffeId() { return spiffeId; } public void setSpiffeId(String spiffeId) { this.spiffeId = spiffeId; } public String getSocketPath() { return socketPath; } public void setSocketPath(String socketPath) { this.socketPath = socketPath; } public long getRefreshIntervalMs() { return refreshIntervalMs; } public void setRefreshIntervalMs(long refreshIntervalMs) { this.refreshIntervalMs = refreshIntervalMs; } public long getExpiryThresholdMs() { return expiryThresholdMs; } public void setExpiryThresholdMs(long expiryThresholdMs) { this.expiryThresholdMs = expiryThresholdMs; } } // Spring Boot properties /* spiffe: spiffe-id: spiffe://example.org/service/api socket-path: /tmp/spire-agent/public/api.sock refresh-interval-ms: 300000 expiry-threshold-ms: 60000 */ SPIFFE-aware REST Controller
package com.spiffe.spring.controller; import org.springframework.web.bind.annotation.*; import org.springframework.http.ResponseEntity; @RestController @RequestMapping("/api") public class SecureApiController { @GetMapping("/secure-data") public ResponseEntity<SecureData> getSecureData( @RequestHeader(value = "x-spiffe-id", required = false) String clientSpiffeId) { if (clientSpiffeId == null) { return ResponseEntity.status(401).body(null); } try { SpiffeId spiffeId = new SpiffeId(clientSpiffeId); // Authorize based on SPIFFE ID if (!isAuthorized(spiffeId)) { return ResponseEntity.status(403).body(null); } SecureData data = fetchDataForSpiffeId(spiffeId); return ResponseEntity.ok(data); } catch (IllegalArgumentException e) { return ResponseEntity.status(400).body(null); } } private boolean isAuthorized(SpiffeId spiffeId) { // Implement authorization logic based on SPIFFE ID path return spiffeId.getPath().startsWith("/service/"); } private SecureData fetchDataForSpiffeId(SpiffeId spiffeId) { return new SecureData("Authorized data for: " + spiffeId); } public static class SecureData { private String data; public SecureData(String data) { this.data = data; } public String getData() { return data; } public void setData(String data) { this.data = data; } } } Advanced SPIFFE Patterns
JWT SVID Support
package com.spiffe.jwt; import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTVerificationException; import com.auth0.jwt.interfaces.DecodedJWT; import com.auth0.jwt.interfaces.JWTVerifier; import com.spiffe.core.SpiffeId; import java.security.interfaces.ECPrivateKey; import java.security.interfaces.ECPublicKey; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; import java.util.Date; public class JwtSvidManager { private final SvidManager x509SvidManager; public JwtSvidManager(SvidManager x509SvidManager) { this.x509SvidManager = x509SvidManager; } public String generateJwtSvid(String audience, long ttlSeconds) { try { SpiffeSvid x509Svid = x509SvidManager.getCurrentSvid(); SpiffeId spiffeId = x509Svid.getSpiffeId(); // Create JWT with SPIFFE claims Algorithm algorithm = createSigningAlgorithm(x509Svid); return JWT.create() .withIssuer(spiffeId.toString()) .withSubject(spiffeId.toString()) .withAudience(audience) .withIssuedAt(new Date()) .withExpiresAt(new Date(System.currentTimeMillis() + ttlSeconds * 1000)) .withClaim("spiffe", true) .sign(algorithm); } catch (Exception e) { throw new RuntimeException("Failed to generate JWT SVID", e); } } public boolean verifyJwtSvid(String jwtToken, String expectedAudience) { try { DecodedJWT decodedJWT = JWT.decode(jwtToken); SpiffeId issuer = new SpiffeId(decodedJWT.getIssuer()); // In production, fetch public key from SPIFFE bundle Algorithm algorithm = createVerificationAlgorithm(issuer); JWTVerifier verifier = JWT.require(algorithm) .withIssuer(issuer.toString()) .withAudience(expectedAudience) .build(); verifier.verify(jwtToken); return true; } catch (JWTVerificationException e) { return false; } } public SpiffeId extractSpiffeIdFromJwt(String jwtToken) { try { DecodedJWT decodedJWT = JWT.decode(jwtToken); return new SpiffeId(decodedJWT.getIssuer()); } catch (Exception e) { throw new IllegalArgumentException("Invalid JWT token", e); } } private Algorithm createSigningAlgorithm(SpiffeSvid svid) { // Implementation depends on the private key type in the SVID // This is a simplified example return Algorithm.HMAC256("secret"); // In production, use proper key } private Algorithm createVerificationAlgorithm(SpiffeId issuer) { // In production, fetch the public key from the SPIFFE bundle // for the given trust domain return Algorithm.HMAC256("secret"); // In production, use proper key } } SPIFFE-aware HTTP Client
package com.spiffe.http; import org.springframework.http.HttpRequest; import org.springframework.http.client.ClientHttpRequestExecution; import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.http.client.ClientHttpResponse; import com.spiffe.core.SpiffeSvid; import com.spiffe.management.SvidManager; import java.io.IOException; import java.security.cert.X509Certificate; public class SpiffeHttpInterceptor implements ClientHttpRequestInterceptor { private final SvidManager svidManager; public SpiffeHttpInterceptor(SvidManager svidManager) { this.svidManager = svidManager; } @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { try { SpiffeSvid svid = svidManager.getCurrentSvid(); // Add SPIFFE headers request.getHeaders().add("x-spiffe-id", svid.getSpiffeId().toString()); // Add certificate chain int certIndex = 0; for (X509Certificate cert : svid.getCertificateChain()) { request.getHeaders().add("x-spiffe-cert-" + certIndex, cert.toString()); certIndex++; } } catch (Exception e) { System.err.println("Failed to add SPIFFE headers: " + e.getMessage()); } return execution.execute(request, body); } } // Configuration for RestTemplate with SPIFFE support @Configuration public class SpiffeRestTemplateConfig { @Bean public RestTemplate spiffeRestTemplate(SvidManager svidManager) { RestTemplate restTemplate = new RestTemplate(); List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>(restTemplate.getInterceptors()); interceptors.add(new SpiffeHttpInterceptor(svidManager)); restTemplate.setInterceptors(interceptors); return restTemplate; } } Security and Validation
SPIFFE Identity Validator
package com.spiffe.validation; import com.spiffe.core.SpiffeId; import java.util.Set; import java.util.regex.Pattern; public class SpiffeValidator { private static final Pattern SPIFFE_ID_PATTERN = Pattern.compile("^spiffe://[a-zA-Z0-9.-]+(/[a-zA-Z0-9._~-]+)*$"); private final Set<String> allowedTrustDomains; private final Set<String> allowedPathPrefixes; public SpiffeValidator(Set<String> allowedTrustDomains, Set<String> allowedPathPrefixes) { this.allowedTrustDomains = Set.copyOf(allowedTrustDomains); this.allowedPathPrefixes = Set.copyOf(allowedPathPrefixes); } public ValidationResult validateSpiffeId(String spiffeIdStr) { try { SpiffeId spiffeId = new SpiffeId(spiffeIdStr); return validateSpiffeId(spiffeId); } catch (IllegalArgumentException e) { return ValidationResult.invalid("Invalid SPIFFE ID format: " + e.getMessage()); } } public ValidationResult validateSpiffeId(SpiffeId spiffeId) { // Validate trust domain if (!allowedTrustDomains.contains(spiffeId.getTrustDomain())) { return ValidationResult.invalid( "Untrusted trust domain: " + spiffeId.getTrustDomain()); } // Validate path if (!isPathAllowed(spiffeId.getPath())) { return ValidationResult.invalid( "Path not allowed: " + spiffeId.getPath()); } return ValidationResult.valid(); } private boolean isPathAllowed(String path) { if (allowedPathPrefixes.isEmpty()) { return true; // No path restrictions } return allowedPathPrefixes.stream() .anyMatch(prefix -> path.startsWith(prefix)); } public static class ValidationResult { private final boolean valid; private final String errorMessage; private ValidationResult(boolean valid, String errorMessage) { this.valid = valid; this.errorMessage = errorMessage; } public static ValidationResult valid() { return new ValidationResult(true, null); } public static ValidationResult invalid(String errorMessage) { return new ValidationResult(false, errorMessage); } public boolean isValid() { return valid; } public String getErrorMessage() { return errorMessage; } } } Testing and Mock SPIRE
Mock Workload API for Testing
package com.spiffe.test; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; import com.spiffe.core.SpiffeId; import com.spiffe.core.SpiffeSvid; import com.spiffe.workload.WorkloadApiClient; import java.security.cert.X509Certificate; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; @Profile("test") @Component public class MockWorkloadApiClient extends WorkloadApiClient { private final ConcurrentHashMap<String, SpiffeSvid> mockSvids; private final AtomicLong certSerial; public MockWorkloadApiClient() { super("mock"); this.mockSvids = new ConcurrentHashMap<>(); this.certSerial = new AtomicLong(1); // Initialize with some test SVIDs initializeTestSvids(); } private void initializeTestSvids() { addMockSvid("spiffe://example.org/service/frontend"); addMockSvid("spiffe://example.org/service/backend"); addMockSvid("spiffe://example.org/service/database"); } public void addMockSvid(String spiffeId) { try { SpiffeSvid svid = createMockSvid(spiffeId); mockSvids.put(spiffeId, svid); } catch (Exception e) { throw new RuntimeException("Failed to create mock SVID", e); } } private SpiffeSvid createMockSvid(String spiffeIdStr) throws Exception { SpiffeId spiffeId = new SpiffeId(spiffeIdStr); // Create mock certificate (in production, use proper certificate generation) X509Certificate mockCert = createMockCertificate(spiffeId); List<X509Certificate> chain = List.of(mockCert); byte[] privateKey = "mock-private-key".getBytes(); long expiresAt = System.currentTimeMillis() + 3600000; // 1 hour return new SpiffeSvid(spiffeId, mockCert, chain, privateKey, expiresAt); } private X509Certificate createMockCertificate(SpiffeId spiffeId) { // Simplified mock certificate creation // In production tests, use proper certificate generation return null; // Implementation would go here } @Override public SpiffeSvid fetchX509Svid(String requestedSpiffeId) throws Exception { if (requestedSpiffeId == null && !mockSvids.isEmpty()) { return mockSvids.values().iterator().next(); } SpiffeSvid svid = mockSvids.get(requestedSpiffeId); if (svid == null) { throw new RuntimeException("No SVID found for: " + requestedSpiffeId); } return svid; } @Override public List<SpiffeSvid> fetchAllX509Svids() throws Exception { return List.copyOf(mockSvids.values()); } } // Test configuration @Configuration @Profile("test") public class TestSpiffeConfig { @Bean public WorkloadApiClient workloadApiClient() { return new MockWorkloadApiClient(); } @Bean public SvidManager svidManager(WorkloadApiClient workloadApiClient) { SvidManager manager = new SvidManager( "spiffe://example.org/service/test", workloadApiClient ); manager.start(); return manager; } } Monitoring and Observability
SPIFFE Metrics and Health Checks
package com.spiffe.monitoring; import io.micrometer.core.instrument.Gauge; import io.micrometer.core.instrument.MeterRegistry; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.stereotype.Component; import com.spiffe.management.SvidManager; import java.util.concurrent.atomic.AtomicLong; @Component public class SpiffeHealthIndicator implements HealthIndicator { private final SvidManager svidManager; private final AtomicLong lastRefreshTime; public SpiffeHealthIndicator(SvidManager svidManager) { this.svidManager = svidManager; this.lastRefreshTime = new AtomicLong(System.currentTimeMillis()); } @Override public Health health() { if (!svidManager.isHealthy()) { return Health.down() .withDetail("reason", "SVID expired or not available") .build(); } long timeSinceLastRefresh = System.currentTimeMillis() - lastRefreshTime.get(); if (timeSinceLastRefresh > 600000) { // 10 minutes return Health.down() .withDetail("reason", "SVID refresh stalled") .withDetail("lastRefreshMs", timeSinceLastRefresh) .build(); } return Health.up() .withDetail("spiffeId", svidManager.getCurrentSvid().getSpiffeId().toString()) .build(); } public void recordRefresh() { lastRefreshTime.set(System.currentTimeMillis()); } } @Component public class SpiffeMetrics { private final SvidManager svidManager; private final AtomicLong svidExpiryTime; public SpiffeMetrics(SvidManager svidManager, MeterRegistry meterRegistry) { this.svidManager = svidManager; this.svidExpiryTime = new AtomicLong(); // Register metrics Gauge.builder("spiffe.svid.expiry_timestamp", svidExpiryTime::get) .description("Timestamp when current SVID expires") .register(meterRegistry); Gauge.builder("spiffe.svid.healthy", () -> svidManager.isHealthy() ? 1 : 0) .description("SPIFFE SVID health status") .register(meterRegistry); } public void updateMetrics() { try { SpiffeSvid svid = svidManager.getCurrentSvid(); svidExpiryTime.set(svid.getExpiresAt()); } catch (Exception e) { svidExpiryTime.set(0); } } } This comprehensive SPIFFE/SPIRE integration for Java provides:
- Core SPIFFE Concepts - SPIFFE ID and SVID management
- Workload API Integration - Communication with SPIRE agent
- Automatic SVID Management - Renewal and lifecycle management
- gRPC Integration - Client and server interceptors
- Spring Boot Support - Auto-configuration and properties
- JWT SVID Support - JWT-based service authentication
- HTTP Client Integration - REST template with SPIFFE headers
- Security Validation - SPIFFE ID validation and authorization
- Testing Support - Mock implementations for development
- Monitoring - Health checks and metrics
The implementation follows SPIFFE/SPIRE standards and provides a robust foundation for implementing secure service identity in distributed Java applications.