A SPIRE integration service that bridges non-SPIFFE workload identity to the SPIRE ecosystem — enabling platform-native tokens (GitHub Actions OIDC, GitLab CI/CD OIDC, Kubernetes service account tokens, and SPIFFE SVIDs from other trust domains) to be exchanged for SPIRE-issued SVIDs via the SPIRE Agent's Delegated Identity API.
This code is in an early and experimental stage of development. Please do not use it in production yet. Please consider testing it out, providing feedback, and possibly submitting fixes.
Workloads in CI/CD pipelines, Kubernetes clusters, and other platforms already possess platform-native identity tokens (OIDC tokens, service account tokens). The SPIRE agent model typically requires a SPIRE agent on every host, plus workload registration entries keyed by process-level selectors.
spire-identity-exchange bridges these worlds: it accepts platform-native tokens from workloads, validates them using pluggable authentication plugins, generates SPIRE selectors from the validated claims, and calls the SPIRE Agent's Delegated Identity API to mint SVIDs. The SPIRE Agent handles SVID issuance against its existing registration entries — no dynamic registration, no agent per workload host.
Key differences from the agent-per-host model:
- Workloads authenticate with tokens they already possess — no SPIRE agent binary, socket, or join token required.
- SVID issuance is governed by SPIRE registration entries matching the generated selectors, not by server-side SPIFFE ID templates.
- Selectors carry rich claim data (repository, workflow, namespace, SA, trust domain, etc.) enabling fine-grained authorization in SPIRE registration entries.
More details can be found in our talk at KubeCon + SecurityCon 2025: From Adoption to Innovation: LinkedIn's SPIRE Journey
┌──── Identity Providers (pluggable) ────┐
│ GitHub Actions │ GitLab CI/CD │
│ K8s PSAT │ SPIFFE SVID JWT │
└────────────────────┬───────────────────┘
│ token (+ optional CSR)
│
▼
┌──────────────────────────────────────────┐
│ spire-identity-exchange │
│ │
│ 1. check replay cache │
│ 2. validate token via plugin │
│ 3. generate SPIRE selectors from claims│
│ 4. call Delegated Identity API │
└────────────────────┬─────────────────────┘
│ selectors
│ (Unix socket)
▼
┌──────────────────────────┐
│ SPIRE Agent │
│ Delegated Identity API │
│ │
│ match selectors against │
│ registration entries → │
│ mint SVID │
└────────────┬──────────────┘
│ SVID
▼
┌──────────────────────────────────────────┐
│ spire-identity-exchange │
│ (return to workload) │
└──────────────────────────────────────────┘
│
▼
Workload (X.509 or JWT SVID)
Systemd:
Kubernetes:
spire-identity-exchange connects to the SPIRE Agent over a Unix domain socket using the Delegated Identity API. The agent matches the generated selectors against its registration entries and returns the SVID. No direct communication with the SPIRE Server is needed — the agent handles all SVID issuance.
For multi-instance deployments, the replay cache should be backed by a shared central store (e.g. Redis) so that a token cannot be replayed against a different instance.
spire-identity-exchange supports one primary deployment topology that connects to the SPIRE Agent's Delegated Identity API.
A legacy deployment mode (co-located with SPIRE Server, using SPIFFE ID templates) is available via the -tags legacy build flag — see the Legacy mode section for details.
spire-identity-exchange runs on a host (physical, VM, or container) that also runs a SPIRE Agent. It connects to the SPIRE Agent over a Unix domain socket using the Delegated Identity API.
- A SPIRE agent runs on the same host as spire-identity-exchange (or is reachable via a mounted Unix socket).
- The SPIRE agent has registration entries with selectors matching the claims that spire-identity-exchange's plugins generate.
- A workload sends its platform-native token to spire-identity-exchange.
- spire-identity-exchange validates the token, generates SPIRE selectors from the claims, and calls the SPIRE Agent's Delegated Identity API.
- The agent matches the selectors against its registration entries and mints an SVID.
The SPIRE agent handles all SVID issuance — spire-identity-exchange never contacts the SPIRE Server directly. This means:
- SPIFFE IDs are determined by SPIRE registration entries, not by spire-identity-exchange configuration.
- SVID revocation and rotation follow the agent's standard lifecycle.
- spire-identity-exchange's privilege is scoped by the agent's
authorized_delegatesconfiguration.
Available with the
-tags legacybuild flag.
SPIRE server co-location mode is the original deployment model, now maintained as a legacy path. spire-identity-exchange runs on the same host as the SPIRE server and connects via Unix domain socket, using SPIFFE ID templates to derive workload identities from token claims. See the build section for compilation instructions.
spire-identity-exchange supports pluggable authentication via a dynamic plugin system. Each plugin validates a specific token type and generates SPIRE selectors. The following plugins are built in:
| Plugin | Token type | Selector type | Configuration reference |
|---|---|---|---|
GitHub Actions OIDC (github) |
GitHub Actions OIDC JWTs via JWKS | github_actions |
plugin_github.md |
GitLab CI/CD OIDC (gitlab) |
GitLab CI/CD OIDC JWTs via JWKS | gitlab_ci |
plugin_gitlab.md |
Kubernetes PSAT (k8s_psat) |
K8s projected SA tokens via JWKS + TokenReview | k8s_psat |
plugin_k8s_psat.md |
SPIFFE SVID JWT (spiffe) |
Incoming SPIFFE SVID JWTs via JWKS | spiffe |
plugin_spiffe.md |
Additional authentication methods can be added by implementing the validator.TokenValidatorLoaderGenerator interface (see pkg/validator/).
- A running SPIRE Agent with the Delegated Identity API enabled (
authorized_delegatesconfigured) - A running SPIRE Server that the agent connects to
- SPIRE registration entries with selectors matching the token claims
- Go 1.24+
The default build produces the agent-based binary:
make build
# binaries under build/<os>/<arch>/ (e.g. build/linux/amd64/spire-identity-exchange-server)For the legacy server-based workflow (SPIFFE ID templates, direct SPIRE Server API):
make build spire_identity_exchange_server_TAGS=legacyspire-identity-exchange is configured via a JSON or YAML file passed with --config.
export SPIFFE_TRUST_DOMAIN=example.org
source config/defualt.env
build/linux/amd64/spire-identity-exchange-server --config config/default.conf -expand-envAuthentication methods are configured via the auth.plugins array. Each entry specifies a user-defined name, a plugin type, and plugin-specific configuration.
At least one plugin must be configured in auth.plugins.
The purposeMode setting controls replay cache scoping:
"shared"(default) — a token can be used exactly once for any SVID type."purpose"— a token can be used once per SVID type (one X.509 + one JWT per token).
The legacy build (-tags legacy) uses top-level githubOIDC and k8sSAToken configuration blocks instead of auth.plugins. See the Legacy mode section.
In the default (agent-based) mode, spire-identity-exchange does not derive SPIFFE IDs from token claims. Instead, each plugin generates SPIRE selectors from the validated claims, and SVID issuance is driven by SPIRE registration entries that match those selectors. This means SPIFFE IDs are entirely managed in SPIRE registration entries — no Go templates required.
The legacy mode (-tags legacy) uses SPIFFE ID templates to derive workload identities. For a full security reference on template-based SPIFFE ID derivation, see docs/spiffe-id-template-guide.md and docs/spiffe-id-token-reference.md.
An automated end-to-end test script is available at scripts/e2e-local.sh. It starts a SPIRE server and agent, configures the exchange, runs a mock GitHub OIDC server, and exercises the gRPC MintCertificate flow end-to-end.
Prerequisites: SPIRE (server + agent) running locally with the Delegated Identity API enabled, grpcurl.
1. Generate TLS certs for the gRPC/HTTP server (one-time):
mkdir -p certs
openssl req -x509 -newkey rsa:4096 \
-keyout certs/server.key -out certs/server.crt \
-sha256 -days 365 -nodes \
-subj "/CN=localhost" \
-addext "subjectAltName=IP:127.0.0.1,DNS:localhost"2. Start the mock OIDC server (in a separate terminal):
go run ./examples/mock-github-oidc --audience spire-identity-exchange3. Start spire-identity-exchange (in a separate terminal):
make build
build/linux/amd64/spire-identity-exchange-server --config config/legacy/config.example-local.json4. Mint a certificate via the PluginAuth gRPC API:
export GITHUB_TOKEN="<token from mock server output>"
grpcurl -insecure \
-d '{
"pluginAuthList": {
"plugins": [
{
"pluginName": "github-actions",
"token": "'"${GITHUB_TOKEN}"'"
}
]
},
"serverKeyGenRequest": {}
}' \
localhost:8443 \
proto.spiffe.spireidentityexchange.SpireIdentityExchangeApi/MintCertificate5. Mint a certificate via the REST API:
export GITHUB_TOKEN="<token from mock server output>"
curl -k -X POST https://localhost:8444/api/v1/svid/github-actions/x509 \
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
-H "Content-Type: application/json"6. Mint a JWT-SVID via the REST API:
export GITHUB_TOKEN="<token from mock server output>"
curl -k -X POST https://localhost:8444/api/v1/svid/github-actions/jwt \
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"audiences": ["my-workload-audience"]}'# List gRPC services
grpcurl -insecure localhost:8443 list
# Describe the API
grpcurl -insecure localhost:8443 describe proto.spiffe.spireidentityexchange.SpireIdentityExchangeApi
# Fetch the trust bundle via REST
curl -k https://localhost:8444/api/v1/trustbundle/x509spire-identity-exchange exposes Prometheus metrics on metricsPort (default 9090):
curl http://localhost:9090/metricsKey metrics:
| Metric | Description |
|---|---|
pki_spire_identity_exchange_operation_duration_seconds |
Latency histogram per operation (validate_token, fetch_jwks, mint_certificate_by_plugin, mint_certificate_by_github_oidc, mint_certificate_by_k8s_sa). Labels: component, plugin, operation, status. |
pki_spire_identity_exchange_operation_count_total |
Request count, same labels and operations as above. |
Standard Go runtime and process metrics are also exported with the pki_spire_identity_exchange_ prefix (e.g. pki_spire_identity_exchange_go_goroutines, pki_spire_identity_exchange_process_cpu_seconds_total).
api/ # Protobuf definitions and generated Go code
cmd/
spire-identity-exchange-server/ # Main server binary (entry point + wiring)
spire-credentialcomposer-identity-exchange/ # SPIRE CredentialComposer plugin
spire-server-attestor-spiffe-workload-api/ # Trust bundle HTTP server
docs/ # Documentation (plugin reference, SPIFFE ID guides)
config/ # Example configuration files
pkg/
validator/ # Plugin system
interface.go # Core interfaces (TokenValidator, SelectorGenerator)
purpose.go # Purpose / PurposeResolver for replay cache
claims.go / jwtclaims.go # Claim types
allowlist.go # Wildcard suffix matching
registry/
registry.go # AllBuiltinPlugins map
jwt/ # Generic JWT validator + key provider (base for github, gitlab, spiffe)
github/ # GitHub Actions OIDC plugin
gitlab/ # GitLab CI/CD OIDC plugin
k8s/ # Kubernetes PSAT plugin
spiffe/ # SPIFFE SVID JWT plugin
internal/
config/ # Configuration structs and validation
const/ # Shared constants (claim names, metric labels)
cache/ # Replay detection (ReplayCache, ReplayCheckingValidator)
github-oidc/ # Legacy GitHub Actions OIDC validator
k8s-sa-token/ # Legacy Kubernetes SA token validator
metrics/ # Metrics interface and Prometheus implementation
service/ # gRPC server, request dispatch, certificate minting
spireagent/delegated/ # Delegated Identity API client
utils/ # JWT claim helpers, certificate utilities
k8s/ # Kubernetes manifests
examples/ # Example servers (mock OIDC)
scripts/ # Automation (e2e-local.sh)
Contributions are welcome. Please open an issue or pull request. When adding a new authentication method:
- Implement
validator.TokenValidatorLoaderGeneratorin a newpkg/validator/<method>/package (see existing plugins for reference) - Register it in
pkg/validator/registry/registry.go


{ "name": "spire-identity-exchange", "logLevel": "info", "server": { "grpcPort": 8443, // gRPC server port "metricsPort": 9090, // Prometheus metrics port "restPort": 8444, // Optional HTTP REST port (see REST API below) "tls": { "certFile": "certs/server.crt", "keyFile": "certs/server.key" } }, "spire": { "agentDelegatedSocketPath": "/tmp/spire-agent/admin/api.sock", "agentWorkloadSocketPath": "/tmp/spire-agent/public/api.sock", "trustDomain": "example.org", "svidTTL": "5m" }, "auth": { "plugins": [ { "name": "github-actions", "plugin": "github", "config": { "issuerURL": "https://token.actions.githubusercontent.com", "audiences": ["spire-identity-exchange"], "allowedRepositoryOwners": ["my-org"] } }, { "name": "kubernetes-prod", "plugin": "k8s_psat", "config": { "clusterName": "prod-us-east", "audiences": ["spire-identity-exchange"], "allowedNamespaces": ["prod-*"], "allowedServiceAccounts": ["prod-*/app"] } } ] } }