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.