Sign in

Workload Identity with SPIFFE

Workload Identity lets your workloads authenticate to MonoCloud as configured clients, using cryptographic identities issued through SPIFFE (their SPIFFE SVIDs) - instead of long-lived client secrets.

A workload running in your infrastructure, typically issued an identity by SPIRE, presents its SVID to MonoCloud's token endpoint. MonoCloud verifies the SVID against the issuing trust domain and, if the SPIFFE ID is associated with a client, issues an access token for the requested APIs.

There are no shared secrets to distribute, rotate, or leak. The workload proves its identity with a short-lived, automatically rotated SVID.

MonoCloud supports both SVID formats defined by SPIFFE:

  • X.509 SVIDs - presented as the client certificate over Mutual TLS (mTLS). The resulting access token is certificate-bound with a cnf claim, so it cannot be replayed by another party.
  • JWT SVIDs - presented as a signed client assertion. MonoCloud verifies the workload identity at token issuance, but the resulting access token is a bearer token and is not certificate-bound.
Workload Identity is an add-on capability. Trust stores and SPIFFE-based client authentication are available only when the Workload Identity add-on is enabled for your tenant.

Concepts

TermMeaning
Trust domainThe root of trust for a set of workloads, written as a hostname such as example.org. Every SPIFFE ID belongs to exactly one trust domain.
SPIFFE IDA URI that names a workload, in the form spiffe://<trust-domain>/<path>, for example spiffe://example.org/ns/billing/sa/worker.
SVIDA SPIFFE Verifiable Identity Document - the credential that carries a SPIFFE ID. It can be an X.509 SVID (a certificate with the SPIFFE ID in its URI SAN) or a JWT SVID (a signed JWT with the SPIFFE ID as sub).
Trust bundleThe set of public keys for a trust domain that relying parties use to verify its SVIDs: X.509 authorities for X.509 SVIDs and JWT authorities for JWT SVIDs.
Bundle endpointAn HTTPS URL that serves the trust bundle in SPIFFE bundle format. MonoCloud fetches the bundle from this endpoint.
Trust storeThe MonoCloud object that federates one trust domain. It holds the bundle endpoint and SVID validation settings, and MonoCloud uses it to verify incoming SVIDs.

The end-to-end model is:

  1. Your SPIRE deployment is the authority for a trust domain and issues SVIDs to your workloads.
  2. You publish that trust domain's bundle at an HTTPS endpoint.
  3. You create a MonoCloud trust store pointing at that endpoint, which federates the trust domain.
  4. You associate SPIFFE IDs with the MonoCloud clients those workloads should act as.
  5. Workloads request access tokens by presenting their SVIDs at the token endpoint.

Providing the trust bundle

Before MonoCloud can verify your workloads' SVIDs, it needs your trust domain's bundle. MonoCloud retrieves the bundle by performing an HTTPS GET against a bundle endpoint URL that you supply.

MonoCloud fetches the bundle when you create the trust store, then refreshes it according to the bundle's refresh hint and MonoCloud's cache rules. Authentication uses the current cached bundle. If MonoCloud needs to refresh the bundle and the endpoint cannot be reached within the configured timeout, authentication is rejected.

There are two common ways to expose your SPIRE trust domain's bundle to MonoCloud. Both result in a single HTTPS URL that you paste into the trust store.

**MonoCloud validates the bundle endpoint's TLS certificate against the public web PKI. If you expose a SPIRE bundle endpoint directly, configure SPIRE to serve it using its https_web profile, with a certificate from a publicly trusted CA. The https_spiffe profile, where the endpoint's TLS certificate is itself an SVID, is not supported.
A trust bundle contains public verification material, not workload private keys. It must be integrity-protected and served over HTTPS, but it does not need to be confidential.

Option A: Expose the SPIRE bundle endpoint directly

