Workload identity federation gives each agent its identity without a copied key.
When your agents run on a platform that signs tokens for them, such as Kubernetes, AWS or GitHub Actions, you write one trust rule instead of copying one key per agent. The SDK exchanges the token of the platform for an agent token valid for 15 minutes, and Paidwen creates the agent at its first exchange. One rule covers 1 agent as well as 10,000.
A trust rule says which tokens become which agents.
You write trust rules in Settings, then Workload identity, or with POST /v1/trust-rules. A rule holds:
- the issuer it trusts, compared exactly with the
issclaim of the token; - the public keys of the issuer, which Paidwen reads at the address of the issuer, or which you paste when the issuer is not reachable from the internet;
- a condition in CEL on the
claimsvariable, the content of the token; - the name of the agent, in CEL on the same variable;
- the environment, the team and the owner of the agents it creates.
{
"name": "Production cluster",
"issuer": "https://oidc.eks.us-west-2.amazonaws.com/id/6FA42E7BFDE8549CB3B1",
"condition": "claims[\"kubernetes.io\"].namespace == \"agents\"",
"agent_name": "claims[\"kubernetes.io\"].serviceaccount.name",
"environment": "production",
"team": "billing"
}The condition and the name use the same CEL as your rules, with the string functions of Kubernetes, such as split and lowerAscii. A condition that reads a missing claim does not accept the token. When two rules accept the same token, the older rule gives the agent.
Your platform signs the token for your organization.
Each organization has an audience: https://api.paidwen.com/organizations/ followed by the identifier of the organization. Settings, then Workload identity, shows it with a copy button. You put this audience in the token your platform signs, so that a token signed for Paidwen serves only your organization.
The audience is not a secret. Only the signature of your platform proves the identity of the agent.
Kubernetes mounts the token in each pod.
Kubernetes projects a token of the service account into the pod, with the audience of your organization, and replaces it before it expires. The SDK reads the file at each exchange.
apiVersion: apps/v1
kind: Deployment
metadata:
name: billing
namespace: agents
spec:
selector:
matchLabels:
app: billing
template:
metadata:
labels:
app: billing
spec:
serviceAccountName: billing
containers:
- name: agent
image: registry.example.com/billing-agent:1.4.0
env:
- name: PAIDWEN_IDENTITY_TOKEN_FILE
value: /var/run/secrets/paidwen/token
volumeMounts:
- name: paidwen-token
mountPath: /var/run/secrets/paidwen
readOnly: true
volumes:
- name: paidwen-token
projected:
sources:
- serviceAccountToken:
audience: https://api.paidwen.com/organizations/<organization id>
expirationSeconds: 3600
path: tokenimport { Paidwen } from "paidwen";
const paidwen = new Paidwen();from paidwen import Paidwen
paidwen = Paidwen()The client reads PAIDWEN_IDENTITY_TOKEN_FILE, as it reads PAIDWEN_API_KEY for a key. You can also pass the file with identityTokenFile in TypeScript, or identity_token_file in Python. Each service account becomes one agent, and the pods of a deployment share it, with one budget.
The issuer of an EKS cluster publishes its keys, so Paidwen reads them itself. When the issuer of your cluster is not reachable from the internet, you paste its keys, which kubectl get --raw /openid/v1/jwks prints, and you paste them again when the cluster changes them.
AWS signs a token for the role of the workload.
You turn on outbound identity federation in IAM, then Account settings, and you give the role of the workload the permission sts:GetWebIdentityToken. The issuer of your account looks like https://<id>.tokens.sts.global.api.aws, and the sub claim is the ARN of the role.
{
"name": "AWS workers",
"issuer": "https://4f7c1d2e-8a90-4b3c-9d6e-1f2a3b4c5d6e.tokens.sts.global.api.aws",
"condition": "claims.sub.startsWith(\"arn:aws:iam::123456789012:role/agent-\")",
"agent_name": "claims.sub.split(\"/\")[1]",
"environment": "production"
}The SDK contains no code for AWS: you give it a function that asks AWS for the token.
import { GetWebIdentityTokenCommand, STSClient } from "@aws-sdk/client-sts";
import { Paidwen } from "paidwen";
const sts = new STSClient({ region: "us-east-1" });
const paidwen = new Paidwen({
identityToken: async () => {
const answer = await sts.send(
new GetWebIdentityTokenCommand({
Audience: ["https://api.paidwen.com/organizations/<organization id>"],
SigningAlgorithm: "RS256",
DurationSeconds: 300,
}),
);
return answer.WebIdentityToken ?? "";
},
});import boto3
from paidwen import Paidwen
sts = boto3.client("sts", region_name="us-east-1")
def aws_token() -> str:
return sts.get_web_identity_token(
Audience=["https://api.paidwen.com/organizations/<organization id>"],
SigningAlgorithm="RS256",
DurationSeconds=300,
)["WebIdentityToken"]
paidwen = Paidwen(identity_token=aws_token)GitHub Actions signs a token for the job.
The job needs the permission id-token: write. Its token carries the repository, the branch and the workflow, which your condition reads.
{
"name": "Release workflow",
"issuer": "https://token.actions.githubusercontent.com",
"condition": "claims.repository == \"acme/billing-agent\" && claims.ref == \"refs/heads/main\"",
"agent_name": "claims.repository",
"environment": "test"
}permissions:
id-token: write
contents: readimport { Paidwen } from "paidwen";
const paidwen = new Paidwen({
identityToken: async () => {
const url = new URL(process.env.ACTIONS_ID_TOKEN_REQUEST_URL ?? "");
url.searchParams.set("audience", "https://api.paidwen.com/organizations/<organization id>");
const response = await fetch(url, {
headers: { authorization: `Bearer ${process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN}` },
});
const { value } = (await response.json()) as { value: string };
return value;
},
});import os
import httpx
from paidwen import Paidwen
def github_token() -> str:
response = httpx.get(
os.environ["ACTIONS_ID_TOKEN_REQUEST_URL"],
params={"audience": "https://api.paidwen.com/organizations/<organization id>"},
headers={"authorization": f"Bearer {os.environ['ACTIONS_ID_TOKEN_REQUEST_TOKEN']}"},
)
response.raise_for_status()
return response.json()["value"]
paidwen = Paidwen(identity_token=github_token)GitHub Actions signs tokens for every repository on GitHub. Your condition must name your repository, or your organization, so that no other workflow becomes one of your agents.
The SDK exchanges the token, then renews it.
The SDK follows OAuth 2.0 Token Exchange: it sends the token of the platform to POST /v1/oauth/token at its first question, and receives an agent token valid for 15 minutes. It exchanges again when less than 5 minutes remain, and simultaneous questions share one exchange.
- When the exchange gets no answer, or an error 5xx, the SDK applies
onUnavailable, as for a decision. - When Paidwen refuses the token, the SDK throws
RequestErrorwith the reason, and the action never runs. - When the decision point refuses an agent token, the SDK exchanges again once and asks again.
Cutting a rule or an agent stops its tokens within a second.
Turning a trust rule off, removing it, disabling an agent or putting it in quarantine reaches the decision points in under a second, without waiting for the 15 minutes of the tokens. A disabled rule also refuses every new exchange.
Removing a trust rule keeps its agents, with their history. Another rule can adopt them when they hold no active key.
An agent declared by hand moves to a trust rule.
A trust rule adopts an agent of the same name and the same environment that holds no active key and belongs to no rule. You revoke the keys of the agent, and the rule adopts it at its first exchange, with its history, its permissions and its budget. A rule never takes an agent that holds a key or that belongs to another rule.
Workload identity has six limits.
- A machine with no platform that signs for it, such as a laptop or a rented server, keeps a key.
- A trust rule is as safe as its platform. A person who can create a service account in the namespace of your condition can create an agent.
- The name of an agent comes from the token. Renaming a service account creates a new agent, without the budget or the permissions of the old one.
- The exchange goes through the Paidwen API. When it does not answer, an agent keeps its token for up to 15 minutes, then the SDK applies
onUnavailable. - Revoking the keys of an agent does not revoke its token. To cut an agent of a trust rule, you disable it, put it in quarantine or turn its rule off.
- The journal does not say whether an agent proved its identity with a key or with a token.