Credential Brokering
Agents that call external APIs need credentials -- GitHub tokens, Jira API keys, OAuth client secrets. The obvious approach is to pass those secrets through the agent's context, but that creates a direct exfiltration path: a prompt-injected agent can read its own context and send secrets to an attacker-controlled endpoint.
Navra's credential broker solves this by keeping secrets out of the agent's context entirely. The gateway resolves credential labels to real values at call time, injects them into the tool execution environment, and strips them from results before the agent sees the response. The agent works with opaque labels; the secret never appears in the conversation.
How it works
The broker follows a four-step lifecycle for every tool call that uses credentials:
- Label reference -- The upstream config references a credential
by label (e.g.,
github_token), not by value. - Backend resolution -- At call time, the gateway resolves the
label through the configured backend (OS keyring or environment
variable) and gets the raw secret. The resolved value is held in
a
Secretstruct that is zeroized on drop. - Injection -- The secret is injected into the upstream process environment (for stdio transports) or request headers (for HTTP transports). The agent's MCP session never contains the value.
- Stripping -- The safety pipeline scans tool results for credential patterns and exfiltration attempts before returning them to the agent.
Only credentials explicitly listed in the [credentials] config
section are accessible. The store cannot discover or enumerate OS
keyring entries -- unlisted labels are rejected with an error.
Configuration
Define credential mappings in the [credentials] section of your
config file. Each entry maps a label to a backend source.
Keyring source
The OS keyring is the recommended backend for production. Navra
uses the keyring crate, which supports GNOME Keyring (via
Secret Service), KWallet, macOS Keychain, and Windows Credential
Manager.
[credentials]
"github.pat" = { source = "keyring", path = "navra/github-pat" }
"jira.token" = { source = "keyring", path = "navra/jira-api-key" }
The path field uses service/user format. The part before the
slash is the keyring service name; the part after is the account or
user identifier within that service.
You can also reference credentials already stored by other applications:
[credentials]
"gnome.github" = { source = "keyring", path = "org.gnome.OnlineAccounts/github" }Environment variable source
For CI pipelines, containers, or environments without a keyring, credentials can be sourced from environment variables:
[credentials]
"ci.token" = { source = "env", var = "GITHUB_TOKEN" }
"api.key" = { source = "env", var = "MY_API_KEY" }
Environment-sourced credentials are read-only -- navra cannot store or delete them. Attempts to do so return an error.
Storing credentials in the keyring
Use secret-tool (part of libsecret) to store credentials in
the GNOME Keyring. The service and user must match the path field
in your config.
# Store a GitHub PAT
secret-tool store --label="navra GitHub PAT" \
service navra username github-pat
# Store a Jira API key
secret-tool store --label="navra Jira key" \
service navra username jira-api-key
# Verify it was stored
secret-tool lookup service navra username github-pat
On KDE, use kwalletcli or kwallet-query instead. On macOS, use
the Keychain Access app or security add-generic-password.
Using credentials in upstream config
The credentials map (recommended)
The credentials field in an upstream config maps environment
variable names to credential labels. At startup, navra resolves
each label and injects the value into the upstream process
environment:
[credentials]
"github.pat" = { source = "keyring", path = "navra/github-pat" }
[[upstream]]
name = "github"
transport = "stdio"
command = ["npx", "-y", "@modelcontextprotocol/server-github"]
credentials = { GITHUB_TOKEN = "github.pat" }
When navra spawns the npx process, it resolves github.pat from
the keyring and sets GITHUB_TOKEN in the child process
environment. The agent never sees the token value -- it only knows
that a tool named github is available.
If resolution fails (label not found, keyring locked, env var unset), navra logs a warning and starts the upstream anyway. The upstream may fail on its own when it tries to use the missing credential.
The env map with ${credential:label} syntax
The env field also supports credential references using the
${credential:label} placeholder syntax:
[[upstream]]
name = "github"
transport = "stdio"
command = ["npx", "-y", "@modelcontextprotocol/server-github"]
env = { GITHUB_TOKEN = "${credential:github_token}" }
Both approaches achieve the same result. The credentials map is
more explicit and easier to audit.
Per-agent credential scoping
Permission sets control which credential labels an agent can use.
The credentials array in a permission set lists the labels that
agents in that set are allowed to access:
[credentials]
"github.pat" = { source = "keyring", path = "navra/github-pat" }
"jira.token" = { source = "keyring", path = "navra/jira-api-key" }
"db.password" = { source = "keyring", path = "navra/db-password" }
[permissions.leader]
ring = 1
operations = ["read", "write"]
credentials = ["github.pat", "jira.token", "db.password"]
can_delegate = true
[permissions.readonly]
ring = 2
operations = ["read"]
credentials = ["github.pat"]
When a leader agent delegates a capability token to a specialist,
the credential set can only be narrowed -- never widened. A child
token that lists db.password when the parent only granted
github.pat is rejected at validation time. This enforcement is
cryptographic: the child token includes the parent's signature, and
the gateway verifies the full delegation chain.
[[agents]]
name = "leader"
token_hash = "..."
permissions = "leader"
capability_token = true
[[agents]]
name = "readonly-helper"
token_hash = "..."
permissions = "readonly"
The readonly-helper agent can only use credentials listed in the
readonly permission set (github.pat). Even if it somehow
obtained the label db.password, the gateway would refuse to
resolve it.
How the gateway prevents credential leakage
Credential brokering is one layer in a defense-in-depth strategy. Several other navra subsystems reinforce it:
IFC taint propagation
When a credential is injected into a tool call, the IFC pipeline tags the resulting data flow. If an agent attempts to write credential-tainted data to a lower-confidentiality channel (e.g., posting to a public webhook), the IFC lattice blocks the operation.
Safety filter stripping
The content safety pipeline scans every tool result for secret
patterns -- API keys, bearer tokens, passwords, and other
credential formats. Matches are redacted before the result reaches
the agent context. The secrets-only and standard safety
profiles both include these patterns.
Exfiltration detection
The ExfilDetectionFilter scans tool arguments for credential
theft patterns: curl commands posting $TOKEN or $SECRET,
env | curl pipelines, base64-encoded key file extraction, and
cloud metadata endpoint access. These are blocked before execution.
Zeroization
Resolved secrets are held in Zeroizing<Vec<u8>> wrappers (from
the zeroize crate). When the Secret struct is dropped, the
memory is overwritten with zeros. This limits the window in which
secrets exist in process memory.
Capability tokens carry labels, not values
Capability tokens encode credential labels (e.g., github.pat),
never secret values. Even if a token is intercepted or logged, it
reveals only which credentials the agent is authorized to use, not
the credentials themselves.
Practical examples
GitHub MCP server
[credentials]
"github.pat" = { source = "keyring", path = "navra/github-pat" }
[permissions.dev]
operations = ["read", "write"]
credentials = ["github.pat"]
[[agents]]
name = "claude"
token_hash = "..."
permissions = "dev"
[[upstream]]
name = "github"
transport = "stdio"
command = ["npx", "-y", "@modelcontextprotocol/server-github"]
credentials = { GITHUB_TOKEN = "github.pat" }# Store the token
secret-tool store --label="GitHub PAT" \
service navra username github-pat
# Paste your token when promptedJira API key via environment variable
For CI environments where no keyring is available:
[credentials]
"jira.key" = { source = "env", var = "JIRA_API_KEY" }
[[upstream]]
name = "jira"
openapi = "https://jira.example.com/v3/api-docs"
[upstream.auth]
bearer = "${JIRA_API_KEY}"export JIRA_API_KEY="your-api-key-here"
navra serveOAuth client secret for upstream MCP server
[credentials]
"oauth.secret" = { source = "keyring", path = "navra/oauth-client-secret" }
[[upstream]]
name = "secure-server"
transport = "http"
url = "https://mcp.example.com/mcp"
credentials = { OAUTH_SECRET = "oauth.secret" }
[upstream.oauth]
client_id = "navra-client"
client_secret = "${OAUTH_SECRET}"
scopes = ["read", "write"]Multi-agent team with scoped credentials
A leader agent has access to both GitHub and Jira credentials. It delegates a read-only sub-task to a specialist that only needs GitHub access:
[credentials]
"github.pat" = { source = "keyring", path = "navra/github-pat" }
"jira.token" = { source = "keyring", path = "navra/jira-api-key" }
[permissions.team-lead]
ring = 1
operations = ["read", "write"]
credentials = ["github.pat", "jira.token"]
can_delegate = true
[permissions.code-reader]
ring = 2
operations = ["read"]
credentials = ["github.pat"]
[[agents]]
name = "lead"
token_hash = "..."
permissions = "team-lead"
capability_token = true
[[agents]]
name = "reviewer"
token_hash = "..."
permissions = "code-reader"
The reviewer agent can use GitHub tools but has no access to
Jira credentials. If the leader delegates a capability token to
the reviewer, the token cannot include jira.token -- the
delegation validator rejects any credential not present in the
parent's grant.