tancheng33/dsh-credentials-vault
HashiCorp Vault backend for the DeepSeek Harness credential seam: central secrets, AppRole machine auth, rotation without restart, and no long-lived provider key on the agent host
Listed
0
Security
Bundle verified
What it does
HashiCorp Vault backend for the credential seam: KV v2/v1, AppRole machine auth, per-operation reads so rotation needs no restart, and compare-and-swap writes.
Best for
- Teams centralizing DSH provider credentials in HashiCorp Vault instead of agent-host files.
- Headless, containerized, or CI agents that can authenticate through AppRole.
- Multi-agent deployments that need key rotation to take effect on subsequent operations without restarting agents.
Not ideal for
- Organizations without a HashiCorp Vault KV v1 or v2 deployment.
- Workflows requiring Vault database, PKI, transit, or dynamic-secret engines rather than a flat KV map.
- Deployments expecting the plugin to prevent an authorized running agent from using resolved credentials, or to emit update events for external Vault edits.
README
dsh-credentials-vault
| English | 中文 |
A HashiCorp Vault backend for the DeepSeek Harness credential seam (ctx.credentials). Provider keys live in Vault; the agent host holds at most a short-lived AppRole-issued token.
Why
The shipped provider, @deepseek-ai/dsh-credentials-local, stores keys in a 0600 YAML document under the harness home. Its own README says where that stops:
The document is
0600under a0700directory, which stops other OS users — not the model. […] That is discretion, not a boundary. A deployment that must keep provider keys away from its own agent cannot get there with file permissions […] and belongs beside this provider as a sibling package.
This is the central answer to that, as distinct from a per-machine OS keychain:
credentials-local |
OS keychain | dsh-credentials-vault |
|
|---|---|---|---|
| Where the key lives | file on the agent host | that machine’s keychain | Vault |
| Headless / container / CI agent | file must be shipped in | no keychain to speak of | works — AppRole login |
| Rotating a key across 20 agents | edit 20 files | 20 machines | one Vault write |
| Who read the key, when | not recorded | not recorded | Vault audit device |
| Bootstrap secret on the host | the key itself | none | a role id + secret id, not the key |
Install
dsh plugin --profile <name> add dsh-credentials-vault
The bundle patch disables credentials-local as it inserts this row: ctx.credentials accepts exactly one implementation per context.
Then point it at your Vault in your profile’s cordis.patch.yml:
- id: credentials-vault
config:
address: https://vault.internal:8200
namespace: '' # Vault Enterprise namespace
mount: secret
path: dsh
kvVersion: 2
tokenRef: VAULT_TOKEN
approleMount: approle
roleIdRef: VAULT_ROLE_ID
secretIdRef: VAULT_SECRET_ID
readOnly: false
environmentWins: true
cacheTtlMs: 0
timeoutMs: 10000
A patch replaces a row’s whole config, so restate every key you want to keep.
Secrets are one flat map at <mount>/<path>, keyed by credential reference — the same shape credentials-local uses in YAML:
vault kv put secret/dsh DEEPSEEK_API_KEY=sk-… OPENAI_API_KEY=sk-…
Configuration
| Key | Default | Meaning |
|---|---|---|
address |
http://127.0.0.1:8200 |
Vault base address. |
namespace |
'' |
Vault Enterprise namespace. Empty sends no header. |
mount |
secret |
KV secrets-engine mount point. |
path |
dsh |
Path under the mount holding the credential map. |
kvVersion |
2 |
KV engine version. v2 gives compare-and-swap and versioning. |
tokenRef |
VAULT_TOKEN |
Env var holding a Vault token. Used when AppRole is not configured. |
approleMount |
approle |
AppRole auth mount path. |
roleIdRef |
VAULT_ROLE_ID |
Env var holding an AppRole role id. |
secretIdRef |
VAULT_SECRET_ID |
Env var holding an AppRole secret id. |
readOnly |
false |
Refuse set/unset entirely. |
environmentWins |
true |
Let an inherited env var shadow Vault, as the local provider does. |
cacheTtlMs |
0 |
Cache the secret map for this many ms. 0 reads per operation. |
timeoutMs |
10000 |
Vault request timeout. |
Nothing secret goes in this file. tokenRef, roleIdRef, and secretIdRef name environment variables carrying the bootstrap credential. The point of the plugin is that long-lived provider keys are not in configuration, and that has to hold for its own bootstrap too.
Authentication
Setting both VAULT_ROLE_ID and VAULT_SECRET_ID selects AppRole; otherwise a static VAULT_TOKEN is used.
AppRole is the machine path and the one worth using: the agent host never holds a provider key, only a role id and a secret id that mint a leased token. The lease is reused until a minute before it expires — a token that expired mid-request would surface as a spurious auth failure on an unrelated call.
Precedence
| Layer | Source id | Writable | Wins |
|---|---|---|---|
| Inherited process environment | env |
no | by default |
| Vault KV map | vault |
yes | otherwise |
This mirrors credentials-local’s honesty rule. A per-run override — DEEPSEEK_API_KEY=… dsh, a CI secret, a container -e — is operator intent for this run, and it cannot be edited from inside the harness. So it wins and is visibly read-only: describe() reports writable: false, and set/unset reject rather than committing a change no reader would ever observe.
Set environmentWins: false to make Vault authoritative; the environment then serves only as a fallback for references Vault does not hold.
Rotation without restart
The seam resolves per operation, and by default so does this provider: cacheTtlMs: 0 means every LLM request reads the current value. Rotate a key in Vault and the next request uses it — no restart, no reload, no cache to bust.
cacheTtlMs above zero trades that immediacy for round trips: a rotation then reaches the agent within the TTL instead of instantly. It exists for deployments where Vault is far away; leave it at 0 unless you have measured a reason.
Concurrency
Writes are read-modify-write on one shared map, under KV v2 compare-and-swap. Two agents storing different references at the same path would otherwise last-write-wins one of them away. On a CAS mismatch the provider re-reads and retries once; a second mismatch surfaces as a genuinely contended path rather than being retried forever.
Tests
56 tests, eight of which run against a real Vault. The unit suite runs against a fake implementing Vault’s contract as this plugin understands it — which is precisely the thing that can be wrong, so the live suite checks the understanding: KV v2’s double nesting, real CAS rejection, real 403 text, and a real AppRole login.
npm test # unit tests only
docker run -d --name vault -p 18200:8200 \
-e VAULT_DEV_ROOT_TOKEN_ID=dsh-test-root --cap-add=IPC_LOCK hashicorp/vault:latest
DSH_VAULT_TEST_ADDR=http://127.0.0.1:18200 npm test
What this does and does not protect
Does: removes the long-lived provider key from the agent host. Centralizes rotation. Puts every credential read in Vault’s audit device. Makes a stolen host disk worth a leased token with a lifetime, not a permanent API key.
Does not: stop the running agent process from using the credentials it is authorized to use. A tool that can make network requests can still spend whatever the resolved key authorizes while the process is live — that is what the key is for. Narrowing what a live agent may reach is a different seam; see dsh-egress-guard for the network side.
Nor does it hide values from a bash tool call that reads the process environment, when environmentWins is supplying them from there. Vault-sourced values are never written to the environment by this plugin.
Limitations
- KV secrets engine only. No database, PKI, transit, or dynamic-secret engines; the seam’s four operations map onto a static key/value store.
- One path, one flat map. Credential references are POSIX identifiers, and this stores them as keys at a single path — the same shape the local provider uses.
-
No lease renewal for the AppRole token, only re-login. Vault’s
auth/token/renew-selfwould extend a lease in place; re-login is simpler and, for a token used every few seconds, indistinguishable. -
No
credentials/updatedfor external Vault edits. The local provider watches its file; this provider has no subscription to Vault. WithcacheTtlMs: 0an external rotation still reaches the next operation — it just does not announce itself.
License
MIT
Frequently Asked QuestionsFAQ
Use the verified command dsh plugin --profile default add github:tancheng33/dsh-credentials-vault in a DSH-enabled shell. The command resolves the public package metadata and keeps the plugin attached to the catalog identity shown on this page.
Compatibility follows the bundle and profile status shown above. If a profile is not detected, keep the plugin disabled there and check the repository documentation before enabling it in production.
The GitHub link and activity metadata are the source of truth for releases and maintenance. Revisit this page after a new release to confirm the catalog has observed the latest version.