SPIRE Server can serve its own trust bundle over HTTPS through its built-in bundle endpoint. Configure the federation.bundle_endpoint block with the https_web profile so the endpoint presents a publicly trusted TLS certificate, for example one obtained automatically through ACME / Let's Encrypt:

spire-server.conf
server {
  trust_domain = "example.org"
  # ...

  federation {
    bundle_endpoint {
      address      = "0.0.0.0"
      port         = 8443

      acme {
        domain_name  = "spire.example.org"
        email        = "admin@example.org"
        tos_accepted = true
      }
    }
  }
}

MonoCloud then fetches the bundle from an endpoint such as: https://spire.example.org:8443

This keeps SPIRE as the single source of truth, but it requires the SPIRE bundle endpoint to be reachable from MonoCloud over the public internet with a publicly trusted certificate.

The exact configuration schema for SPIRE's bundle endpoint can vary between SPIRE versions. The block above is representative. Consult the SPIRE federation documentation for your installed version.

Option B: Publish the bundle to object storage

If you would rather not expose your SPIRE server publicly, use a SPIRE Server bundle publisher plugin to push the bundle to external storage whenever it rotates, then serve that object over HTTPS.

The aws_s3 bundle publisher writes the bundle to an S3 object. Publish it in SPIFFE format so MonoCloud can parse it:

spire-server.conf
server {
  trust_domain = "example.org"
  # ...
}

BundlePublisher "aws_s3" {
  plugin_data {
    region     = "us-east-1"
    bucket     = "example-trust-bundle"
    object_key = "spiffe-bundle.json"

    # Publish SPIFFE JWK Set format, not SPIRE's PEM format.
    format = "spiffe"
  }
}

Front the object with HTTPS and a publicly trusted certificate - for example, an S3 bucket behind CloudFront or another CDN - then point MonoCloud at that URL: https://bundles.example.org/spiffe-bundle.json.

This decouples MonoCloud from your SPIRE server. MonoCloud only reaches the static object, while SPIRE pushes a fresh bundle whenever its signing material rotates.

SPIRE also offers publishers for other object stores, such as GCP Cloud Storage. Any option works as long as the published object is served over HTTPS in SPIFFE format.

Bundle format requirements

Whichever option you choose, the URL must return a SPIFFE bundle encoded as an RFC 7517 JWK Set. This is not an ordinary OAuth JWKS: the keys must include SPIFFE-specific use values such as x509-svid and jwt-svid.

  • Keys with "use": "x509-svid" are the X.509 authorities used to build and verify X.509 SVID certificate chains. These keys carry the trust domain's certificates in x5c.
  • Keys with "use": "jwt-svid" are the JWT authorities used to verify JWT SVID signatures.

A few practical requirements:

  • The bundle must include the trust domain's X.509 authorities. MonoCloud discovers the trust domain from the spiffe://<trust-domain> URI SAN of those certificates. You never enter the trust domain manually. A bundle with no X.509 authority carrying a SPIFFE ID SAN is rejected.
  • For a nested SPIRE deployment, the bundle needs only the trust domain's root authority. The downstream intermediate CAs are presented with each X.509 SVID during the TLS handshake rather than published in the bundle. See Nested SPIRE and SVID chains.
  • The bundle may include a top-level numeric spiffe_refresh_hint value in seconds. MonoCloud uses it to decide how long to cache the bundle, so set it to match your rotation cadence.
  • You can inspect what SPIRE will publish with spire-server bundle show -format spiffe.

Creating a trust store

A trust store federates one trust domain. Creating one tells MonoCloud to fetch and trust SVIDs issued by that domain.

In the dashboard

  1. In the MonoCloud Dashboard, go to Client Authentication → Workload Identity.
  2. Click Add Store.
  3. In the Create Truststore dialog, enter the SPIFFE Bundle Endpoint - the HTTPS URL from the previous section.
  4. Optionally enable Show in Discovery Document to publish this trust store's mTLS endpoint aliases in the mtls_additional_endpoint_aliases extension of the OpenID Connect discovery document.
  5. Click Create Truststore.

