SPIFFE Integration
Ahdapa can act as a SPIFFE trust domain authority and Workload API server.
When [spiffe] trust_domain is set in the configuration file, ahdapa:
- Generates or loads an ECDSA P-256 (or P-384) CA for the trust domain.
- Issues X.509-SVIDs and JWT-SVIDs to workloads that connect to the gRPC Workload API Unix socket.
- Publishes the trust bundle (CA cert + JWT signing keys) at
GET /.well-known/spiffe-bundle. - Bridges SPIFFE identity into OAuth2: workloads that present an X.509-SVID in an mTLS connection and whose SPIFFE ID matches a registered OAuth2 client receive an access token.
SPIFFE features are fully optional. When [spiffe] trust_domain is absent,
no SPIFFE code paths are activated.
Specification coverage
The table below maps Ahdapa against the SPIFFE implementation feature matrix.
| Feature | Status | Notes |
|---|---|---|
| X.509 SVID | ✔ Supported | ECDSA P-256/P-384 CA; fresh leaf keypair per SVID; correct cert profile (empty subject, critical URI SAN, cA=false, digitalSignature, serverAuth+clientAuth EKU). |
| JWT SVID | ✔ Supported | RS/PS/ES algorithm allowlist enforced per SPIFFE JWT-SVID spec; ML-DSA explicitly excluded. Issue, validate, and bundle endpoints all implemented. |
| Attestation-based issuance | ✔ Supported | SO_PEERCRED (pid/uid/gid) + /proc/<pid>/exe path + supplemental GIDs + IMA hash + hostname + IPA hostgroups. Fail-closed on unparseable selectors. Re-attests on every X.509-SVID refresh cycle for revocation detection. |
| Workload API | ✔ Supported | All five gRPC methods (FetchX509SVID, FetchJWTSVID, ValidateJWTSVID, FetchX509Bundles, FetchJWTBundles) on a Unix domain socket. ahdapa-spiffe-proxy extends coverage to IPA-enrolled hosts that do not run a local Ahdapa node. |
| PKI integration | ✔ Supported | Auto-generated software CA (CRDT-distributed, AES-256-GCM encrypted key), external PEM key + cert files, or PKCS#11 HSM-backed key. |
| VM / bare metal | ✔ Supported | Linux-native via /proc; IPA/LDAP hostgroup integration for host-based attestation. |
| SDS API | ✗ Not implemented | The Envoy Secret Discovery Service API is not implemented. Envoy integration requires the Workload API via the proxy or a bridge such as SPIRE agent. |
| SPIFFE Federation | ✔ Supported | Cross-trust-domain bundle distribution via https_web (WebPKI TLS) and https_spiffe (X.509-SVID authenticated) bundle endpoint profiles. Federation relationships are managed via the admin API (GET/POST/DELETE /api/admin/spiffe/federation). Cached foreign bundles are CRDT-gossiped across cluster nodes and served in FetchX509SVIDResponse.federated_bundles, FetchX509BundlesResponse, and FetchJWTBundlesResponse. Bundle refresh is triggered manually; automatic periodic polling is a planned follow-up. |
| OIDC Federation | ✔ Supported | GET /spiffe/oidc/openid-configuration and GET /spiffe/oidc/jwks when [spiffe] oidc_issuer is set. Only RS/PS/ES signing keys are advertised (ML-DSA excluded per SPIFFE JWT-SVID spec). |
| Kubernetes | ✔ Supported (cgroup-based) | Pod UID, container ID, and QoS class selectors (K8sPodUid, K8sContainerId, K8sQosClass) via /proc/<pid>/cgroup parsing (cgroup v1 and v2). Namespace, pod name, and service account selectors are not implemented — they require the kubelet API and are deferred to a follow-up. |
| Serverless | N/A | Not applicable to this deployment model. |
Quick start
Add a [spiffe] section to the configuration file:
[spiffe]
trust_domain = "example.org"
workload_socket = "/run/spiffe/workload.sock"
svid_ttl_seconds = 3600
ca_ttl_days = 365
On first startup ahdapa generates a fresh ECDSA P-256 CA for the trust domain, stores the encrypted private key in the CRDT (so all cluster nodes share one CA), and begins serving the Workload API on the Unix socket.
Workload registration
Before a workload can receive an SVID, a registration entry must exist in the
CRDT that matches the workload’s Unix identity. Registration entries are
managed via the admin API (/api/admin/spiffe/entries) and the SPIFFE
Entries page in the management WebUI. See
Admin API — SPIFFE Workload Entries
for the full endpoint reference. Each entry contains:
| Field | Type | Description |
|---|---|---|
id | UUID string | Unique identifier for this entry. |
spiffe_id | SPIFFE ID URI | SPIFFE ID to issue to matching workloads, e.g. spiffe://example.org/workload/myapp. |
selectors | list of strings | Workload attestation selectors stored as JSON objects — see below. |
node_constraint | string or null | Restrict this entry to a specific cluster node ID. null means any node. |
ttl_seconds | integer | SVID TTL override. 0 uses the global [spiffe] svid_ttl_seconds default. |
A workload must match all selectors in an entry to receive that entry’s SPIFFE ID.
Selector types
Selectors are stored as JSON objects with a "type" tag and a "value" field.
Eleven selector types are supported:
| Type | Value | Attestation source | Match condition |
|---|---|---|---|
Uid | u32 | SO_PEERCRED on the Unix socket | Caller’s Unix UID equals the value |
Gid | u32 | SO_PEERCRED on the Unix socket | Caller’s primary GID equals the value |
SupplementalGid | u32 | /proc/<pid>/status Groups: line | Caller’s supplemental group list contains the value |
Path | string | /proc/<pid>/exe symlink | Executable path equals the value (must start with /) |
Hostname | string | gethostname() at service init (local) or caller-declared (remote) | Machine hostname equals the value |
Hostgroup | string | IPA LDAP hostgroup lookup (server-verified) | Machine belongs to the named IPA host group |
ImaHash | string | Hash of the executable binary, computed at accept time | Hash of the running executable matches; value format: "alg:hexdigest" (supported algorithms: sha256, sha512, sha1) |
NodeId | string | Ahdapa node identity | The Workload API request originates from the named Ahdapa node |
K8sPodUid | string | /proc/<pid>/cgroup cgroup path | Pod UID from the pod<UUID> cgroup path component |
K8sContainerId | string | /proc/<pid>/cgroup cgroup path | Container ID from the last cgroup path component (runtime prefix stripped) |
K8sQosClass | string | /proc/<pid>/cgroup cgroup path | Kubernetes QoS class: "guaranteed", "burstable", or "besteffort" |
Important notes on remote attestation: When a remote caller uses POST /spiffe/jwt-svid with a hostname field, the server builds an AttestationContext with UID and GID set to u32::MAX sentinel values. This means Uid, Gid, SupplementalGid, and Path selectors will never match remote callers of that endpoint — only Hostname, Hostgroup, ImaHash (if ima_hash is also provided), and NodeId selectors can match via jwt-svid.
When using the ahdapa-spiffe-proxy daemon (see SPIFFE Workload Proxy below), the proxy reads the actual SO_PEERCRED, /proc/<pid>/exe, and /proc/<pid>/status from its own local socket and forwards the real values to POST /spiffe/issue-svid. All eight selector types — including Uid, Gid, SupplementalGid, and Path — can therefore match in the proxy path.
Example entry (JSON body for POST /api/admin/spiffe/entries):
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"spiffe_id": "spiffe://example.org/workload/myapp",
"selectors": [
"{\"type\":\"Uid\",\"value\":1000}",
"{\"type\":\"Path\",\"value\":\"/usr/bin/myapp\"}"
],
"node_constraint": null,
"ttl_seconds": 3600
}
The admin WebUI provides a SelectorBuilder widget that handles the JSON encoding automatically — use it to add selectors by type without writing raw JSON.
Selector JSON examples:
| Selector JSON | Meaning |
|---|---|
{"type":"Uid","value":1000} | Caller UID must be 1000 (local Unix socket only) |
{"type":"Gid","value":1000} | Caller primary GID must be 1000 (local Unix socket only) |
{"type":"SupplementalGid","value":5000} | Caller supplemental groups must include GID 5000 (local only) |
{"type":"Path","value":"/usr/bin/myapp"} | /proc/<pid>/exe must resolve to /usr/bin/myapp (local only) |
{"type":"Hostname","value":"web01.example.org"} | Machine hostname must be web01.example.org |
{"type":"Hostgroup","value":"web-servers"} | Machine must belong to the IPA host group web-servers (server-verified) |
{"type":"ImaHash","value":"sha256:deadbeef..."} | Executable SHA-256 hash must match (local: computed at accept time; remote: caller-declared) |
{"type":"NodeId","value":"node1.example.org"} | Request must arrive on the named Ahdapa node |
{"type":"K8sPodUid","value":"550e8400-e29b-41d4-a716-446655440000"} | Pod UID must match (Kubernetes cgroup attestation) |
{"type":"K8sContainerId","value":"abc123def456"} | Container ID must match (runtime prefix stripped) |
{"type":"K8sQosClass","value":"burstable"} | QoS class must match: guaranteed, burstable, or besteffort |
Kubernetes attestation
Ahdapa supports cgroup-based Kubernetes pod attestation with no additional
network calls or dependencies. When a workload connects to the Workload API
Unix socket from inside a Kubernetes pod, Ahdapa reads /proc/<pid>/cgroup
at accept time and parses the pod UID, container ID, and QoS class from the
cgroup path.
How it works
Both cgroup v1 and cgroup v2 are supported:
- cgroup v2 — a single line
0::/kubepods[.burstable|.besteffort]/pod<UUID>/<container-id>. - cgroup v1 — any line whose third
:field contains/kubepods.
The following information is extracted from the cgroup path:
| Field | Source | Example |
|---|---|---|
| Pod UID | pod<UUID> component | 550e8400-e29b-41d4-a716-446655440000 |
| Container ID | Last path component, runtime prefix (containerd-, docker-, crio-) stripped | abc123def456 |
| QoS class | First path component: kubepods → guaranteed; kubepods.burstable → burstable; kubepods.besteffort → besteffort | burstable |
On non-Kubernetes nodes the /proc/<pid>/cgroup read succeeds but the path
does not match the kubepods pattern; all three Kubernetes selectors are simply
left unset and never match. There is no configuration required and no overhead
on non-Kubernetes deployments.
Selector examples
A registration entry that matches a specific pod (by UID) and QoS class:
{
"id": "550e8400-e29b-41d4-a716-446655440001",
"spiffe_id": "spiffe://example.org/k8s/my-namespace/my-app",
"selectors": [
"{\"type\":\"K8sPodUid\",\"value\":\"550e8400-e29b-41d4-a716-446655440000\"}",
"{\"type\":\"K8sQosClass\",\"value\":\"burstable\"}"
],
"node_constraint": null,
"ttl_seconds": 3600
}
A registration entry that matches by container ID (useful when pods are ephemeral and the UID changes on each restart, but a specific container image has a known ID):
{
"id": "550e8400-e29b-41d4-a716-446655440002",
"spiffe_id": "spiffe://example.org/k8s/sidecar",
"selectors": [
"{\"type\":\"K8sContainerId\",\"value\":\"abc123def456\"}"
],
"node_constraint": null,
"ttl_seconds": 3600
}
What is not supported
The following Kubernetes-specific selectors are not yet implemented because they require calling the kubelet API (TLS configuration and network access to the node-local kubelet):
- Pod name
- Pod namespace
- Service account name
- Node name
These are tracked as a follow-up to the current cgroup-based implementation.
OIDC Federation
When [spiffe] oidc_issuer is set, Ahdapa exposes two OIDC endpoints that
allow external OIDC-aware systems to discover and verify JWT-SVIDs:
| Endpoint | Description |
|---|---|
GET /spiffe/oidc/openid-configuration | OIDC Discovery document for the SPIFFE trust domain. |
GET /spiffe/oidc/jwks | JWK Set containing the active JWT signing keys (RS/PS/ES only). |
[spiffe]
trust_domain = "example.org"
oidc_issuer = "https://spiffe.example.org"
The issuer field in the discovery document is set to the configured oidc_issuer
URL. The jwks_uri points to <server-issuer>/spiffe/oidc/jwks, where
<server-issuer> is [server] issuer in the Ahdapa configuration.
For strict SPIFFE OIDC Federation compliance (where the discovery document must be
at https://<trust-domain>/.well-known/openid-configuration), set up a reverse
proxy or DNS alias so that https://spiffe.example.org/.well-known/openid-configuration
redirects to the Ahdapa node.
Algorithm note: ML-DSA signing keys are excluded from the OIDC JWKS because the SPIFFE JWT-SVID specification only permits RS256/384/512, PS256/384/512, and ES256/384/512. The OIDC JWKS endpoint deliberately matches the SPIFFE allowlist so that JWT-SVIDs validated via OIDC Discovery behave identically to those validated via the Workload API.
SPIFFE Federation
SPIFFE Federation allows workloads in one trust domain to authenticate to workloads in another trust domain by sharing trust bundle material via signed bundle endpoints.
Configuring a federation relationship
Use the admin API to register a foreign trust domain’s bundle endpoint:
# https_web profile (WebPKI TLS — the endpoint's certificate is validated
# against the system trust roots or a configured CA bundle).
curl -s -X POST https://idp.example.org/api/admin/spiffe/federation \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"trust_domain": "partner.example.com",
"bundle_endpoint_url": "https://partner.example.com/spiffe-bundle",
"bundle_endpoint_profile": "https_web"
}'
# https_spiffe profile (the endpoint server presents an X.509-SVID;
# its SPIFFE ID must match endpoint_spiffe_id and its cert must chain
# to the cached foreign bundle — bootstrapped via https_web first).
curl -s -X POST https://idp.example.org/api/admin/spiffe/federation \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"trust_domain": "partner.example.com",
"bundle_endpoint_url": "https://partner.example.com/spiffe-bundle",
"bundle_endpoint_profile": "https_spiffe",
"endpoint_spiffe_id": "spiffe://partner.example.com/bundle-endpoint"
}'
Refreshing a foreign bundle
After registering the relationship, import the foreign bundle:
# Fetch the bundle manually and import it via the admin API.
BUNDLE_JSON=$(curl -s https://partner.example.com/spiffe-bundle)
curl -s -X POST https://idp.example.org/api/admin/spiffe/federation/partner.example.com/bundle \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "$BUNDLE_JSON"
Note: Automatic periodic bundle refresh (background polling) is not yet implemented. Until that feature lands, bundle refresh must be triggered manually via the admin API or via a cron job / CI pipeline.
Listing and deleting relationships
# List all configured federation relationships.
curl -s https://idp.example.org/api/admin/spiffe/federation \
-H "Authorization: Bearer $TOKEN"
# Delete a relationship (the cached bundle is not automatically evicted).
curl -s -X DELETE https://idp.example.org/api/admin/spiffe/federation/partner.example.com \
-H "Authorization: Bearer $TOKEN"
Workload API behaviour
Once a foreign bundle is cached, the SPIFFE Workload API includes it in all bundle responses:
FetchX509SVIDResponse.federated_bundles— concatenated X.509 CA cert DER bytes per trust domain.FetchX509BundlesResponse.bundles— same as above, plus the local trust domain.FetchJWTBundlesResponse.bundles— raw JWK Set JSON bytes per trust domain.
Foreign bundles are CRDT-gossiped across cluster nodes so any node can serve them without contacting the remote endpoint directly.
Workload API (gRPC)
The Workload API is a gRPC service on the Unix domain socket configured by
[spiffe] workload_socket. All RPC calls must carry the metadata header:
workload.spiffe.io: true
Calls that omit this header are rejected with PermissionDenied.
Caller attestation uses SO_PEERCRED on the Unix socket to obtain the caller’s
UID, GID, and PID. The kernel-returned ucred struct length is verified to
equal sizeof(ucred); a truncated response denies attestation (uid/gid are
set to u32::MAX so no selector will match). At accept time the server also:
- Reads
/proc/<pid>/exeto resolve the executable path. - Reads
/proc/<pid>/statusto collect the supplemental group ID list (Groups:line). - Reads
/proc/<pid>/cgroupto extract Kubernetes pod identity (pod UID, container ID, QoS class). - Computes SHA-256, SHA-512, and SHA-1 hashes of the executable binary (capped at 256 MB).
- Injects the node’s hostname (from
/proc/sys/kernel/hostnameat service init) into the context.
All eleven selector types are evaluated against this full AttestationContext.
| RPC | Type | Description |
|---|---|---|
FetchX509SVID | server-streaming | Attest the caller; issue an X.509-SVID (DER leaf cert + PKCS#8 private key) for each matching registration entry. If SVID issuance fails for any matched entry, the stream terminates with Status::internal. |
FetchJWTSVID | unary | Attest the caller; issue a JWT-SVID signed with the node’s active JWT signing key. Returns Status::internal if issuance fails. |
FetchX509Bundles | server-streaming | Return the DER-encoded CA certificate for the trust domain. |
FetchJWTBundles | server-streaming | Return the JWKS for JWT-SVID verification. |
ValidateJWTSVID | unary | Validate a JWT-SVID using the trust bundle. Propagates a Status::unauthenticated error if the system clock is before the Unix epoch. |
JWT-SVID signing key: JWT-SVIDs are signed with the node’s active OAuth2 JWT signing key (same key used for OAuth2 access tokens), loaded from the shared cluster signing key store. The key is included in the trust bundle returned by
FetchJWTBundlesandGET /.well-known/spiffe-bundle. ML-DSA keys are excluded from the JWT-SVID bundle because the SPIFFE JWT-SVID specification only permits RS/PS/ES algorithms.
Trust bundle endpoint
GET /.well-known/spiffe-bundle
Returns an application/json JWK Set containing:
- The CA certificate as an
x509-svidkey (DER-encoded, base64url inx5c). - Each active non-ML-DSA JWT signing key as a
jwt-svidkey. spiffe_sequence— monotonically increasing counter; consumers can detect bundle updates by comparing this value.spiffe_refresh_hint— suggested polling interval in seconds (configured by[spiffe] bundle_refresh_hint, default 300).
This endpoint is public and requires no authentication. The bundle is also
served at /spiffe/bundle (same handler, both paths are active).
CA key management
The SPIFFE CA key is stored encrypted (AES-256-GCM, using the cluster wrapping key) in the CRDT and gossiped to all cluster nodes. Key loading follows this priority order:
- CRDT contains an encrypted blob → decrypt and use.
ca_key_filestarts withpkcs11:→ load from HSM; cert loaded fromca_cert_file. The HSM-backed key is not stored in the CRDT.ca_key_fileandca_cert_fileare both set → load PEM files; encrypt the private key and gossip via CRDT.- Nothing found → generate a new ECDSA CA (algorithm from
ca_algorithm); encrypt and gossip the key.
OAuth2 bridge
To issue OAuth2 access tokens to a workload that authenticates with its
X.509-SVID, register an OAuth2 client and set its spiffe_id field to the
workload’s SPIFFE ID:
POST /api/admin/clients
Content-Type: application/json
{
"client_name": "myapp",
"token_endpoint_auth_method": "tls_client_auth",
"tls_client_certificate": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
"spiffe_id": "spiffe://example.org/workload/myapp",
"scopes": ["openid"]
}
When a workload presents its X.509-SVID in an mTLS connection and the SPIFFE
URI SAN in the certificate matches the spiffe_id of a registered client,
ahdapa issues an OAuth2 access token for that client. The certificate chain
is also structurally verified against the SPIFFE CA before the SPIFFE ID is
trusted.
OAuth2 token → JWT-SVID (reverse bridge)
A client that already holds a valid OAuth2 access token can exchange it for one
or more JWT-SVIDs by calling POST /spiffe/jwt-svid.
Simple exchange (client SPIFFE ID)
When the request body contains only an audience, the server issues a single
JWT-SVID using the spiffe_id registered on the OAuth2 client:
POST /spiffe/jwt-svid
Authorization: Bearer <access-token>
Content-Type: application/json
{"audience": ["https://target-service.example.org"]}
audience is optional and defaults to the server issuer URL. Returns 403
if the client has no spiffe_id registered.
Remote attestation exchange (hostname-based)
For workloads that cannot connect to the Unix socket directly (e.g. remote
machines or containerised services), the exchange endpoint supports
selector-based remote attestation. Include hostname (and optionally
ima_hash) in the request body:
POST /spiffe/jwt-svid
Authorization: Bearer <access-token>
Content-Type: application/json
{
"audience": ["https://target-service.example.org"],
"hostname": "web01.example.org",
"ima_hash": "sha256:deadbeef..."
}
When hostname is provided, the server:
- Looks up IPA host group memberships for the declared hostname via LDAP (server-verified — the caller cannot fake this step).
- Builds an
AttestationContextwith the hostname, host groups, and (if supplied) theima_hash. UID and GID are set tou32::MAXsentinel values so thatUid,Gid,SupplementalGid, andPathselectors never match remote callers. - Runs the full selector matching against all live
RegistrationEntrys. - Issues one JWT-SVID per matching entry’s SPIFFE ID.
- Falls back to the client’s registered
spiffe_idif no entries match and the client has one set. - Returns
403if neither path yields a SPIFFE ID.
The response always contains a svids array (one element per matched SPIFFE
ID) and the current trust bundle:
{
"svids": [
{
"spiffe_id": "spiffe://example.org/workload/web-fleet",
"svid": "<compact-jwt>",
"hint": ""
}
],
"bundle": "<trust-bundle-jwks-json>"
}
See Protocol Endpoints — SPIFFE JWT-SVID Exchange for the full request/response reference.
Socket permissions
The workload_socket_mode key controls the Unix permission bits on the Workload
API socket (expressed as a decimal integer). Common values:
| Decimal | Octal | Effect |
|---|---|---|
432 | 0o660 | Owner and group read-write (default). |
438 | 0o666 | Any local user can connect. Use only in development/demo. |
384 | 0o600 | Owner only. |
The socket is created by the ahdapa process user. Workload processes must
have read-write access to the socket file to call the Workload API. In
production, set the socket file’s group to a shared group containing both the
ahdapa service user and the workload users, and use workload_socket_mode = 432 (0o660).
Multi-node clusters
In a multi-node cluster the SPIFFE CA key is shared via the CRDT gossip protocol. All nodes present the same CA certificate, so X.509-SVIDs issued by any node are verifiable by any other node and by external workloads that trust the CA.
When using an HSM-backed CA key (ca_key_file = "pkcs11:..."), the private key
never leaves the HSM and is not gossiped. Each node must have local access to
the HSM and to the CA cert file (ca_cert_file).
Demo
A self-contained demo is included at contrib/demo/spiffe/. Run it with:
contrib/demo/spiffe/run.sh
The script generates a TLS PKI, pre-seeds a registration entry for the current
user’s UID, starts a single-node ahdapa instance, bootstraps an admin account,
and runs workload_client.py — a Python gRPC client that exercises all four
Workload API RPCs (FetchX509SVID, FetchJWTSVID, FetchX509Bundles,
FetchJWTBundles).
SPIFFE Workload Proxy
ahdapa-spiffe-proxy is a standalone daemon for IPA-enrolled hosts that do
not run an Ahdapa instance of their own. It presents a full SPIFFE Workload
API gRPC endpoint on a local Unix socket and forwards SVID requests to a
remote Ahdapa node using the host’s Kerberos credentials.
When to use the proxy
Use the proxy when workloads on a non-Ahdapa host need SPIFFE identities but you cannot or do not want to run a full Ahdapa cluster node on that host. Typical scenarios:
- Application servers or batch hosts enrolled in IPA but not running Ahdapa.
- Containers where the host’s keytab is available but Ahdapa is not installed.
- Environments where a single central Ahdapa cluster serves multiple hosts.
How it works
- At startup the proxy reads its configuration from
/etc/ahdapa/spiffe-proxy.toml(overridable with a positional CLI argument). - If
credential = "keytab", the proxy calls into libgssapi (equivalent tokinit -k -t <keytab> <principal>) to obtain a Kerberos TGT stored in a private in-memory ccache. Ifcredential = "ccache", it uses the default ccache maintained externally (e.g. by SSSD or a systemd credential). - It exchanges a SPNEGO token for an OAuth2 bearer token by posting
grant_type=client_credentialsto<ahdapa_url>/tokenwithAuthorization: Negotiate <base64-SPNEGO>. This is the samekerberos_client_authflow that SSSD uses for the identity API. - The bearer token is cached and refreshed proactively in the background at
75% of its
expires_inlifetime. - The proxy serves all five SPIFFE Workload API RPCs on the configured Unix
socket:
FetchJWTSVID— readsSO_PEERCREDfrom the local socket, reads/proc/<pid>/exeand/proc/<pid>/status, and forwards the real workload identity to Ahdapa viaPOST /spiffe/issue-svid. Returns the JWT-SVIDs from the response.FetchX509SVID— additionally generates an ephemeral EC keypair locally (algorithm fromx509_key_algorithm), sends only the SPKI DER to Ahdapa, receives the signed X.509-SVID certificate chain back, and assembles theX509SVIDmessage with the local private key. The private key never leaves the proxy host.FetchJWTBundlesandFetchX509Bundles— fetch fromGET /spiffe/spiffe-bundleevery 300 seconds and stream updates to connected clients.ValidateJWTSVID— fetches the bundle and validates the JWT locally.
Ahdapa server-side setup
On the Ahdapa node, the proxy host’s OAuth2 client must be registered with
kerberos_client_auth and its service principal must appear in
[spiffe] accepted_proxies.
POST /api/admin/clients
Content-Type: application/json
{
"client_name": "proxy-myhost.ipa.example.com",
"token_endpoint_auth_method": "kerberos_client_auth",
"kerberos_principal": "host/myhost.ipa.example.com@IPA.REALM"
}
The default accepted_proxies = ["host", "HTTP"] accepts both
host/myhost@REALM and HTTP/myhost@REALM principals. To restrict proxy
access to a specific set of principals, list only the service components you
trust:
[spiffe]
trust_domain = "example.org"
accepted_proxies = ["host"] # only host/ principals; not HTTP/
Set accepted_proxies = [] to disable the POST /spiffe/issue-svid endpoint
entirely.
Proxy configuration file
The proxy reads /etc/ahdapa/spiffe-proxy.toml at startup.
[proxy]
ahdapa_url = "https://ahdapa.ipa.example.com/idp"
trust_domain = "example.org"
workload_socket = "/run/spiffe/workload.sock" # default
workload_socket_mode = 432 # default (0o660)
svid_ttl_seconds = 3600 # default
x509_key_algorithm = "EC-P256" # default
[kerberos]
credential = "keytab" # or "ccache"
keytab = "/etc/krb5.keytab"
principal = "host/myhost.ipa.example.com@IPA.REALM"
target_service = "HTTP@ahdapa.ipa.example.com"
client_id = "host/myhost.ipa.example.com@IPA.REALM"
[tls]
# ca_cert = "/etc/ipa/ca.crt" # optional; defaults to system trust roots
[proxy] keys
| Key | Type | Default | Description |
|---|---|---|---|
ahdapa_url | string | — | HTTPS base URL of the Ahdapa node, e.g. "https://ahdapa.ipa.example.com/idp". |
trust_domain | string | — | SPIFFE trust domain, e.g. "example.org". Used as the bundle map key in Workload API responses. |
workload_socket | string | "/run/spiffe/workload.sock" | Local Unix socket path for the SPIFFE Workload API. |
workload_socket_mode | integer | 432 (= 0o660) | Unix permission bits for the socket. |
svid_ttl_seconds | integer | 3600 | SVID lifetime hint in seconds. The proxy uses half this value as the X.509-SVID refresh interval (minimum 30 s). |
x509_key_algorithm | string | "EC-P256" | Algorithm for ephemeral leaf keypairs used in FetchX509SVID. Accepted values: "EC-P256", "EC-P384", "EC-P521". Must be compatible with the Ahdapa CA algorithm. |
[kerberos] keys
| Key | Type | Required | Description |
|---|---|---|---|
credential | string | yes | "keytab" or "ccache". When "keytab", the proxy initiates a TGT from the specified keytab. When "ccache", it uses the default ccache maintained externally. |
keytab | string | keytab only | Path to the host keytab file, e.g. "/etc/krb5.keytab". |
principal | string | keytab only | Kerberos principal to use for the TGT, e.g. "host/myhost.ipa.example.com@IPA.REALM". |
target_service | string | yes | Kerberos target service for SPNEGO, e.g. "HTTP@ahdapa.ipa.example.com". |
client_id | string | yes | OAuth2 client_id to present at the token endpoint. Typically the Kerberos principal string. |
[tls] keys
| Key | Type | Default | Description |
|---|---|---|---|
ca_cert | string | — | Path to a PEM CA certificate to validate Ahdapa’s TLS certificate. When absent, the system trust roots are used. Set this to /etc/ipa/ca.crt when Ahdapa uses the IPA CA. |
Keytab access
/etc/krb5.keytab is typically 0600 root:root. The proxy service user
(conventionally spiffe-proxy) must be able to read it. The recommended
approach is to provision a dedicated keytab file:
ipa-getkeytab -s ipa.example.com -p host/myhost.ipa.example.com \
-k /etc/ahdapa/spiffe-proxy.keytab
chown root:spiffe-proxy /etc/ahdapa/spiffe-proxy.keytab
chmod 0640 /etc/ahdapa/spiffe-proxy.keytab
Then reference it in the config:
[kerberos]
credential = "keytab"
keytab = "/etc/ahdapa/spiffe-proxy.keytab"
principal = "host/myhost.ipa.example.com@IPA.REALM"
Systemd service
The binary is installed as ahdapa-spiffe-proxy. The recommended systemd
unit runs it as a dedicated system user and uses RuntimeDirectory=spiffe to
create /run/spiffe/ with appropriate ownership:
[Unit]
Description=SPIFFE Workload API proxy for IPA-enrolled hosts
After=network-online.target
[Service]
Type=simple
User=spiffe-proxy
Group=spiffe-proxy
RuntimeDirectory=spiffe
RuntimeDirectoryMode=0755
ExecStart=/usr/sbin/ahdapa-spiffe-proxy /etc/ahdapa/spiffe-proxy.toml
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target
See also
- Protocol Endpoints — SPIFFE Proxy SVID Issuance for the full
POST /spiffe/issue-svidrequest/response reference.
Configuration reference
See [spiffe] in the Configuration Reference for
all keys and defaults.