Obexal Docs

Docs/AI agents/Secret rotation

Agent secret rotation

Rotate an AI agent's client secret in one call, set an expiry so stale credentials die on their own, and script the rotation for real credential hygiene.

Confidential agents authenticate to the token endpoint with a client_secret. Secrets leak: they end up in logs, CI variables, forked repositories, and the agent that holds them often outlives the person who created it. Obexal treats rotation as a first-class operation and lets you give every secret an expiry date, enforced fail-closed.

Rotate a secret

POST /v1/admin/agents/{clientId}/secret generates a new secret, with an optional lifetime in the body. It requires the apps:manage permission, with an Admin API token (Authorization: Bearer obx_...) or a console session. In the console it is a sensitive action: a fresh MFA check is required. If your organization uses a custom domain, it replaces accounts.obexal.com.

curl -sS -X POST https://accounts.obexal.com/v1/admin/agents/$AGENT_ID/secret \
  -H "Authorization: Bearer $OBEXAL_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"ttlSeconds": 7776000}'
{
  "secret": "kJ2mX0aQ3vTzq8Rw5nY7cD1fH4bL6sN9pE0uG2iA5oM",
  "secretExpiresAt": "2026-09-30T09:14:03Z"
}

Three things to know:

  • The secret is shown once. Only its SHA-256 is stored: nobody, including Obexal, can display it again. Put it in your secret manager immediately.
  • The old secret stops working immediately. There is no overlap window with two valid secrets: update the agent's deployment right after rotating.
  • Only confidential clients have a secret. Rotating a public agent (PKCE, no secret) answers 400. ttlSeconds: 0 or omitted means a secret without expiry.
Warning

Because the previous secret ceases immediately, plan the rotation together with the redeployment of the agent that uses it.

Expiry is enforced fail-closed

When secretExpiresAt has passed, authentication at the token endpoint is refused with invalid_client, even if the presented secret value is correct. An expired secret cannot be quietly kept in service: the only way forward is a rotation. Each refused attempt also raises an expired_secret anomaly, see Kill switch and anomaly detection.

The agent inventory (GET /v1/admin/agents and the console) exposes secretExpiresAt and secretExpired for every confidential agent; an expired secret shows as a red "secret expired" badge so it never goes unnoticed.

Choosing a lifetime

The console proposes the same values you can pass as ttlSeconds:

LifetimettlSeconds
No expiry0
30 days2592000
90 days7776000
1 year31536000

A good default is 90 days. Use 30 days for agents with broad scopes or sensitive audiences, and reserve "no expiry" for cases where you already rotate on a strict external schedule.

Script the rotation

The best practice is a regular, scripted rotation through the API, for example from a scheduled job:

#!/bin/sh
# Rotate the agent secret (new 90-day lifetime) and store it.
RESP=$(curl -sS -X POST "https://accounts.obexal.com/v1/admin/agents/$AGENT_ID/secret" \
  -H "Authorization: Bearer $OBEXAL_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"ttlSeconds": 7776000}')
printf '%s' "$RESP" | jq -r '.secret' | secret-store put "agents/$AGENT_ID/client_secret"
# Then redeploy or reload the agent so it picks up the new secret.

Setting an expiry turns the schedule into a dead man's switch: if the rotation job silently breaks, the old secret dies at its expiry date instead of living forever.

Audit trail

Every rotation is written to the audit log as admin.agent.secret_rotated, targeting agent:<clientId>, with the new secretExpiresAt in the metadata when one was set. Combined with the expired_secret anomaly, you can prove both that rotations happen and that expired credentials are actually refused.