Private betaZynth Auth is currently in private beta testing.New organizations are created by invitation only, and no plan can be purchased yet.Request early access

Federated workload identity for AI agents

An agent normally authenticates with a credential (agt_…) that sits in your CI secrets, a Kubernetes Secret, or an env var — indefinitely. Federated workload identity removes it: your runtime proves what it is with the attestation it already has (a GitHub Actions OIDC token, a Kubernetes projected service-account token, cloud instance identity, a SPIFFE JWT-SVID), and Zynth exchanges that directly for the same short-lived agent token.

No Zynth secret at rest, anywhere. There is nothing to leak, nothing to rotate, and nothing whose theft is indistinguishable from the agent. The token that does briefly exist can additionally be sender-constrained with DPoP, making it useless if stolen.

Availability: feature-flagged per deployment (AGENT_FEDERATION_ENABLED). If the Attestation issuers section of the AI Agents console shows a "not enabled" card, ask your platform operator. The classic credential path always keeps working — bare-metal environments have no attestation source, and that is fine.

How trust is scoped (read this once)

  • You register the issuers your organization trusts, per issuer profile (GitHub Actions, Kubernetes, AWS, GCP, SPIFFE). Each profile enforces its own signature algorithms, maximum token lifetime, and key-discovery posture — see the profile table.
  • A binding maps exact attestation claims to one pre-registered agent. No wildcards — they cannot even be saved. A token matching zero bindings, or more than one, is refused identically.
  • Every attestation must be minted for your Zynth deployment (its aud is checked both ways), is accepted exactly once (single-use jti), and every registry change raises an immediate Command Center detection.

1. Register the issuer and bind your agent

AI Agents → Attestation issuers → Register issuer, pick the profile, then Bind agent with the exact claims. For GitHub Actions the subject claim looks like:

repo:<owner>/<repo>:ref:refs/heads/<branch>

Bind exactly the repository and ref your workflow runs from. If the console refuses your binding, read the message — it names the rule (a wildcard character, a missing subject claim, a claim outside the profile's schema) and the fix is always to be more specific.

2. Exchange from GitHub Actions

Grant the job an OIDC token minted for your deployment and exchange it:

permissions:
  id-token: write   # lets the job request an OIDC token

steps:
  - name: Get an agent token (no secrets configured anywhere)
    run: |
      ATTESTATION=$(curl -sSf -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
        "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=https://auth.example.com" | jq -r .value)
      curl -sSf -X POST https://auth.example.com/api/v1/agents/token/federated \
        -H "Content-Type: application/json" \
        -d "{\"tenant\": \"acme-inc\", \"subject_token\": \"$ATTESTATION\"}"

The response is the standard agent token ({access_token, token_type, expires_in, agent_id}). Note the two things you supply: audience = your deployment's URL (the exchange refuses tokens minted for anyone else), and tenant = your organization slug (an attestation carries no Zynth tenant, so the request names it).

With the TypeScript SDK, the whole loop — including transparent re-exchange with a fresh attestation each time — is:

import { AgentSession, githubActionsIdToken } from "@zynth/auth-client";

const agent = new AgentSession({
  baseUrl: "https://auth.example.com",
  federated: {
    tenant: "acme-inc",
    getSubjectToken: githubActionsIdToken("https://auth.example.com"),
  },
});
await agent.callTool("github", "search", { q: "zynth" });

3. Exchange from Kubernetes

Project a service-account token with your deployment as the audience — the kubelet rotates it for you:

volumes:
  - name: zynth-attestation
    projected:
      sources:
        - serviceAccountToken:
            path: token
            audience: https://auth.example.com
            expirationSeconds: 3600

Then federated: { tenant, getSubjectToken: kubernetesProjectedToken("/var/run/secrets/zynth/token") }. The kubernetes profile requires the issuer's JWKS URI pinned at registration (your cluster's discovery document usually isn't reachable from outside) — the console asks for it up front.

Harden with DPoP

Send a DPoP proof with the exchange and the issued agent token binds to your run's ephemeral key — stolen alone, it is useless. In the SDK, pass fetch: createDpopFetch(await createDpopKeypair()) to the AgentSession. Federation removes the secret at rest; DPoP makes the remaining short-lived token theft-resistant. Together they are the strongest agent-identity posture the platform offers.

Troubleshooting

Every refusal is the same generic 401 — deliberately, so the exchange can't be used to probe your configuration. The real reason is visible to your admins in the Command Center (repeated failures raise a detection naming it). The usual causes, in order:

  • aud mismatch — your attestation was minted without audience= set to the Zynth deployment URL. This is the most common one.
  • No unique binding — the token's claims match zero bindings (be less specific in the token or fix the binding) or more than one (delete the overlap; ambiguity never resolves to "first match").
  • Replay — each attestation exchanges once. Mint a fresh one per exchange; the SDK's provider callback does this for you.
  • Issuer disabled / unknown tenant / retired agent — all read identically from outside; check the registry and the agent's status in the console.
  • 429 with Retry-After — the fail-closed exchange throttles; back off and retry.