SPIFFE Client
experimentalSPIFFE (Secure Production Identity Framework for Everyone) provides cryptographic workload identities in zero-trust environments. The SPIFFE client extension uses the Workload API to retrieve JWT-SVIDs and X.509-SVIDs for Quarkus workloads directly from the local SPIRE Agent. It simplifies deployment by eliminating the need for a SPIFFE client sidecar that stores SVIDs on a mounted file path for Quarkus to read.
|
This technology is considered experimental. In experimental mode, early feedback is requested to mature the idea. There is no guarantee of stability nor long term presence in the platform until the solution matures. Feedback is welcome on our mailing list or as issues in our GitHub issue tracker. For a full list of possible statuses, check our FAQ entry. |
Adding the extension
Add the quarkus-spiffe-client extension to your project:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-spiffe-client</artifactId>
</dependency>
implementation("io.quarkus:quarkus-spiffe-client")
Configuration
The extension requires the URI of the SPIRE Agent’s Workload Endpoint socket:
quarkus.spiffe-client.endpoint-socket=unix:///run/spire/sockets/agent.sock
If quarkus.spiffe-client.endpoint-socket is not set, the extension falls back to the standard SPIFFE_ENDPOINT_SOCKET environment variable as defined by the SPIFFE Workload Endpoint specification.
This environment variable is typically set by the SPIRE Agent for workloads it manages.
The URI must use one of the following schemes, as defined by the SPIFFE Workload Endpoint specification:
-
unix://for Unix Domain Sockets (production default), for exampleunix:///run/spire/sockets/agent.sock -
tcp://for TCP connections (development and testing), for exampletcp://127.0.0.1:8080
For the tcp scheme, the host must be an IP address and a port is required.
Hostnames are not accepted.
On Windows, only the tcp:// scheme is currently supported.
Retrieve JWT-SVID
Inject the SpiffeClient CDI bean to retrieve JWT-SVIDs from the SPIRE Agent.
The following example retrieves a single JWT-SVID for one audience:
import io.quarkus.spiffe.client.WorkloadJsonWebToken;
import io.quarkus.spiffe.client.SpiffeClient;
@ApplicationScoped
public class MyService {
@Inject
SpiffeClient spiffeClient;
public Uni<String> getToken() {
return spiffeClient.getWorkloadJsonWebToken("https://my-audience")
.map(WorkloadJsonWebToken::token);
}
}
To specify multiple audiences:
import java.util.Set;
import io.quarkus.spiffe.client.WorkloadJsonWebToken;
import io.quarkus.spiffe.client.SpiffeClient;
import io.smallrye.mutiny.Uni;
public Uni<String> getWorkloadIdentity() {
return spiffeClient.getWorkloadJsonWebToken(Set.of("https://audience-a", "https://audience-b"))
.map(WorkloadJsonWebToken::subject); (1)
}
| 1 | The subject() method returns the JWT sub claim, which is a valid SPIFFE ID, for example spiffe://example.org/myservice. |
Calls are not retried automatically; implement your own retry logic if your use case requires it.
Default audiences
Pre-configure default audiences with quarkus.spiffe-client.audiences so callers do not need to specify them on every request:
quarkus.spiffe-client.audiences=https://keycloak.example.com,https://mcp-server.example.com
return spiffeClient.getWorkloadJsonWebToken().map(WorkloadJsonWebToken::token);
When explicit audiences are provided, they take precedence over the configured defaults:
return spiffeClient.getWorkloadJsonWebToken("https://other-service.example.com").map(WorkloadJsonWebToken::token);
OIDC SPIFFE Client Authentication
When this extension is combined with either quarkus-oidc or quarkus-oidc-client extensions, JWT-SVIDs can be supplied automatically and used as OIDC client authentication assertions.
Please see the OIDC provider client authentication section of the OIDC code flow authentication guide and the OIDC Client authentication section of the OIDC client guide for more information.
Retrieve X.509-SVID
X.509-SVID is an X.509 workload certificate document which includes a client certificate chain and private key, as well as a server trust bundle.
To retrieve an X.509-SVID from the SPIFFE Agent, inject the SpiffeClient CDI bean:
Quarkus REST Client
The following example shows how to configure a Quarkus REST client with a retrieved X.509-SVID:
import java.net.URI;
import jakarta.enterprise.context.RequestScoped;
import jakarta.enterprise.inject.Produces;
import jakarta.inject.Inject;
import io.quarkus.rest.client.reactive.QuarkusRestClientBuilder;
import io.quarkus.spiffe.client.SpiffeClient;
import io.quarkus.spiffe.client.WorkloadCertificateDocument;
import io.quarkus.tls.BaseTlsConfiguration;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.net.KeyCertOptions;
import io.vertx.core.net.PemKeyCertOptions;
import io.vertx.core.net.PemTrustOptions;
import io.vertx.core.net.TrustOptions;
@RequestScoped
class MyServiceClientProducer {
@Inject
SpiffeClient spiffeClient;
@Produces
@ApplicationScoped
MyServiceClient createClient() { (1)
WorkloadCertificateDocument cert = spiffeClient.getWorkloadCertificate()
.await().indefinitely();
return QuarkusRestClientBuilder.newBuilder()
.baseUri(URI.create("https://my-service:8443"))
.tlsConfiguration(new SpiffeTlsConfiguration(cert))
.build(MyServiceClient.class);
}
private static final class SpiffeTlsConfiguration extends BaseTlsConfiguration {
private final KeyCertOptions keyCertOptions;
private final TrustOptions trustOptions;
private SpiffeTlsConfiguration(WorkloadCertificateDocument cert) {
var keyCert = new PemKeyCertOptions(); (2)
for (String pem : cert.certificateChain().chainPem()) {
keyCert.addCertValue(Buffer.buffer(pem));
}
keyCert.addKeyValue(Buffer.buffer(cert.certificateChain().privateKeyPem()));
this.keyCertOptions = keyCert;
var trust = new PemTrustOptions(); (3)
for (String pem : cert.trustBundle().certificatesPem()) {
trust.addCertValue(Buffer.buffer(pem));
}
this.trustOptions = trust;
}
@Override
public KeyCertOptions getKeyStoreOptions() {
return keyCertOptions;
}
@Override
public TrustOptions getTrustStoreOptions() {
return trustOptions;
}
}
}
| 1 | Creates a REST client per request, fetching a fresh X.509-SVID from the SPIFFE Workload API. In practice, clients should consider caching SVIDs until close to expiration. |
| 2 | Builds PEM key-cert options from the SPIFFE certificate chain and private key. |
| 3 | Builds PEM trust options from the trust bundle CA certificates. The server certificate must be issued by a CA in the SPIFFE trust bundle for the TLS handshake to succeed. |
When the server also presents an X.509-SVID, standard hostname verification must be disabled because SPIFFE conveys workload identity in the URI SAN, not the DNS SAN.
In that case, the X.509-SVID specification requires additional leaf certificate validation — verifying the URI SAN contains a valid SPIFFE ID, that basicConstraints has cA=false, and that keyUsage includes digitalSignature but not keyCertSign.
SPIFFE-aware trust validation via TLS registry integration will be provided in a future release.
|
Testing
In dev and test modes, the extension provides a Dev Service that emulates the SPIFFE Workload API with a pre-configured identity and a locally generated key pair.
While it does not support custom authorization policies or registration entries, it is sufficient for verifying that your application correctly retrieves and uses JWT-SVIDs without requiring external SPIRE infrastructure.
You can choose the transport protocol with the quarkus.spiffe-client.devservices.transport property, which accepts unix (default) or tcp.
Testing SPIFFE client authentication with Keycloak
To test OIDC SPIFFE client authentication against Keycloak Dev Services, you need to configure a Keycloak realm with a SPIFFE identity provider and a federated-jwt client.
You can automate this setup by creating a custom test resource using KeycloakTestClient. This ensures the realm is ready before your tests execute.
For example, you can create a test realm like this:
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import io.quarkus.test.common.DevServicesContext;
import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
import io.quarkus.test.keycloak.client.KeycloakTestClient;
public class SpiffeKeycloakTestResource implements QuarkusTestResourceLifecycleManager, DevServicesContext.ContextAware {
private final KeycloakTestClient client = new KeycloakTestClient();
@Override
public void setIntegrationTestContext(DevServicesContext context) {
client.setIntegrationTestContext(context);
}
@Override
public Map<String, String> start() {
RealmRepresentation realm = new RealmRepresentation();
realm.setRealm("quarkus-spiffe");
realm.setEnabled(true);
realm.setUsers(new ArrayList<>());
realm.setClients(new ArrayList<>());
IdentityProviderRepresentation idp = new IdentityProviderRepresentation();
idp.setProviderId("spiffe");
idp.setAlias("spiffe-idp");
idp.setEnabled(true);
idp.setConfig(Map.of(
"trustDomain", "spiffe://test.quarkus.io",
"bundleEndpoint", "http://host.testcontainers.internal:18443/bundle"));
realm.addIdentityProvider(idp);
ClientRepresentation spiffeClient = new ClientRepresentation();
spiffeClient.setClientId("my-app");
spiffeClient.setPublicClient(false);
spiffeClient.setServiceAccountsEnabled(true);
spiffeClient.setStandardFlowEnabled(true);
spiffeClient.setClientAuthenticatorType("federated-jwt");
spiffeClient.setRedirectUris(List.of("*"));
spiffeClient.setEnabled(true);
spiffeClient.setAttributes(Map.of(
"jwt.credential.issuer", "spiffe-idp",
"jwt.credential.sub", "spiffe://test.quarkus.io/test-workload"));
realm.getClients().add(spiffeClient);
client.createRealm(realm);
return Map.of();
}
@Override
public void stop() {
}
}
Next, adjust your application configuration to enable the SPIFFE feature in Keycloak Dev Services.
The SPIFFE Dev Service bundle endpoint listens on port 18443 by default, and Keycloak must be able to reach it:
quarkus.keycloak.devservices.host-accessible-ports=${quarkus.spiffe-client.devservices.http-port}
quarkus.keycloak.devservices.features=spiffe
quarkus.keycloak.devservices.realm-name=quarkus-spiffe
quarkus.keycloak.devservices.create-realm=false
Alternatively, if you already have an existing realm, export it to a JSON file and set the SPIFFE identity provider’s trustDomain to spiffe://test.quarkus.io and bundleEndpoint to http://host.testcontainers.internal:18443/bundle.
In your realm JSON file, add or update the SPIFFE identity provider entry in the identityProviders array:
"identityProviders": [
{
"alias": "spiffe-idp",
"providerId": "spiffe",
"enabled": true,
"config": {
"trustDomain": "spiffe://test.quarkus.io",
"bundleEndpoint": "http://host.testcontainers.internal:18443/bundle"
}
}
]
Then load the realm file with:
quarkus.keycloak.devservices.host-accessible-ports=${quarkus.spiffe-client.devservices.http-port}
quarkus.keycloak.devservices.features=spiffe
quarkus.keycloak.devservices.realm-path=quarkus-spiffe.json
Finally, apply this setup to your test class using the @QuarkusTestResource annotation:
import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;
@QuarkusTest
@QuarkusTestResource(SpiffeKeycloakTestResource.class)
class MySpiffeClientTest {
@Test
void testEndpointThatReliesOnSpiffeClientAuthentication() {
// Test application endpoint that relies on Keycloak SPIFFE client authentication
}
}
Extension integration
Extensions integrating with SpiffeClient should depend on the API module:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-spiffe-client-api</artifactId>
</dependency>
Configuration reference
Configuration property fixed at build time - All other configuration properties are overridable at runtime
Configuration property |
Type |
Default |
|---|---|---|
If SPIFFE Workload API client is enabled. Environment variable: Show more |
boolean |
|
Flag to enable (default) or disable Dev Services. Environment variable: Show more |
boolean |
|
Transport protocol for the SPIFFE Workload API server. Environment variable: Show more |
|
|
The port for the bundle HTTP server to listen on. Environment variable: Show more |
int |
|
SPIFFE Workload Endpoint socket URI. Supports If not set, the standard Example values: Environment variable: Show more |
|
|
Default audience values that a SPIFFE JSON Web Token (JWT-SVID) is required to contain in its audience Environment variable: Show more |
list of string |