> ## Documentation Index
> Fetch the complete documentation index at: https://docs.declaw.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Credential Vault

> Inject secrets into a sandbox's outbound traffic at the egress proxy with vault_refs — the real value lives server-side in OpenBao and never enters the VM, which sees only the declaw:vault-managed placeholder.

The credential vault lets a sandbox use a secret (API key, token, database password) **without the secret value ever entering the VM**. The value lives server-side in [OpenBao](https://openbao.org/) and is injected at the egress proxy on outbound requests to the destinations you scope it to. Inside the VM, the env var holds only the placeholder string `declaw:vault-managed`.

This is the stronger sibling of [Environment Secrets](/security/env-secrets). With env secrets, the value *is* present in the VM (just masked in audit logs and excluded from `get_info()`) — any process inside the sandbox can read it from its environment. With the vault, the value is never delivered to the VM at all: a compromised agent that dumps `/proc`, reads the environment, or exfiltrates the process table sees only the placeholder. The credential is resolved and applied at the proxy layer between the VM and the upstream service.

Secrets are stored and referenced **by name** — there is no team or environment to set up. The value is written to the vault backend (OpenBao) and is **never returned** by any API after creation.

## Using a vault secret in a sandbox

Pass a map of **env var name → secret name** at sandbox create time.

<CodeGroup>
  ```python Python theme={null}
  from declaw import Sandbox

  sbx = Sandbox.create(
      template="base",
      network={"allow_out": ["postman-echo.com"]},
      vault_refs={
          "DEMO_TOKEN": "demo-token",
      },
  )
  ```

  ```typescript TypeScript theme={null}
  import { Sandbox } from "@declaw/sdk";

  const sbx = await Sandbox.create({
    network: { allowOut: ["postman-echo.com"] },
    vaultRefs: {
      DEMO_TOKEN: "demo-token",
    },
  });
  ```

  ```go Go theme={null}
  sbx, err := declaw.Create(ctx,
      declaw.WithTemplate("base"),
      declaw.WithNetwork(declaw.SandboxNetworkOpts{
          AllowOut: []string{"postman-echo.com"},
      }),
      declaw.WithVaultRefs(map[string]string{
          "DEMO_TOKEN": "demo-token",
      }),
  )
  ```

  ```bash CLI theme={null}
  declaw sandbox create \
    --template base \
    --vault-ref DEMO_TOKEN=demo-token
  ```
</CodeGroup>

A vault ref resolves by secret **name** — `demo-token` looks up the secret named `demo-token`. The `--vault-ref` CLI flag is repeatable.

<Warning>
  **Two requirements for injection to actually happen.** A vault ref always hides the value (the VM gets the placeholder), but injection only fires when **both** of these hold:

  1. **The scope's `domain_regex` must use the `~` regex prefix.** An unprefixed entry is an *exact hostname* match, so a regex literal like `^postman-echo\.com$` would never equal the real host and nothing would be injected. Write `~^postman-echo\.com$`.
  2. **The target host must be in the sandbox's network `allow_out`.** Injection happens at the L7 egress proxy. A host that is not in the network policy bypasses the proxy entirely, so there is nothing to inject into.

  Miss either one and the value stays hidden but no credential is attached to the outbound request.
</Warning>

## What the agent sees

The env var inside the VM holds the placeholder — never the real value:

```bash theme={null}
$ printenv DEMO_TOKEN
declaw:vault-managed
```

An outbound request to a **scoped** host carries the injected credential (here, `bearer` injection sets the `Authorization` header). The agent issues a plain request; the proxy attaches the header before forwarding:

```bash theme={null}
$ curl -s https://postman-echo.com/get
# reflected request headers include:
#   "authorization": "Bearer <the-real-value>"
```

A request to a host **not** covered by a scope carries nothing — no credential is leaked to unintended destinations:

```bash theme={null}
$ curl -s https://some-other-host.example/get
# no Authorization header — the secret is scoped to postman-echo.com only
```

The credential never existed anywhere inside the VM. It was fetched worker-side from OpenBao and applied at the proxy. Rotating the secret value takes effect on the next outbound request — no sandbox restart required.

## Storing a secret

A secret stores a value plus one or more **scopes** that tell the egress proxy which destination host the value is for and how to inject it. There are two ways to supply the scopes. The **value is write-only**: it is written to OpenBao and is **never returned** by any API after creation — not by create, list, or get.

<Note>
  The vault is a **cloud-mode** feature and requires an authenticated API key. All operations are scoped to your account.
</Note>

### (a) From a provider preset

Name a built-in provider with `provider=` and the preset supplies the domain regex, injection type, and any required static headers — you only paste the value. The secret `name` defaults to the provider key when omitted. Browse the catalog with `list_presets` / `declaw vault presets`.

<CodeGroup>
  ```python Python theme={null}
  from declaw import VaultClient

  vault = VaultClient()  # uses DECLAW_API_KEY

  secret = vault.create_secret(
      name="openai",
      value="sk-...",         # never returned
      provider="openai",      # supplies scopes
  )
  print(secret.secret_id)
  ```

  ```typescript TypeScript theme={null}
  import { Vault } from "@declaw/sdk";

  const secret = await Vault.createSecret({
    name: "openai",
    value: "sk-...",          // never returned
    provider: "openai",       // supplies scopes
  });
  console.log(secret.secretId);
  ```

  ```go Go theme={null}
  v := declaw.NewVaultClient()
  defer v.Close()

  secret, _ := v.CreateSecret(ctx, declaw.CreateSecretInput{
  	Name:     "openai",
  	Value:    "sk-...",     // never returned
  	Provider: "openai",     // supplies scopes
  })
  fmt.Println(secret.SecretID)
  ```

  ```bash CLI theme={null}
  declaw vault create \
    --name openai \
    --provider openai \
    --value "sk-..."
  ```
</CodeGroup>

### (b) With explicit scopes

For a custom destination, provide the scopes yourself. `domain_regex` uses the vault's `~` prefix to mark a case-insensitive regex match against the request host. The proxy **anchors every scope automatically** (wrapping it as `^(?:…)$`), so a pattern like `api.example.com` matches that host only and never an attacker-suffixed look-alike (`api.example.com.evil.com`) — you may still write explicit `^…$` anchors, but you no longer have to. `injection_type` defaults to `bearer`; an optional `rotation_interval_days` enables a rotation policy.

<CodeGroup>
  ```python Python theme={null}
  from declaw import VaultClient, VaultScope

  vault = VaultClient()

  secret = vault.create_secret(
      name="internal-api",
      value="my-token-value",
      scopes=[
          VaultScope(
              domain_regex=r"~^api\.internal\.example\.com$",
              injection_type="header",
              header_name="X-Api-Key",
          ),
      ],
      rotation_interval_days=30,
  )
  ```

  ```typescript TypeScript theme={null}
  const secret = await Vault.createSecret({
    name: "internal-api",
    value: "my-token-value",
    scopes: [
      {
        domainRegex: "~^api\\.internal\\.example\\.com$",
        injectionType: "header",
        headerName: "X-Api-Key",
      },
    ],
    rotationIntervalDays: 30,
  });
  ```

  ```go Go theme={null}
  secret, _ := v.CreateSecret(ctx, declaw.CreateSecretInput{
  	Name:  "internal-api",
  	Value: "my-token-value",
  	Scopes: []declaw.VaultScope{{
  		DomainRegex:   `~^api\.internal\.example\.com$`,
  		InjectionType: "header",
  		HeaderName:    "X-Api-Key",
  	}},
  	RotationIntervalDays: 30,
  })
  ```

  ```bash CLI theme={null}
  # --scope is repeatable: domain_regex,injection_type[,header_name]
  declaw vault create \
    --name internal-api \
    --value "my-token-value" \
    --rotation-days 30 \
    --scope '~^api\.internal\.example\.com$,header,X-Api-Key'
  ```
</CodeGroup>

## List, rotate, and delete secrets

`list` returns **metadata only** (id, name, scopes, rotation status) — never the value. `rotate` replaces the stored value while leaving the scopes unchanged. Running sandboxes pick up the rotated value on their **next outbound request** (a short-lived TTL cache at the proxy) — no sandbox restart is required. `delete` removes both the metadata and the stored value.

<CodeGroup>
  ```python Python theme={null}
  # List — metadata only
  for s in vault.list_secrets():
      print(s.secret_id, s.name, s.rotation_due)

  # Rotate — value replaced (by name), scopes unchanged
  vault.rotate_secret("internal-api", "sk-new-...")

  # Delete — metadata + stored value (by name)
  vault.delete_secret("internal-api")
  ```

  ```typescript TypeScript theme={null}
  const secrets = await Vault.listSecrets();
  for (const s of secrets) console.log(s.secretId, s.name, s.rotationDue);

  await Vault.rotateSecret("internal-api", "sk-new-...");

  await Vault.deleteSecret("internal-api");
  ```

  ```go Go theme={null}
  secrets, _ := v.ListSecrets(ctx)
  for _, s := range secrets {
  	fmt.Println(s.SecretID, s.Name, s.RotationDue)
  }

  v.RotateSecret(ctx, "internal-api", "sk-new-...")

  v.DeleteSecret(ctx, "internal-api")
  ```

  ```bash CLI theme={null}
  declaw vault list
  declaw vault rotate internal-api --value "sk-new-..."
  declaw vault delete internal-api
  ```
</CodeGroup>

<Note>
  A secret with `rotation_interval_days > 0` is flagged **`rotation_due`** in `list` output once that many days pass since it was last rotated (or created). This is an advisory signal — rotation is not automatic; call `rotate` to clear it.
</Note>

### Update a secret's scopes

`update_scopes` replaces a secret's injection scopes in place — point it at a new destination or change the injection format — **without re-supplying the value**. It's the in-place alternative to delete-and-recreate; running sandboxes pick up the new scopes on their next outbound request.

<CodeGroup>
  ```python Python theme={null}
  from declaw import VaultScope

  vault.update_scopes("internal-api", [
      VaultScope(domain_regex=r"~^api\.v2\.example\.com$", injection_type="header", header_name="X-Api-Key"),
  ])
  ```

  ```typescript TypeScript theme={null}
  await Vault.updateScopes("internal-api", [
    { domainRegex: "~^api\\.v2\\.example\\.com$", injectionType: "header", headerName: "X-Api-Key" },
  ]);
  ```

  ```go Go theme={null}
  v.UpdateScopes(ctx, "internal-api", []declaw.VaultScope{{
      DomainRegex:   `~^api\.v2\.example\.com$`,
      InjectionType: "header",
      HeaderName:    "X-Api-Key",
  }})
  ```

  ```bash CLI theme={null}
  # --scope is repeatable: domain_regex,injection_type[,header_name]
  declaw vault update-scopes internal-api \
    --scope '~^api\.v2\.example\.com$,header,X-Api-Key'
  ```
</CodeGroup>

## Injection types

A secret's scope declares **how** the proxy presents it to the allowed destination. HTTP types inject into the TLS-intercepted request; socket types complete an authentication handshake transparently so the agent connects with no password.

| Type       | Transport | What it does                                                                           |
| ---------- | --------- | -------------------------------------------------------------------------------------- |
| `bearer`   | HTTP      | `Authorization: Bearer <value>`                                                        |
| `header`   | HTTP      | `<header_name>: <value>` (with optional `value_prefix` scheme word)                    |
| `basic`    | HTTP      | `Authorization: Basic <value>` (or `base64(username:value)` via `basic_username`)      |
| `query`    | HTTP      | Inject the value as a URL query parameter (`header_name` names the param)              |
| `sigv4`    | HTTP      | Re-sign the request with AWS SigV4 (stored keys or STS federation)                     |
| `oidc`     | HTTP      | Mint a short-lived OAuth2/OIDC bearer (client\_credentials, token-exchange, discovery) |
| `hmac`     | HTTP      | Sign the request with a configurable HMAC template (GitHub / Stripe / Slack styles)    |
| `redis`    | Socket    | Redis `AUTH` brokering                                                                 |
| `postgres` | Socket    | Postgres auth brokering (cleartext / MD5 / SCRAM-SHA-256)                              |
| `mysql`    | Socket    | MySQL auth brokering (native + caching\_sha2, incl. RSA full-auth)                     |
| `smtp`     | Socket    | SMTP `AUTH` brokering (AUTH PLAIN, STARTTLS)                                           |
| `mongodb`  | Socket    | MongoDB SCRAM-SHA-256 brokering                                                        |

## Scope fields

A scope is one per-destination injection rule on a secret. The proxy matches a request's host against `domain_regex` and injects the value per `injection_type`. All of the static fields below are applied **at the egress proxy after the security scan**, so they never appear in the request the agent issued — they let one scope express a provider's full contract (e.g. Anthropic's required `anthropic-version` header) without a new injection mechanism.

| Field            | Required               | Description                                                                                                                                                                                                     |
| ---------------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `domain_regex`   | Yes                    | Host match. Use the `~` prefix for a case-insensitive regex; the proxy anchors it automatically (`^(?:…)$`) so it cannot over-match a suffixed host. The value is only injected on requests to a matching host. |
| `injection_type` | Defaults to `bearer`   | How the value is presented: `bearer`, `header`, `basic`, `query`, plus brokered forms `sigv4`, `oidc`, `hmac`, and socket forms `redis`, `postgres`, `mysql`, `smtp`, `mongodb`.                                |
| `header_name`    | For `header` / `query` | Header name (e.g. `X-Api-Key`); for `query` it is the URL parameter name. Defaults to `Authorization` for the `header` type.                                                                                    |
| `value_prefix`   | No                     | Prepended verbatim to the value for the `header` type, so a scheme word can be emitted — e.g. `"Token "` → `Authorization: Token <key>`.                                                                        |
| `basic_username` | No                     | For the `basic` type: the proxy emits `base64(username:value)`, letting you store a raw key instead of a pre-encoded credential.                                                                                |
| `extra_headers`  | No                     | Static, **non-secret** headers set alongside the credential (e.g. Anthropic's required `anthropic-version: 2023-06-01`).                                                                                        |
| `query_params`   | No                     | Static, **non-secret** query parameters added to the request URL. For a *secret* in the query string, use `injection_type: "query"` instead.                                                                    |

A single secret may carry multiple scopes — one secret can be injected differently for different destination hosts.

## Provider presets

Declaw ships a built-in catalog of \~39 first-party provider templates (OpenAI, Anthropic, Gemini, Bedrock, Cohere, Mistral, Hugging Face, Pinecone, LangSmith, ElevenLabs, Tavily, and more) so you can store a credential by **naming the provider and pasting the value** — no hand-written domain regex or header (see [Storing a secret](#a-from-a-provider-preset)). The preset expands server-side into the correct scopes at create time; the worker only ever sees the resulting injection specs.

Browse the live catalog:

```bash theme={null}
curl https://api.declaw.ai/vault/presets
```

## BYO secret-store connectors

A secret can hold a **connector descriptor** instead of a literal value — a pointer to your existing secret store. At injection time the worker fetches the real value from that store (cached, per the revocation TTL), so an upstream rotation is picked up automatically on the next cache miss. Implemented connectors:

`aws_sm` · `aws_ssm` · `gcp_sm` · `azure_kv` · `vault` / `openbao` · `infisical` · `doppler` · `k8s` · `1password` (Connect) · `conjur` · `akeyless`

The descriptor names the provider and the path to the value; the literal credential never lives in declaw.

<Note>
  **Secret values live in OpenBao, never in declaw's Postgres.** The control plane stores only metadata — the secret's id, name, scopes, and rotation status. The value is written to the vault backend and is fetched worker-side and injected at the proxy. It never enters the control-plane database and is never returned by the API after creation.
</Note>

<Note>
  **Trust boundary and the cert-pinned limitation.** Vault HTTP injection rides declaw's TLS interception at the egress proxy — the same edge proxy used for PII and injection scanning (see [Network Policies](/security/network-policies)). The proxy terminates TLS, attaches the credential, and re-originates to the upstream. This means an upstream that **certificate-pins** (rejects the proxy's CA) cannot have a credential injected, because the proxy can't sit in the TLS path. For socket brokers, the agent↔proxy leg is always cleartext within the sandbox's own network namespace (where the proxy is the only gateway); the proxy↔upstream leg can opt into TLS (set `tls: true` in the secret value) — `smtp` upgrades via STARTTLS, `redis` and `mongodb` negotiate TLS at connect time, and `postgres` and `mysql` upgrade in-protocol — so a database that *mandates* TLS on the wire is brokered too.
</Note>
