Documentation — Outbound credentials
Guides · Security

Outbound credentials

agentd dials three kinds of endpoint that may need a credential: model providers, MCP servers and A2A peers. Each takes one auth: block with the same shape, so a static token, an OAuth login, AWS SigV4 or a SPIFFE workload identity is configured the same way wherever it is used. This guide is for operators: it shows how to choose a credential, configure it, log in once when a person is needed, and what happens when a credential is missing. Signing callers in to agentd's own listener is a different task, covered in Connect agents over A2A.

Choose a credential

Where agentd runsUseInteractive?
anywhere, the endpoint takes an API keystatic, from a secret referencenever
a server, machine-to-machine OAuthoauth2 with grant: client_credentialsnever
a developer's or operator's machine, a person's OAuth identityoauth2 with grant: device (or authorization_code for a browser) and one agentd --loginonce
EC2 or EKS, an endpoint behind AWS IAMaws with source: imds or source: irsanever
a workstation, AWS IAM Identity Centeraws with source: sso and one agentd --loginonce
Kubernetes or a cloud with SPIREspiffe, a JWT-SVID as a bearer or an X.509-SVID for mutual TLSnever

A daemon never stops to wait for a person. Interactive credentials are obtained ahead of time with agentd --login, cached, and refreshed by the daemon.

A complete example

This agent calls a corporate model gateway with a machine credential, reads an issue tracker as the operator who logged in, and hands work to a partner agent with a static token:

agent:
  instruction: "Triage new tracker issues and hand billing questions to the partner desk."

intelligence:
  endpoints: https://llm.corp.example.com/v1/chat/completions
  model: gpt-5.1
  auth:
    kind: oauth2
    grant: client_credentials
    issuer: https://sso.corp.example.com
    client_id: agentd-triage
    client_secret: "{{secret:SSO_CLIENT_SECRET}}"
    scopes: [llm.invoke]

mcp:
  servers:
    - name: tracker
      endpoint: https://mcp.tracker.example.com/mcp
      tags: { "*": [untrusted_input] }
      auth:
        kind: oauth2
        grant: device
        issuer: https://sso.corp.example.com
        client_id: agentd-cli
        scopes: [tracker.read, offline_access]

a2a:
  peers:
    - name: partner
      endpoint: https://a2a.partner.example.com
      auth: { kind: static, token: "{{secret:PARTNER_TOKEN}}" }

With SSO_CLIENT_SECRET and PARTNER_TOKEN set, validate it, then log in to the tracker once:

agentd --validate-config --config agentd.yaml
agentd --login mcp:tracker --config agentd.yaml

The client-credentials grant needs no login: the daemon fetches and refreshes that token itself.

Where a credential goes

Endpointauth: blockOther ways to set a credential
the model connectionintelligence.authintelligence.token or token_file (a bearer), intelligence.headers
a named model providerintelligence.providers.<name>.auththat provider's token, token_file, headers
an MCP servermcp.servers[].authheaders; oauth: {token_url, client_id, client_secret, scope}, a client-credentials shortcut
a service catalog entryservices.<name>.authheaders
an A2A peera2a.peers[].authheaders; client_cert and client_key for mutual TLS

On a model connection, a credential from auth: takes precedence over token. The AGENTD_INTELLIGENCE_TOKEN and AGENTD_INTELLIGENCE_TOKEN_FILE environment overrides apply to the top-level connection only, never to a named provider. On an MCP server, auth: wins over oauth:, and either one turns off AAuth signing for that server; static headers are sent alongside.

One credential for many consumers. A service catalog entry holds the credential every server or peer that references it uses, cached once under service:<name>:

agent:
  instruction: "Answer account questions from the CRM."

intelligence:
  endpoints: https://api.openai.com/v1/chat/completions
  model: gpt-5.1
  token: "{{secret:OPENAI_API_KEY}}"

services:
  crm:
    endpoint: https://mcp.crm.example.com/mcp
    tags: { "*": [sensitive] }
    auth:
      kind: oauth2
      grant: device
      issuer: https://sso.corp.example.com
      client_id: agentd-cli
      scopes: [crm.read]
mcp:
  servers:
    - { name: crm, service: crm }

A server that references an entry may not restate its endpoint, auth or headers. agentd --login mcp:crm and agentd --login service:crm log into the same cached credential.

Static credentials

A fixed bearer token, or a fixed value in a header of your choosing, always from a secret reference:

# fragment: under mcp.servers[]:
auth: { kind: static, token: "{{secret:KB_TOKEN}}" }
# fragment: under mcp.servers[]:
auth: { kind: static, header: X-API-Key, value: "{{secret:KB_API_KEY}}" }