When you save, MonoCloud fetches the bundle from the endpoint, parses it as a SPIFFE JWK Set, and discovers the trust domain from the X.509 authorities' SPIFFE ID SAN. The discovered trust domain becomes the trust store's name.

The first trust store you create (MutualTLS or Workload Identity) automatically becomes the tenant default.

The dialog asks only for the bundle endpoint because everything else is derived. The Trust Domain is read from the bundle, and the signing keys come from the bundle itself.

MonoCloud publishes the default trust store's mTLS endpoints in the standard mtls_endpoint_aliases field. Additional trust stores are published in mtls_additional_endpoint_aliases, a MonoCloud-specific discovery extension. Clients that need a non-default trust store must read this extension and use the corresponding endpoint.

Validation settings

Each trust store has SVID validation settings, editable from its Settings tab. They apply to every SVID verified through that trust store.

SettingDefaultDescription
Validate Certificate UseOnEnforce the X.509 SVID leaf-certificate requirements. It must be usable for digital signatures, must not be a CA, and must not have certificate-signing or CRL-signing key usage.
Validate Certificate ExpiryOnReject the SVID unless the current time falls within its Not Before / Not After validity period.
Cache Authentication Result / Authentication Cache Duration300sCache successful certificate-authentication results to avoid repeating cryptographic checks. Range: 0-3600 seconds.
Bundle Fetch Timeout10sMaximum time to wait when downloading the bundle from the endpoint. If MonoCloud needs to refresh the bundle and the endpoint does not respond in time, authentication is rejected. Range: 3-30 seconds.

Banning specific SVIDs

From a trust store's Banned SVIDs tab, you can block an individual workload identity that is still trusted by the trust domain - for example, a compromised workload that you cannot immediately deprovision in SPIRE.

Click Ban SVID, enter the full SPIFFE ID such as: spiffe://example.org/ns/billing/sa/worker. You can also add an optional reason for banning the SVID.

Banned SVIDs are rejected at authentication time regardless of whether their SVID is otherwise valid.

Using the Management API

You can also manage trust stores with the MonoCloud Management API. To create a SPIFFE trust store:

Request
POST /truststores/spiffe/
Content-Type: application/json

{
  "spiffe_bundle_endpoint": "https://bundles.example.org/spiffe-bundle.json",
  "show_in_discovery_document": false,
  "options": {
    "validate_certificate_use": true,
    "validate_validity_period": true,
    "certificate_auth_cache_duration": 300,
    "bundle_fetch_timeout": 10
  }
}

A successful call returns 201 Created with the trust store, including the discovered trust domain.

Creating or enabling a SPIFFE trust store requires the Workload Identity add-on. Without it, the API returns 402 Payment Required. The number of trust stores you can create is governed by your add-on quantity.

Associating SPIFFE IDs with a client

A trust store tells MonoCloud which SVIDs are authentic. It does not, by itself, specify which client a workload may authenticate as.

You make that association by adding a secret to the client whose value is the workload's SPIFFE ID. The client is what the access token is issued for, the SPIFFE ID secret is how the workload proves it may act as that client.

There is one secret type per SVID format. Both take a SPIFFE ID as their value and both require the Workload Identity add-on:

Secret type in the dashboardUsed byValue
SPIFFE ID (X.509-SVID)The X.509 SVID mutual-TLS flowThe workload's SPIFFE ID
SPIFFE ID (JWT-SVID)The JWT SVID client-assertion flowThe workload's SPIFFE ID

To add one in the dashboard:

  1. Open the client under Applications and find the Secrets section.
  2. Click Add Secret.
  3. For Secret Type, choose SPIFFE ID (X.509-SVID) or SPIFFE ID (JWT-SVID), depending on how the workload will authenticate.
  4. Enter the workload's SPIFFE ID as the value and save.

