Get tokens for external services
This guide shows how to give your workloads short-lived access tokens for a third-party service, such as a SaaS product, without giving any workload that service's long-lived credentials. Use it when a service outside your trust domain accepts only its own OAuth client credentials, rather than SPIFFE identities.
How it works
You register Defakto with the third-party service once. To authenticate to the service's authorization server, Defakto uses either a client secret that only your Trust Domain Servers can read, or a JWT that Defakto signs. When a workload needs to call the service, it asks Defakto for a token with an OAuth token exchange request, authenticating with its JWT-SVID or X.509-SVID. Defakto requests an access token from the service's authorization server using the OAuth client credentials grant, and returns that token to the workload unchanged. The workload then calls the service's APIs with it.
Workloads never see Defakto's credentials for the service, and each workload holds only a short-lived token.
Before you begin
Setting this up has two parts:
- Connect the service to your trust domain. Your Defakto administrators do this once for each service.
- Get tokens from a workload. The owner of each workload that calls the service does this.
Example values
This page uses these example values. Replace them with your own as you go.
| Value | Example |
|---|---|
| The address where you host your Trust Domain Servers | https://td-server.defakto.example.com |
| The third-party service, as workloads name it | https://api.example.net |
| The service's authorization server (its issuer URL) | https://auth.example.net |
| The scopes Defakto may request from the service | records.read, records.write |
| The name you give the service in the configuration | vendor-api |
Part 1: Connect the service to your trust domain
Your Defakto administrators do this once for each service. After that, the workloads that your token policy allows can get tokens for the service. If the service is already connected, skip to Part 2.
You need:
- Permission to register an OAuth client with the third-party service
- If Defakto will authenticate with a client secret, permission to create Kubernetes Secrets in the namespace where your Trust Domain Servers run
- Permission to change your trust domain's Managed Configuration
Step 1: Register Defakto with the service
Following the service provider's instructions, create an OAuth client for Defakto that uses the client credentials grant. Some providers call this a service account, a machine-to-machine application, or an API client. Grant it the scopes your workloads need, and record its client ID. Then choose how Defakto authenticates to it:
- A client secret. Record the client's secret, and continue with step 2.
- A JWT that Defakto signs (
private_key_jwt), if the service's authorization server supports it. In the client's authentication settings, add your trust domain's OAuth JWKS Endpoint as its JWKS URL, so the authorization server can verify Defakto's JWTs and pick up new keys when Defakto rotates them. Find the OAuth JWKS Endpoint in the Defakto console, under your trust domain's Settings > Trust domain URLs, or in the output ofdefakto trust-domain info <trust-domain>. No secret is stored, so skip step 2.
Step 2 (client secret only): Store the client secret
Create a Kubernetes Secret that holds the client secret, in the namespace where your Trust Domain Servers run. You create and manage the Secret yourself, the same way you manage your other Kubernetes Secrets. If you run several Trust Domain Server deployments, create the Secret for each one. Trust Domain Servers pick up a changed secret without a restart.
apiVersion: v1
kind: Secret
metadata:
name: vendor-api
namespace: <Trust Domain Server namespace>
labels:
defakto.security/credential: oauth-server
annotations:
defakto.security/issuer: https://auth.example.net
type: Opaque
stringData:
clientSecret: <client-secret-from-step-1>
- The
defakto.security/credentiallabel marks the Secret as one the OAuth server may read. - The
defakto.security/issuerannotation names the service's authorization server. Defakto uses the secret only with that authorization server.
Then let your Trust Domain Servers read the Secret. In your Helm values, turn on oauthClientSecrets, and list the Secret's name:
trustDomainDeployment:
deployment:
oauthClientSecrets:
enabled: true
names: [vendor-api]
Then upgrade your Helm release. List the name of every Secret that holds a client secret. Without names, Trust Domain Servers can read every Secret in their namespace.
Step 3: Configure the OAuthServer section
Add the service to your trust domain's OAuthServer configuration, creating the configuration if the trust domain doesn't have one yet:
section: OAuthServer
schema: v1
spec:
endpoints:
publicBaseURL: https://td-server.defakto.example.com
externalResources:
- name: vendor-api
resource: https://api.example.net
authorizationServer:
issuer: https://auth.example.net
grant:
clientCredentials:
clientID: <client-id-from-step-1>
clientAuth:
method: client_secret_basic
secretRef:
name: vendor-api
key: clientSecret
scopes: [records.read, records.write]
publicBaseURLis the address where you host your Trust Domain Servers. If Defakto hosts your Trust Domain Servers, leave it out.resourceis the value workloads name when they ask for a token for this service.authorizationServer.issueris the issuer URL of the service's authorization server. Defakto finds the authorization server's token endpoint in the OAuth or OpenID Connect discovery metadata published at this issuer, so the authorization server must publish that metadata.clientIDis the client ID from step 1.clientAuthis how Defakto authenticates to the service. With a client secret,methodisclient_secret_basic, andsecretRefnames the Kubernetes Secret from step 2 and the key that holds the secret.scopesare the scopes Defakto requests from the service. A workload can ask for fewer, and gets all of them if it asks for none.
If Defakto authenticates with a JWT that it signs, use this clientAuth instead, with no secretRef:
clientAuth:
method: private_key_jwt
Apply the configuration to your trust domain. You can apply Managed Configuration through the Defakto API, Terraform, or the Defakto CLI. See applying configuration. For example, with the CLI:
defakto config set trust-domain --id <trust-domain-id> oauth-server.yaml
Step 4: Allow workloads to get tokens
Every workload gets tokens through Defakto's one registration at the service, so the service can't tell your workloads apart. Your token policy decides which workloads can get them. A workload gets a token for the service only when an allow or external authorizer rule matches its request, and the rule's match.audience is the service's resource. Without such a rule, every request for the service is refused.
For example, this policy lets only the reporting workloads get tokens for the service, with read-only scope, and refuses every other request:
spec:
# endpoints and externalResources as in step 3
policy:
authorization:
unmatched: deny
allow:
- name: reporting-reads-vendor-records
match:
caller:
spiffeID: spiffe://example.com/ns/reporting/**
requestedTokenType: urn:ietf:params:oauth:token-type:access_token
audience: https://api.example.net
token:
allowedScopes: [records.read]
See the token policy reference for every kind of rule.
Part 2: Get tokens from a workload
The owner of each workload that calls the service does this.
You need:
- Part 1 completed for the service
- A workload that can get JWT-SVIDs or X.509-SVIDs, and can reach your Trust Domain Servers
Step 1: Request a token for the service
Send a token exchange request to <address of your Trust Domain Servers>/oauth/token, naming the service as the audience:
- JWT-SVID
- X.509-SVID
The workload proves its identity with a JWT-SVID sent as a client assertion. Get the JWT-SVID the usual way, through the SPIFFE Workload API or, in serverless runtimes, the Defakto serverless SDK, with its audience set to your trust domain's JWT issuer, not its OAuth issuer. Find the JWT Issuer in the Defakto console, under your trust domain's Settings > Trust domain URLs, or ask your Defakto administrators.
POST /oauth/token HTTP/1.1
Host: td-server.defakto.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&audience=https://api.example.net
&scope=records.read
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-spiffe
&client_assertion=<the workload's JWT-SVID>
The workload proves its identity with its X.509-SVID as the TLS client certificate, and sends client_id set to its SPIFFE ID:
POST /oauth/token HTTP/1.1
Host: td-server.defakto.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&audience=https://api.example.net
&scope=records.read
&client_id=<the workload's SPIFFE ID>
The body is shown on separate lines for readability. Send it as a single URL-encoded form body.
| Parameter | Required | Value |
|---|---|---|
grant_type | Yes | Exactly urn:ietf:params:oauth:grant-type:token-exchange |
audience | Yes | The service's resource value from the configuration, such as https://api.example.net. You can send it as resource instead. |
scope | No | The scopes your workload needs, separated by spaces. Without it, Defakto requests all the scopes in the configuration. |
client_assertion_type and client_assertion | With a JWT-SVID | urn:ietf:params:oauth:client-assertion-type:jwt-spiffe, and the workload's JWT-SVID |
client_id | With an X.509-SVID | The workload's SPIFFE ID, exactly as it appears in the X.509-SVID |
The response contains the service's own access token, exactly as the service's authorization server issued it, in a standard OAuth token response:
{
"access_token": "<the service's access token>",
"token_type": "Bearer",
"expires_in": 3600
}
Step 2: Call the service
Call the service's APIs with the access token, as the service's documentation describes, usually in the Authorization header. The token's lifetime is set by the service's authorization server. Reuse the token until shortly before expires_in elapses, then request a new one.
Troubleshooting
To see why a request failed, open your trust domain in the Defakto console, and go to Issuance > Token Exchanges.
The token endpoint returns invalid_client. Defakto couldn't authenticate the workload. With a JWT-SVID, check that its audience is the JWT issuer URL, not the OAuth issuer or the service's URL, and that it hasn't expired. With an X.509-SVID, check that client_id is exactly the SPIFFE ID in the workload's X.509-SVID.
The token endpoint returns invalid_scope. None of the scopes the workload asked for is in the service's scopes in the configuration and allowed by the token policy. When only some of them are, Defakto requests just those, without an error.
The token endpoint returns server_error. Defakto couldn't get a token from the service's authorization server. The OAuth server's audit event for the request records why:
secret_unusable: The Kubernetes Secret is missing from a Trust Domain Server's namespace, isn't labeled, or names a different issuer in its annotation. Or the Trust Domain Servers aren't allowed to read it, becauseoauthClientSecretsisn't enabled in your Helm values, or doesn't list the Secret.provider_denied: The service's authorization server refused Defakto's request. For example, the client secret or client ID is wrong, the authorization server doesn't have Defakto's JWKS URL or doesn't accept signed JWTs from clients, the client doesn't allow the client credentials grant, or Defakto's registration at the service doesn't have the requested scopes.upstream_unavailable: Defakto couldn't reach the service's authorization server, or it didn't respond.
The token endpoint refuses every request for the service. No allow or external authorizer rule matches the request with the service's resource in match.audience. Add a rule as in step 4.
The service rejects the token. Check the scopes the service requires for the call, and that Defakto's registration at the service has been granted them.
Choosing how many registrations to use
You can register Defakto with the service once, and share that registration across every workload that's allowed to use it. Or you can create a separate registration for each workload, or for each group of workloads.
A shared registration is a reasonable default, and keeps setup light. A separate registration is worth the extra setup when:
- The service's own logs and permissions need to tell your workloads apart
- You want to limit the effect of one registration's credentials being compromised
- The service requires a separate client for each caller
Either way, Defakto's audit events record which workload received each token.
To use separate registrations, add an externalResources entry for each one. Give each entry its own resource value, such as https://api.example.net/reporting and https://api.example.net/billing. The resource value is a name that workloads use to ask for a token, so it doesn't need to be the service's real address. Then add a token policy rule for each entry, so that each workload can request tokens only from its own entry.