token is sent as Authorization: Bearer <token>. A {{secret-file:PATH}} reference is read again on every request, so a rotated file takes effect without a restart.

On a model connection, a static block takes token; a header + value pair is refused at load there. Use intelligence.headers for a custom header on a model connection.

OAuth 2.0

# fragment: under mcp.servers[]:
auth:
  kind: oauth2
  issuer: https://sso.corp.example.com      # or pin token_url, device_authorization_url
  client_id: agentd-cli
  client_secret: "{{secret:SSO_CLIENT_SECRET}}"   # omit for a public client
  grant: device                             # device | authorization_code | client_credentials
  scopes: [tracker.read, offline_access]
  audience: https://tracker.example.com     # sent with the device authorization request
GrantWhenHow the token arrives
device (the default)a person authorizes once, from any terminal, over SSH includedagentd --login prints a URL and a short code and polls until the person approves
authorization_codea person with a desktop browseragentd --login prints the authorization URL, catches the redirect on a one-time 127.0.0.1 listener, checks state, and exchanges the code with PKCE
client_credentialsmachine to machinethe daemon fetches the token with client_secret and refetches it before it expires; no login

Endpoints. With issuer, agentd discovers the token, device and authorization endpoints from <issuer>/.well-known/openid-configuration, or <issuer>/.well-known/oauth-authorization-server. Pin token_url, device_authorization_url or authorization_url to skip discovery. An oauth2 block needs client_id and either issuer or token_url; the device grant also needs issuer or device_authorization_url, and the browser grant issuer or authorization_url. The load checks each of these.

Discovery runs when the connection is built: at startup and at each reload. Pin the endpoints if the authorization server may be unreachable then.

The MCP shortcut. mcp.servers[].oauth is a client-credentials grant with a pinned token endpoint, and nothing else:

# fragment: under mcp.servers[]:
oauth:
  token_url: https://sso.corp.example.com/oauth2/token
  client_id: agentd-search
  client_secret: "{{secret:SEARCH_CLIENT_SECRET}}"
  scope: search.read

Limit. agentd reads OAuth responses as JSON, which is what RFC 6749 and RFC 8628 specify, and does not send Accept: application/json. An authorization server that answers form-encoded unless asked for JSON, such as GitHub's OAuth endpoints, fails with oauth: bad device-auth response.

Log in with agentd --login

When a credential needs a person (an oauth2 device or authorization-code grant, or aws with source: sso), complete it once with agentd --login. The release binaries and the container image include the oauth feature it needs.

agentd --login mcp:tracker --config agentd.yaml
┌─ authorize agentd ──────────────────────────────
│  target   mcp:tracker
│  visit    https://sso.corp.example.com/device
│  code     WDJB-MJHT
│  waiting… (expires in 900s · Ctrl-C to cancel)
└──────────────────────────────────────────────────
logged in to mcp:tracker — token cached in /home/you/.local/state/agentd/creds

The prompt goes to stderr and the one-line result to stdout. The token is never printed.

TargetLogs into
intelligencethe top-level model connection's auth
intelligence:<provider>intelligence.providers.<provider>.auth
mcp:<name>that MCP server's auth; a server that references a service logs into service:<entry>
service:<name>a service catalog entry's auth, shared by every consumer

An A2A peer can be logged into only through a service entry: give the peer service: <entry> with a kind: peer catalog entry, and log into service:<entry>. A peer's own auth block caches under a name --login does not accept.

agentd --logout <target> removes a cached credential.

The device grant, from login to the daemon's refresh:

The cache is one file per target, mode 0600, in $AGENTD_CRED_DIR, else $XDG_STATE_HOME/agentd/creds, else ~/.local/state/agentd/creds. Log in as the user the daemon runs as, or point both at the same AGENTD_CRED_DIR. The daemon refreshes a token in memory a minute before it expires, using the refresh token; a refreshed token is not written back to the cache. A restart reads the cache again, so after a restart the daemon refreshes from the refresh token the login stored. If your authorization server issues single-use refresh tokens, plan on logging in again after a restart.

AWS SigV4

An endpoint behind AWS IAM, such as an API Gateway in front of an MCP server or Amazon Bedrock, takes a kind: aws block. Every request is signed over its method, path and body; a session token is signed as x-amz-security-token.

# fragment: under mcp.servers[]:
auth:
  kind: aws
  region: us-east-1
  service: execute-api
  source: irsa
