Secret vaults
XTM One can read secrets from your enterprise vault instead of storing them. A secret field holds a vault reference, and the platform reads the secret from the vault each time a feature uses it. Rotating the secret in the vault is enough: nothing changes in XTM One and no restart is needed.
Four vaults are supported: HashiCorp Vault, AWS Secrets Manager, Google Cloud Secret Manager and Azure Key Vault. The feature is opt-in. Until you declare a provider, nothing changes, and a secret field only takes a value.
Write a reference
A reference has this shape:
| Part | Meaning |
|---|---|
provider |
The name of a provider you declared (see Declare a provider), in lowercase. |
path |
Where the secret lives in that vault, as the vault names it: secret/xtm-one/github in HashiCorp Vault, prod/github or an ARN in AWS Secrets Manager. |
#key |
Optional. The entry to read when the secret holds several: a key of a HashiCorp KV secret, a field of a JSON secret. Without it, a secret stored as one value is read whole (a JSON secret string in AWS, Google Cloud or Azure included), and a HashiCorp KV secret holding a single entry is read as that entry. An empty value is never used: the read fails. |
For example, vault://corp/secret/xtm-one/github#token reads the token entry of the secret/xtm-one/github secret in the corp provider.
These fields take a reference instead of a value:
| Where | Fields |
|---|---|
| Integrations | API key, client secret |
| MCP servers | Each header value, each environment variable value |
| LLM providers (Settings > AI Models) | API key |
| Variables | The value of a secret variable |
| Channel bots (Settings > Channels & Bots) | Bot token, client secret |
Under each of these fields, the form says that it accepts a reference and lists, in a tooltip, the providers you may name; the hint only appears when you may name one. The reference is the whole value of the field: Bearer vault://corp/... in a header is stored as text, not read from the vault. Store Bearer <token> in the vault instead.
An MCP server reads only what its transport sends: the environment variables for a local (stdio) server, the headers for a remote one. After a transport switch the other set stays stored, unread, so a reference left in it neither reaches the vault nor stops the server.
The vault:// prefix is reserved in these fields: a value starting with it is always read as a reference, so a secret that must literally start with vault:// is stored in a vault and referenced. A value stored before the upgrade that starts with vault:// is listed by the security audit, naming the entity and the field, never the value: under encryption.vault_reference_unknown_provider when no provider of that name is declared, under encryption.vault_reference_invalid when it is not a valid reference at all. The entity page flags the second kind too.
When you save, the platform checks the reference without reading the vault. A malformed reference, or one that names an undeclared provider, is refused with a 422 that says what is wrong. Any other field, a client id or a URL for example, refuses a reference.
Declare a provider
You declare a provider in the platform's environment, never in the UI or the database:
<NAME> is the name references use, in lowercase: SECRET_VAULT_CORP_TYPE declares vault://corp/.... A name starts with a letter and has at most 32 letters, digits or _. Set the same variables on the platform and the worker containers: both read secrets, and both log at startup the providers they declare and the declarations they refuse.
The provider's own credentials also come from the environment or the workload identity: a token file, an AppRole, a Kubernetes service account, an IAM role, GKE Workload Identity, an Azure managed identity. Nothing that opens the vault is ever stored by the platform or shown in the UI.
A declaration the platform cannot use (an unknown type, a missing required setting, a malformed value) does not stop the platform. Every secret read from that provider fails, and the security audit names the variable to fix.
A provider address (HashiCorp ADDRESS, AWS ENDPOINT_URL, Azure VAULT_URL) is a plain http(s) URL. One that carries a user name or a password, a query string or a fragment is refused, so no credential ever sits in an address the platform logs or reports.
HashiCorp Vault
| Setting | Description |
|---|---|
ADDRESS |
Required. The Vault address, for example https://vault.example.com:8200. |
AUTH_METHOD |
token (default), approle or kubernetes. |
TOKEN / TOKEN_FILE |
With token: the token itself, or a file that holds it. The file is read again on every request, so a token rotated by Vault Agent or a mounted secret is picked up at once. |
ROLE_ID, SECRET_ID / SECRET_ID_FILE |
With approle: the role id (required) and the secret id, inline or in a file. |
ROLE, JWT_FILE |
With kubernetes: the Vault role (required) and the service account token, by default the pod's /var/run/secrets/kubernetes.io/serviceaccount/token. |
AUTH_MOUNT |
Where the auth method is mounted, approle or kubernetes by default. |
NAMESPACE |
A Vault Enterprise namespace. |
KV_VERSION |
2 (default) or 1. |
KV_MOUNT |
When set, reference paths are relative to this KV mount. Otherwise the first segment of the path is the mount, as with vault kv get: secret/xtm-one/github reads xtm-one/github from the secret/ mount. Required with KV_VERSION=1, whose paths have no data/ segment to keep a read inside a KV engine. |
CA_CERT_FILE |
The CA bundle that signs the Vault certificate, for a private CA. |
With approle or kubernetes, the login token is kept in memory until shortly before its lease ends, and a read refused with 403 logs in again once (reads refused at the same time share that login), so a revoked token recovers on its own. Vault also answers 403 for a path its policy denies, so a refused read logs in again at most once every 15 seconds, and a failed login is not tried again for 15 seconds either. Every request to Vault carries X-Vault-Namespace when NAMESPACE is set.
SECRET_VAULT_CORP_TYPE=hashicorp
SECRET_VAULT_CORP_ADDRESS=https://vault.example.com:8200
SECRET_VAULT_CORP_AUTH_METHOD=token
SECRET_VAULT_CORP_TOKEN_FILE=/run/secrets/vault-token
SECRET_VAULT_CORP_WRITERS=admins
SECRET_VAULT_CORP_PATHS=secret/xtm-one
With this declaration, vault://corp/secret/xtm-one/github#token reads /v1/secret/data/xtm-one/github.
The platform only reads secrets from a KV engine. Without KV_MOUNT, a reference whose first segment is one of Vault's own mounts (sys, auth, identity, cubbyhole, in any case), or that names a mount alone, is refused when it is saved (422) and when it is read: otherwise a reference could make the platform call a Vault API such as auth/token/lookup-self with its own token and send the answer wherever the entity points. A KV_MOUNT naming one of those mounts, or holding a . or .. segment, makes the declaration unusable. With KV_MOUNT set, every reference path stays under that mount, whatever its first segment.
AWS Secrets Manager
| Setting | Description |
|---|---|
REGION |
The region of the secrets. Without it, the AWS SDK's region from the environment (AWS_REGION). |
ENDPOINT_URL |
A VPC endpoint, when the platform reaches Secrets Manager through one. |
The platform authenticates with the AWS SDK's default credential chain: an IRSA or EKS Pod Identity role, an ECS task role, an instance profile. A reference path is the secret's name or its ARN, and #key reads one field of a JSON secret string.
With this declaration, vault://aws/prod/github#token reads the token field of the prod/github secret. The role needs secretsmanager:GetSecretValue on the secrets you reference, and kms:Decrypt when they are encrypted with a customer-managed key.
Google Cloud Secret Manager
| Setting | Description |
|---|---|
PROJECT |
The project of a secret named without one. Without it, the project of the platform's credentials. |
The platform authenticates with Application Default Credentials: GKE Workload Identity or the attached service account. A reference path is <secret>, <secret>/<version>, or the full projects/<project>/secrets/<secret>[/versions/<version>]. The version defaults to latest, and #key reads one field of a JSON payload. The service account needs the roles/secretmanager.secretAccessor role on the secrets you reference.
Azure Key Vault
| Setting | Description |
|---|---|
VAULT_URL |
Required. The https:// URL of the key vault, for example https://my-vault.vault.azure.net. |
CLIENT_ID |
A user-assigned managed identity, or the application of the workload identity. Defaults to AZURE_CLIENT_ID. |
TENANT_ID |
The tenant of the workload identity. Defaults to AZURE_TENANT_ID. |
The platform gets its token from AKS Workload Identity (the federated token in AZURE_FEDERATED_TOKEN_FILE), then from the App Service or Container Apps identity endpoint, then from the instance metadata service of a VM or a scale set. A reference path is <secret name> or <secret name>/<version>, and #key reads one field of a JSON value. The identity needs the Key Vault Secrets User role, or a get secret permission.
Who may write a reference
A reference is read with the platform's own vault identity, for whoever uses the field it sits in. An integration or an MCP server may also send its secret to whatever address it points at. So, by default, only administrators may write a reference to a provider. Two settings, read for every provider type, change that:
| Setting | Description |
|---|---|
WRITERS |
admins (default): only administrators may store a reference to this provider. all: anyone who may edit a secret field may store one. |
PATHS |
Comma-separated path prefixes that references to this provider may read, for example secret/xtm-one,secret/shared. Prefixes match whole path segments and are case-sensitive: secret/xtm-one admits secret/xtm-one/github, never secret/xtm-one-staging/github. Empty (default): every path. |
A reference the user may not write is refused when it is saved, with a 403 that names the field and the provider. A path outside PATHS is refused when it is saved (422), and again when it is read, so narrowing PATHS later stops the references already stored outside it. The same rules apply to an import (an integration bundle, an MCP configuration file) and to the agent builder tools. The tooltip under a secret field only lists the providers its user may name.
The rule also covers a reference already stored. When an integration or an MCP server holds a reference its editor may not write (an administrator stored it), that editor may still rename it, tag it, share it or replace the secret, but an edit that changes where it sends the secret is refused with a 403: an integration's provider settings (its base URL, for example), token or authorize URL, or auth type; an MCP server's URL, transport, command, arguments, auth mode or OAuth endpoints; for both, the relay group they are routed through, set, changed or cleared. The same edit is accepted when it also replaces the field holding the reference, so the secret never follows an address the editor chose. Saving the form with those settings unchanged is not refused.
PATHS compares the path as written in the reference. When you use it with AWS or Google Cloud, also list the ARN or projects/... forms you allow, or write references by secret name only.
Give all only to a provider scoped to what users may read
With WRITERS=all, any user who can edit an integration can point it at their own server and have the platform send it any secret the provider can read. Pair all with a PATHS list, or with a vault identity that can only read secrets meant for every user. The security audit reports a provider with WRITERS=all and no PATHS.
Cache and rotation
A secret read from a vault is kept in the process memory for SECRET_VAULT_CACHE_SECONDS (300 by default), then dropped from memory whether or not it is used again, and read again on its next use. A secret rotated in the vault is therefore used within that time, without a restart. 0 reads the vault on every use.
| Variable | Required | Default | Description |
|---|---|---|---|
SECRET_VAULT_CACHE_SECONDS |
No | 300 |
How long a secret read from a vault stays in memory. 0 reads it on every use. |
- When an integration's service refuses its credential (
401or403), the integration's references are dropped from the cache and read again on the next call. So is a client secret refused by an OAuth token endpoint (a token refresh) or by an A2A partner's token endpoint. A token refresh refused that way (invalid_client, or GitHub's and Slack's client refusals inside a200) is tried once more with the secret read again when the vault now holds another value, before the integration is marked as needing re-authorization. When the vault cannot be read again, the refresh backs off and tries later instead. - An LLM provider or a remote MCP server refusing its key does not drop it from the cache: the rotated key is used once the cache time has passed.
- An LLM provider whose API key is a reference switches to a rotated key within the same time, in a failover chain too: its client is rebuilt as soon as the key read from the vault changes.
- The Discord bot keeps one connection open with the token it connected with. When Discord ends it (you reset the token, which revokes the old one), the bot connects again 30 seconds later with the token read from the vault again, not from memory. Consecutive failures wait longer, up to 10 minutes.
- Concurrent uses of one secret share a single vault request.
- A failed read is not retried for 15 seconds, so a broken reference does not hammer the vault. Nor is a failed login or token request (HashiCorp AppRole or Kubernetes, Azure, Google Cloud): the reads waiting for it fail at once instead of each trying again.
- Every request to a vault times out after 10 seconds, the requests that obtain the platform's own vault credentials included. The AWS SDK retries a failed call once, and its own credential providers (an IRSA role, an instance profile) use their own timeouts.
- Loading the variables of a run reads at most 8 secret variables from vaults at a time.
When a secret cannot be read
The feature that needs the secret fails with a generic error. An API call, a connection test for example, answers 502 with "A secret stored in a vault could not be read. An administrator can find the details in the server logs." In an agent run, an MCP server whose secret cannot be read is left out, and the other servers still load. The vault's answer never reaches the user.
- The server log holds the details: the provider, the path and the vault's reason (an HTTP status, an AWS error code), never the value.
- The entity page (an integration, an MCP server, a variable, an LLM provider, a channel bot) shows which fields are read from a vault and, for a field whose last read failed, when and why: the vault has no such secret, the vault refused the platform's credentials, the vault did not answer, and so on. A field holding a value that starts with
vault://but is not a valid reference is flagged as such, whether or not it was used. - The security audit (Settings > Security) reports the failures of the last 7 days.
A failure is kept for 7 days in Redis, and cleared by the next successful read of that field. It is shown only while the field still holds the reference that failed: replacing it with a value or with another reference clears it from the entity page and the security audit at once. The record holds the reason, the provider and a keyed fingerprint of the reference, never its path. An MCP header or environment document that references several providers lists each of them.
A CA bundle that cannot be read, or a provider that fails in a way the platform does not recognize, is reported like any other failure (misconfigured or unreachable), never as an internal error.
What stays secret
- A credential read from a vault (the secret of an integration, an MCP server, an LLM provider or a channel bot) lives in the platform's memory only. It is never written to the database, to Redis, to a log or to a trace.
- A secret variable's value is substituted into the text the model reads (a persona, an assignment prompt), so it reaches the model, but not what the run stores, traces or logs: those hold a marker the platform turns back into the value when the run resumes or the conversation continues, and a person sees
[secret]where the model repeated it. This applies to every secret variable, read from a vault or stored in XTM One. See Variables. - An API response never carries a secret, nor the path of a reference: it says which fields are read from which provider.
- An integration export with credentials carries the reference as written, never the value. A duplicate, an import and the Filigran AI Catalog keep the reference as it is and never read the vault. An OpenRouter integration that feeds the platform OpenRouter LLM provider copies its API key reference to the provider as written, and the provider reads it from the vault where it is used.
- The reference itself is stored encrypted, like any secret.
Network and TLS
Vault addresses are declared by you in the environment, so the platform reaches them even on a private network: the outbound URL policy that applies to user-entered addresses does not apply to them. The platform never follows a redirect from a vault, and always verifies its TLS certificate. Point CA_CERT_FILE at your CA bundle when HashiCorp Vault uses a private CA; there is no setting that turns verification off.
A HashiCorp ADDRESS or an AWS ENDPOINT_URL may be a plain http:// URL (a Vault Agent on the same host, a local test). There is no TLS then: off loopback, the platform logs a warning at startup and the security audit reports it (encryption.vault_provider_plain_http).
Prefer a _FILE setting (TOKEN_FILE, SECRET_ID_FILE) or a workload identity to an inline TOKEN or SECRET_ID: a value in the environment is visible to whoever can inspect the container.
Security audit findings
The security audit (Settings > Security) reports, in the Encryption category, once a provider is declared or a secret field holds a value starting with vault://:
| Finding | Severity | Meaning |
|---|---|---|
encryption.vault_provider_refused |
Warning | A SECRET_VAULT_<NAME>_* declaration cannot be used. The finding names the variable to fix. |
encryption.vault_provider_plain_http |
Warning | A provider's address (HashiCorp ADDRESS, Azure VAULT_URL, AWS ENDPOINT_URL) is plain HTTP outside loopback: the secrets and the platform's token cross the network unencrypted. |
encryption.vault_provider_open_writers |
Warning | A provider has WRITERS=all and no PATHS: any user who may edit an integration, an MCP server or a secret variable can reference any secret the platform's vault identity reads. |
encryption.vault_reference_unknown_provider |
Warning | A stored secret references a provider nothing declares. |
encryption.vault_reference_invalid |
Warning | A secret field holds a value starting with vault:// that is not a valid reference (stored before the prefix was reserved), so every use of it fails. The finding names the entity and the field, never the value. |
encryption.vault_reference_failing |
Warning | Secrets that still hold their reference could not be read from their vault in the last 7 days. |
encryption.vault_failures_unknown |
Info | Redis did not answer, so the recent failures could not be checked. No pass finding is reported then. |
encryption.vault_references_resolving |
Pass | Every stored reference names a declared provider and none failed lately. |
Next step
To check that a reference works, save it on an integration and run the integration's connection test: a failure shows on the integration page and in the server log. See Configuration for the other security settings.