Let your services call with tokens
Let your own workloads call a model with short-lived tokens from your OpenID Connect issuer: a Kubernetes cluster's service-account issuer, or your identity provider. AstroPulse stores no secret: the gateway checks each token's signature, issuer, audience and claims, and you can test a token before any traffic. For callers you hand a secret to, use API keys.
Before you start
- A model on a verified domain, on a cluster with the Gateway add-on. See Access policies.
- An issuer whose discovery document and key set are on the public internet over HTTPS. The model's cluster must reach the key set too.
- An organization owner creates the policy and tests tokens.
In the console
-
Open Integrations > Access Policies and select Identity provider (OIDC).
-
In New identity-provider (OIDC) policy, enter the Policy name, for example
eks-workload-callers. -
Enter the Issuer: the token's
iss, exactly. AstroPulse checks its discovery document when you save. -
Leave JWKS URI (optional) empty to read it from the issuer, or enter it.
-
Enter the Audiences, comma separated, up to 8, for example
qwen-chat. -
Optional: under Required claims, select Add a claim, then enter the claim, such as
sub, and its allowed values, such assystem:serviceaccount:team-a:chat-client. -
Select Create. If the issuer cannot be reached, the form offers Save without checking: fill in JWKS URI (optional) first, since an unchecked policy needs it.
Open zoom view -
On the policy's row, select Test a token and test a real token: Test a token.
-
Attach the policy to the model under Who may call it on the Access step of Deploy model.
You should see
The policy's row reads Identity provider (OIDC) at revision 1. Test a token with a token from your issuer reads Accepted. Once the model is deployed, its Endpoint section reads Ready: "Callers must send a token that access policy eks-workload-callers accepts."
How token callers work
A JWT (JSON Web Token, RFC 7519) is
a signed set of statements called claims: who issued it (iss), who it
is for (aud), who it is about (sub), and when it expires (exp). An
OpenID Connect issuer is the service that signs those tokens and
publishes the public keys that verify them. Your issuer can be an identity
provider, or a Kubernetes cluster, which signs a token for each service
account.
With a jwt policy, AstroPulse stores no secret. The caller gets a token
from your issuer and sends it as Authorization: Bearer <token>; the gateway
checks the signature and the claims.
Issuer, audience and claims: the three rules
| Rule | Policy field | Passes when | Example |
|---|---|---|---|
| Issuer | spec.jwt.issuer | The token's iss equals it exactly, character for character, including any trailing slash | https://oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE |
| Audience | spec.jwt.audiences (1 to 8) | The token's aud contains any one of them | [qwen-chat] |
| Claims | spec.jwt.require (0 to 16 entries) | Every entry holds: the token has the claim, and its value equals any one of values | claim: sub, values: [system:serviceaccount:team-a:chat-client] |
- The issuer says whose tokens are accepted. Anyone your issuer signs for can get a token, so the issuer alone is rarely enough.
- The audience says which service a token was made for. Give your model
its own audience, such as its application name, and never reuse one meant
for another service (for example
sts.amazonaws.com), so a token minted for something else is refused here and a token for this model is refused there. The IETF's JWT best practices make the same point (RFC 8725, section 3.9). - Required claims narrow the callers further, for example to one
Kubernetes service account. Matching is exact. A dotted name reaches a
nested claim (
org.teamreadsteaminsideorg), so a claim whose own name contains a dot cannot be named. Use claims that hold a single string, such assub.
How a token is checked
When a policy is saved:
- AstroPulse reads
<issuer>/.well-known/openid-configuration(the OpenID Connect Discovery document) and requires itsissuerto equalspec.jwt.issuerexactly. - When
jwksUriis empty, it is filled from the document'sjwks_uri. - The key set at
jwksUrimust hold at least one public signing key.
Both addresses must be HTTPS and on the public internet. When the provider
cannot be reached, the save is refused with 422
IdentityProviderUnreachable; astroctl access-policy apply --allow-unverified saves it unchecked, and then jwksUri must be given.
On every call, the gateway on the model's cluster:
- Reads the token from
Authorization: Bearer <token>. No token:401. - Checks
issagainstissuer, and that the token is not expired (exp) or not yet valid (nbf). A minute of clock skew is allowed both ways: a token is accepted up to a minute after itsexpor before itsnbf. Any failure:401. - Checks
audagainstaudiences. A token for another audience:403. - Verifies its signature with a key from
jwksUri(fetched and cached by the gateway, so the cluster must reach that address). Accepted algorithms: RS256, RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES512 and EdDSA. A bad signature:401. - Checks every
requireentry. A missing claim or a value not in the list:403. - Removes the token and passes the request to the model.
The checks run in this order, so a token with several faults is refused for the first one.
Example: a workload on EKS calls a model with its service-account token
Every EKS cluster is an OpenID Connect issuer. A pod can ask Kubernetes for a short-lived token for its service account, with an audience you choose (service account token projection). The gateway checks that token directly, so this needs no IAM role and no IAM OIDC provider. The calling pod can run on any cluster; the model's cluster only needs to reach the issuer over the internet.
Here a pod running as service account chat-client in namespace team-a
on eks-prod calls the model qwen-chat.
1. Find the cluster's issuer URL (Amazon EKS user guide):
aws eks describe-cluster --name eks-prod \
--query "cluster.identity.oidc.issuer" --output text
https://oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE
2. Write the policy. A Kubernetes service-account token carries
sub: system:serviceaccount:<namespace>:<service-account>, so requiring
sub admits exactly one service account.
# eks-workload-callers.yaml
apiVersion: platform.astropulse.io/v1
kind: AccessPolicy
metadata:
name: eks-workload-callers
spec:
jwt:
issuer: https://oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE
audiences: [qwen-chat]
require:
- claim: sub
values: ["system:serviceaccount:team-a:chat-client"]
astroctl access-policy apply -f eks-workload-callers.yaml
The answer is revision 1, with jwksUri filled in from the cluster's
discovery document.
3. Create the namespace, the service account and a pod with a token for that audience. The kubelet writes the token to a file and replaces it before it expires (after 80% of its lifetime or 24 hours); the application must read the file again rather than keep the first value.
# chat-client.yaml
apiVersion: v1
kind: Namespace
metadata:
name: team-a
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: chat-client
namespace: team-a
---
apiVersion: v1
kind: Pod
metadata:
name: chat-client
namespace: team-a
spec:
serviceAccountName: chat-client
containers:
- name: app
image: python:3.12-slim
command: ["sleep", "infinity"]
volumeMounts:
- name: qwen-chat-token
mountPath: /var/run/secrets/qwen-chat
readOnly: true
volumes:
- name: qwen-chat-token
projected:
sources:
- serviceAccountToken:
audience: qwen-chat # one of the policy's audiences
expirationSeconds: 3600
path: token
kubectl apply -f chat-client.yaml
kubectl wait --for=condition=Ready pod/chat-client -n team-a --timeout=120s
4. Test a token before any traffic. Mint one for the same service account and audience from a terminal, and test it:
kubectl create token chat-client -n team-a --audience qwen-chat --duration 10m > t.jwt
astroctl access-policy test eks-workload-callers --token-file t.jwt
accepted: true
5. Attach the policy to the model and deploy it. In the model's Application manifest, set
spec.externalAccess (excerpt):
spec:
externalAccess:
domain: apps.example.com
accessPolicyRef:
name: eks-workload-callers
Wait until astroctl app endpoint get qwen-chat shows phase: Ready.
6. Call the model from the pod. Install the openai package in the pod,
then run the call there. The script reads the token file for each client, so
a rotated token is picked up:
kubectl exec -n team-a chat-client -- pip install --quiet openai
kubectl exec -i -n team-a chat-client -- python - <<'EOF'
from pathlib import Path
from openai import OpenAI
TOKEN = Path("/var/run/secrets/qwen-chat/token")
def model_client() -> OpenAI:
return OpenAI(base_url="https://qwen-chat.apps.example.com/v1",
api_key=TOKEN.read_text().strip())
reply = model_client().chat.completions.create(
model="<model-name>",
messages=[{"role": "user", "content": "Hello"}],
)
print(reply.choices[0].message.content)
EOF
The same steps work on any Kubernetes cluster whose service-account issuer is published over HTTPS on the public internet.
Example: a service with a token from your identity provider
Any OpenID Connect provider that signs tokens with one of the algorithms above works. A service with no Kubernetes identity, such as a backend or a CI job, typically uses the OAuth 2.0 client credentials grant (RFC 6749, section 4.4): it trades a client ID and secret, kept by your provider, for a short-lived token.
1. In your provider, register the service as a client and set the
token's audience to the value your policy will name, here
https://inference.example.com. Providers name this setting differently (an
API, a resource, an audience); what matters is the aud in the token.
2. Write and apply the policy. Use the issuer exactly as your provider's discovery document spells it:
curl -s https://login.example.com/.well-known/openid-configuration | jq -r .issuer
apiVersion: platform.astropulse.io/v1
kind: AccessPolicy
metadata:
name: backend-callers
spec:
jwt:
issuer: https://login.example.com/
audiences: ["https://inference.example.com"]
require:
- claim: sub
values: ["<client-id>"] # many providers put the client's ID in sub; check your token
astroctl access-policy apply -f backend-callers.yaml
3. Get a token and read its claims. The token address is the
token_endpoint of the discovery document; the audience parameter name
depends on your provider.
curl -s https://login.example.com/oauth2/token \
-d grant_type=client_credentials \
-d client_id='<client-id>' \
-d client_secret='<client-secret>' \
-d audience=https://inference.example.com | jq -r .access_token > t.jwt
# print the claims (reading only: decoding does not verify the token)
python3 -c 'import sys,json,base64; p=open(sys.argv[1]).read().split(".")[1]; print(json.dumps(json.loads(base64.urlsafe_b64decode(p+"="*(-len(p)%4))),indent=2))' t.jwt
Check that iss, aud and sub read as the policy expects, then test it
(next section) and call the model with Authorization: Bearer <token> as in
the EKS example.
Test a token
In the console, select Test a token on the policy's row. In Test a token against <policy>, paste the token and select Test: it reads Accepted or Refused, with the reason. The platform checks it exactly as the gateway would, against the policy's latest revision: signature, issuer, audience, expiry and required claims. The token is never stored or logged.
astroctl access-policy test <policy-name> --token-file <file>
# or from stdin
kubectl create token chat-client -n team-a --audience qwen-chat | astroctl access-policy test eks-workload-callers --token-file -
--token-file takes a file holding the JWT, with or without Bearer , or - for stdin; a token is never a flag value, so it stays out of shell history. The answer has accepted, reason (a code, empty when accepted) and message. A token minted with the wrong audience:
accepted: false
message: the token's audience [other-aud] includes none of the policy's [qwen-chat]
reason: AudienceMismatch
Every reason and its fix: Token refused.
Other ways in
- Nova: ask it, for example "why is this token refused by eks-workload-callers?".
- SDK and API: the same operations, in the API reference.
With astroctl
astroctl access-policy apply -f eks-workload-callers.yaml
astroctl access-policy apply -f eks-workload-callers.yaml --allow-unverified # issuer unreachable; jwksUri required
astroctl access-policy test eks-workload-callers --token-file t.jwt
Every flag: astroctl access-policy.
Settings
issuer, jwksUri, audiences and require are on Access policies. identity, projecting a claim into an upstream header, is refused in this release.
If it's refused
422 IdentityProviderUnreachable: AstroPulse could not reach the issuer or its key set, or it is not on the public internet.apply --allow-unverified).jwt policy.A save can also be refused on field jwt when the discovery document names another issuer, names no jwks_uri, or its key set has no public signing key; the message says which. Make Issuer match the discovery document's issuer exactly.
A caller's call refused with 401 or 403: Token refused.
Next
- Access policies: attach the policy, check the endpoint, close it.
- Give callers API keys
- Serve a model