sourceCredentialsLogin
env (the default)AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN, read when the connection is builtnone
staticthe same as envnone
imdsthe EC2 instance role, through IMDSv2, refreshed a minute before expirynone
irsathe EKS pod's web identity (AWS_WEB_IDENTITY_TOKEN_FILE, AWS_ROLE_ARN) exchanged with STS, refreshed a minute before expirynone
ssoIAM Identity Center; needs sso_start_url, account_id, role_nameagentd --login, which caches temporary credentials; the signer re-reads them on every request
# fragment: under mcp.servers[]:
auth:
  kind: aws
  region: us-east-1
  service: execute-api
  source: sso
  sso_start_url: https://my-org.awsapps.com/start
  account_id: "123456789012"
  role_name: AgentdLedger

For Amazon Bedrock's own API, set intelligence.dialect: bedrock with a kind: aws block and service: bedrock; the load refuses the Bedrock dialect without one. Connect model providers has complete configurations for Bedrock, Azure OpenAI with Microsoft Entra ID, and Google Vertex AI.

SPIFFE workload identity

With SPIRE writing SVIDs to files, an agent needs no stored secret at all.

# fragment: under mcp.servers[]:
auth: { kind: spiffe, svid: jwt, jwt_svid_file: /run/spire/jwt.svid }
# fragment: under mcp.servers[]:
auth: { kind: spiffe, svid: x509, svid_file: /run/spire/svid.pem, key_file: /run/spire/svid.key }

A JWT-SVID is sent as a bearer token and read again on every request, so SPIRE's rotation needs nothing from you. An X.509-SVID is a mutual-TLS client identity: it works on MCP servers. A model connection cannot use one, and the load refuses it there. For an A2A peer, an x509 block applies nothing; set the peer's client_cert and client_key to the SVID files instead.

Mutual TLS and private CAs

An A2A peer that requires a client certificate takes client_cert and client_key, PEM files read when the peer is dialled:

# fragment: under a2a.peers[]:
name: billing
endpoint: https://a2a.billing.example.com
client_cert: /run/spire/svid.pem
client_key: /run/spire/svid.key

To trust an endpoint whose certificate a private CA signed, set security.tls_ca to a PEM bundle. agentd adds it to the bundled trust roots for every outbound TLS dial, in the daemon and its child processes.

Which kinds each endpoint accepts

static tokenstatic headeroauth2awsspiffe jwtspiffe x509
model connection or provideryesrefused at loadyesyesyesrefused at load
MCP server or serviceyesyesyesyesyesyes (mutual TLS)
A2A peeryesyesyes, login through a service entryyesyesuse client_cert and client_key

When a credential is missing or refused

The load catches what it can see, and exits 2 before any side effect:

  • a {{secret:…}} or {{secret-file:…}} reference that does not resolve;
  • an auth: block missing the fields its kind and grant need;
  • an inline token, value or client_secret in any auth: block, and an inline intelligence.token, provider token or oauth.client_secret in a config file.

An interactive credential that was never logged in, or has expired with no way to refresh, is not caught at load. The request goes out without the credential and the endpoint refuses it: an MCP server fails to connect (mcp.connect.fail), and a model call fails as an authentication error, which is not retried and fails the run. In a one-shot job, the run's run.done line carries the error:

{…,"err":"step \"work\" failed: turn failed: intel: intelligence HTTP 401: {\"error\":{\"message\":\"unauthorized\"}}","event":"run.done",…,"status":"failed",…}

The job then exits 4. If you see a 401 from an endpoint whose auth needs a login, run agentd --login for its target as the user the daemon runs as. The CLI reference has every exit code.

Field reference

FieldKindsMeaning
kindallstatic, oauth2, aws or spiffe (required)
tokenstaticbearer token; must be a secret reference
header, valuestatica custom header and its value; value must be a secret reference
issueroauth2authorization server for endpoint discovery
token_urloauth2token endpoint; pins discovery
device_authorization_urloauth2device authorization endpoint
authorization_urloauth2authorization endpoint for grant: authorization_code
client_idoauth2client id (required)
client_secretoauth2client secret; must be a secret reference; required for client_credentials
grantoauth2device (default), authorization_code or client_credentials
scopesoauth2requested scopes
audienceoauth2audience parameter sent with the device authorization request, for authorization servers that want one
regionawsAWS region (required)
serviceawssigning service name, such as bedrock or execute-api (required)
sourceawsenv (default), static, imds, irsa or sso
sso_start_url, account_id, role_nameawsIAM Identity Center portal, account and permission set; required for source: sso
svidspiffejwt (default) or x509
jwt_svid_filespiffeJWT-SVID file; required for svid: jwt
svid_file, key_filespiffeX.509-SVID certificate and key; required for svid: x509

Next steps