A client can hold several SPIFFE ID secrets. You can add secrets of both types to the same client if it uses both flows.

The value can match either an exact identity or a path prefix using a trailing /* wildcard:

  • spiffe://example.org/ns/billing/sa/worker matches only that exact workload.
  • spiffe://example.org/ns/billing/* matches workloads below that path, such as spiffe://example.org/ns/billing/sa/worker.

At authentication time, MonoCloud compares the SPIFFE ID from the presented SVID against the client's SPIFFE ID secrets of the matching type. If none match, authentication fails.

Workload Identity applies to both application and agent clients, so either client type can authenticate with an SVID.

If you omit client_id on the JWT SVID flow, MonoCloud finds the client by looking up which client holds a matching SPIFFE ID (JWT-SVID) secret. Each SPIFFE ID should normally be associated with a single client. See Requesting tokens with JWT SVIDs.

Requesting tokens with X.509 SVIDs

In this flow the workload presents its X.509 SVID as the client certificate over mutual TLS to MonoCloud's mTLS token endpoint.

Find the correct endpoint in your tenant's OpenID Connect discovery document. For the default trust store, use the corresponding token_endpoint value in mtls_endpoint_aliases. For an additional trust store, use the corresponding endpoint from MonoCloud's mtls_additional_endpoint_aliases extension.

This flow is normally combined with the client_credentials grant for service-to-service access.

Terminal
curl -X POST https://{tenant}.{region}.mtls.monocloud.com/connect/token \
  --cert svid.pem \
  --key  svid.key \
  -d grant_type=client_credentials \
  -d client_id=billing-worker \
  -d scope=billing \
  -d resource=https://api.example.com/billing

You can include client_id to select a specific client. It is optional when the SPIFFE ID maps to exactly one client, but required when the same SPIFFE ID is associated with more than one client.

The client_id is required in this flow. The SVID is presented at the TLS layer, while client_id tells MonoCloud which client to issue the token for.

MonoCloud verifies the SVID against the matching trust store before issuing a token. It checks that:

  • the certificate is a leaf certificate and not a CA,
  • it has exactly one URI SAN, and that URI is a valid workload SPIFFE ID with a non-root path,
  • the SPIFFE ID's trust domain matches the trust store's trust domain,
  • the certificate chains to the trust domain's x509-svid authorities from the bundle - either directly, or through one or more intermediate CAs the workload presents alongside the leaf,
  • the leaf's key usage and the validity period of every certificate in the chain satisfy the trust store's validation settings, and
  • the SPIFFE ID is not banned.

MonoCloud then confirms that the SPIFFE ID matches one of the client's SPIFFE ID (X.509-SVID) secrets.

On success, MonoCloud issues an access token that is bound to the certificate used on the TLS connection through the cnf claim. See Proof of possession.

Nested SPIRE and SVID chains

In a nested SPIRE topology, downstream SPIRE servers issue SVIDs from their own intermediate signing CAs, which chain up to a single root.

The federated trust bundle holds only that root authority; the downstream intermediate CAs are not published in the bundle - they travel with each X.509 SVID during the TLS handshake.

MonoCloud supports this topology. It builds the full path leaf -> intermediate(s) -> root.

It anchors validation on the bundle's root authority, so a workload issued by a downstream SPIRE server validates without you adding that server's intermediate CA to the bundle.

For this to work, the workload must present its complete X.509-SVID chain as the client certificate: the leaf first, followed by any intermediate CAs.

This is what the SPIFFE Workload API returns for an X.509 SVID, so a PEM built from it already has the certificates in the correct order:

svid.pem
-----BEGIN CERTIFICATE-----
<leaf SVID - carries the SPIFFE ID>
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
<downstream / intermediate CA>
-----END CERTIFICATE-----

A few things to keep in mind:

  • Only the root authority belongs in the bundle. Intermediates are presented with the SVID, not published.
  • Every certificate in the presented chain, leaf and intermediates, must satisfy the trust store's validation settings. An expired or otherwise invalid intermediate fails the entire chain.
  • If the workload presents only the leaf, or omits an intermediate, MonoCloud cannot build the chain and authentication fails.
  • A single-tier SPIRE deployment, whose SVID leaf is signed directly by the bundle's authority, needs no intermediates and is unaffected.

Requesting tokens with JWT SVIDs

In this flow, the workload obtains a JWT SVID through its SPIFFE Workload API and presents it as a client assertion on a regular, non-mTLS request to the token endpoint.

Find the token endpoint in your tenant's OpenID Connect discovery document.

First request a JWT SVID with MonoCloud's issuer URL as its audience:

Terminal
SVID_JWT=$(spire-agent api fetch jwt \
  -audience https://{tenant}.{region}.monocloud.com \
  -socketPath /run/spire/sockets/agent.sock \
  -output json | jq -r '.svids[0].svid')

Then present that SVID as the client assertion:

Terminal
curl -X POST https://{tenant}.{region}.monocloud.com/connect/token \
  -d grant_type=client_credentials \
  -d scope=billing \
  -d resource=https://api.example.com/billing \
  -d client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-spiffe \
  -d client_assertion="$SVID_JWT"

The single most important requirement is the audience:

A JWT SVID used to authenticate to MonoCloud must have an aud claim equal to your tenant's issuer URL, and that must be its only audience. This is stricter than private_key_jwt. A JWT SVID whose audience is the token endpoint, or that carries any additional audience, is rejected. Take the issuer value from your tenant's OpenID configuration.

A JWT SVID is a standard SPIFFE JWT with the workload's SPIFFE ID as its sub:

JWT-SVID
{
  "sub": "spiffe://example.org/ns/billing/sa/worker",
  "aud": "https://{tenant}.{region}.monocloud.com",
  "exp": 1735689600
}

MonoCloud:

  • resolves the trust domain from the SPIFFE ID in sub and looks up the matching trust store,
  • verifies the assertion's signature against that trust domain's jwt-svid authorities,
  • requires a signed token with an expiry and a supported algorithm: RS256, RS384, RS512, PS256, PS384, PS512, ES256, ES384, or ES512,
  • requires sub to be a valid workload SPIFFE ID with a non-root path,
  • rejects the SVID if it is banned, and
  • confirms the SPIFFE ID matches one of the client's SPIFFE ID (JWT-SVID) secrets.

client_id is optional on this flow.

If you omit it, MonoCloud resolves the client from the SPIFFE ID in sub. If more than one client is associated with that SPIFFE ID the request is ambiguous and you must send client_id to disambiguate.

Access tokens issued through the JWT SVID flow are bearer tokens associated with the client. They are not key-bound and carry no cnf claim. Use the X.509 SVID flow when you need proof-of-possession, sender-constrained tokens.

Proof of possession and the cnf claim

When MonoCloud issues a token through the X.509 SVID flow, it binds the token to the client certificate used on the TLS connection, following RFC 8705 (OAuth 2.0 Mutual-TLS).

The token carries a confirmation (cnf) claim containing the SHA-256 thumbprint of that certificate:

Access-Token
{
  "iss": "https://{tenant}.{region}.monocloud.com",
  "client_id": "billing-worker",
  "aud": "https://api.example.com/billing",
  ...
  "cnf": {
    "x5t#S256": "bwcK0esc3ACC3DB2Y5_lESsXE8o9ltc05O89jdN-dg2"
  }
}

The same cnf claim is returned for the token at the introspection endpoint, so resource servers that use reference (opaque) tokens can retrieve it.

Certificate-bound tokens are issued only through the mTLS token endpoint.

How a resource server validates the binding

A cnf-bound access token may only be used by the holder of the matching certificate.

A resource server that accepts these tokens must, in addition to validating the token itself:

  1. Require the caller to present its SVID certificate over mutual TLS.
  2. Compute the SHA-256 thumbprint of that certificate.
  3. Compare it to the x5t#S256 value inside the token's cnf claim.
  4. Reject the request if the values differ.

This ensures a leaked or exfiltrated token is useless to anyone who does not also hold the corresponding SVID private key.

Because SVIDs are short-lived and rotate frequently, each certificate-bound token is tied to the specific SVID that requested it. After rotation, the workload requests a new token with its new SVID.

JWT SVID tokens have no cnf claim and are validated as ordinary bearer tokens. Reserve resource-server proof-of-possession checks for the X.509 SVID flow.

Using SPIFFE identity in API access policies

Once a workload authenticates with an SVID, its SPIFFE identity is available to API Access Policies, so you can authorize token issuance based on workload identity, not just the client.

For requests authenticated with either SVID format, the workload's SPIFFE ID is exposed as context.spiffe_id:

Policy
permit (
  principal,
  action == Action::"IssueAccessToken",
  resource == Api::"https://api.example.com/billing"
)
when {
  context has spiffe_id &&
  context.spiffe_id == "spiffe://example.org/ns/billing/sa/worker"
};

For X.509 SVIDs, which authenticate over mTLS, you can additionally match on the certificate that was validated, including the trust store that validated it and the SPIFFE ID in its URI SAN:

Policy
permit (
  principal,
  action == Action::"IssueAccessToken",
  resource == Api::"https://api.example.com/billing"
)
when {
  context has certificate &&
  context.certificate.san_uris.contains("spiffe://example.org/ns/billing/sa/worker")
};

context.spiffe_id and context.certificate are optional, so guard them with context has .... See the API Access Policies guide for the complete context reference.

Troubleshooting

  • The bundle endpoint could not be reached: MonoCloud must be able to reach the endpoint over HTTPS within the trust store's Bundle Fetch Timeout when it needs to fetch or refresh the bundle. Confirm that the URL is publicly reachable and served with a publicly trusted TLS certificate. MonoCloud validates the endpoint against the web PKI and does not support the https_spiffe profile.
  • The SPIFFE bundle could not be parsed or the trust domain could not be determined: The endpoint must return a SPIFFE bundle encoded as a JWK Set and must contain the trust domain's X.509 authorities. MonoCloud reads the trust domain from those certificates' spiffe:// URI SAN. Publish with SPIRE's spiffe format, not the PEM format.
  • 402 Payment Required when creating a trust store or credential: The Workload Identity add-on is required for trust stores and SPIFFE credentials. Reaching your trust-store quota also returns 402; the limit is governed by your add-on quantity.
  • A trust store with that trust domain already exists: Each trust domain can be federated only once per tenant.
  • X.509 SVID rejected: Check that the SVID is a leaf certificate with exactly one URI SAN that is a workload SPIFFE ID, that its trust domain matches the trust store, that it chains to the bundle's x509-svid authorities, and that it satisfies Validate Certificate Use and Validate Certificate Expiry. Also confirm that the client has a matching SPIFFE ID (X.509-SVID) secret and that the SPIFFE ID is not banned.
  • X.509 SVID rejected with a chain error in nested SPIRE: The workload must present its complete SVID chain, the leaf followed by every intermediate CA. Every certificate in the chain must be valid. Presenting only the leaf, omitting an intermediate, or presenting an expired intermediate prevents chain building. Keep only the root in the bundle. See Nested SPIRE and SVID chains.
  • JWT SVID rejected for audience: The assertion's aud must be exactly your tenant's issuer URL and nothing else. An audience set to the token endpoint, or any extra audience, fails validation.
  • JWT SVID request is ambiguous: More than one client is associated with the SPIFFE ID in sub. Send an explicit client_id to select the client.
  • Workload authenticated but no token was issued: Authentication and authorization are separate. Even after a valid SVID, an API Access Policy must still permit issuing the token for the requested API. Check the policies configured for that API.
© 2024 MonoCloud. All rights reserved.