Secret Vault Endpoints

Store and manage encrypted secrets — API keys, connection strings, and other sensitive values used by Bulwark services and agent integrations.

Plaintext secret values are never returned via REST. The vault deliberately exposes no GET secret value endpoint. Stored secrets are vended to authenticated agents through POST /api/v1/agent/sessions/{id}/credentials with policy enforcement and audit logging at the credential service layer. Use the endpoints below to configure the vault; use the credential vending API to consume it.


Authentication

All vault endpoints accept either:

  • A Bulwark JWT (admin role required for write endpoints; any role for read), or
  • An API key with vault:read (read endpoints) or vault:write (write endpoints).

See API Keys for scope details.

Required on every request:

| Header | Description | |--------|-------------| | Authorization | Bearer <jwt-or-api-key> | | X-Bulwark-Tenant | Tenant UUID. The application this vault entry belongs to. |


Store Secret

POST /api/v1/vault/secrets

Encrypts and stores a secret value. Upsert: writing the same (service_name, name) pair overwrites the previous ciphertext. Encryption is per-row with the configured BULWARK_ENCRYPTION_KEY and BULWARK_VAULT_KEY_VERSION.

Required scope: vault:write

Body

{
  "service_name": "kisi",
  "name": "api_key",
  "value": "<plaintext>"
}

| Field | Description | |-------|-------------| | service_name | The vault service this secret belongs to (must already exist via the service-config endpoint, OR will be implicitly referenced — service config is independent of secret storage). | | name | The secret name within the service. Common values: api_key, client_secret, credentials. | | value | The plaintext value to encrypt. Never returned in any response. |

Response 201

{
  "status": "stored",
  "service_name": "kisi",
  "name": "api_key"
}

The plaintext is never echoed back. Every successful store fires a vault.secret_stored audit event.


Configure Service

POST /api/v1/vault/services

Creates or updates a service configuration in the vault registry. Service configs describe how a stored secret should be used — base URL, auth header shape, available operations.

Required scope: vault:write

Body

{
  "name": "kisi",
  "type": "rest_api",
  "base_url": "https://api.kisi.io",
  "available_operations": ["unlock", "list_doors"],
  "dynamic_secret_type": "none",
  "auth_header_name": "Authorization",
  "auth_header_format": "Bearer {api_key}"
}

| Field | Required | Description | |-------|----------|-------------| | name | Yes | Service identifier (matches the service_name used when storing secrets). | | type | Yes | One of rest_api, database, cloud_iam, oauth, custom. | | base_url | No | Base URL for rest_api services. | | available_operations | No | Operation labels surfaced to policy decisions. Empty array if omitted. | | dynamic_secret_type | No | One of none, db_user, cloud_iam, oauth_exchange. Defaults to none. | | auth_header_name | No | Header name for outbound API calls (e.g. Authorization, X-API-Key). | | auth_header_format | No | Header value template. {api_key}, {client_secret}, etc. interpolated from stored secrets at vending time. |

Response 200

{
  "status": "configured",
  "name": "kisi"
}

List Services

GET /api/v1/vault/services

Returns every service config registered for the tenant. Stored secret values are NOT included — this endpoint exposes only the registry shape.

Required scope: vault:read

Response 200

{
  "services": [
    {
      "id": "01j...",
      "tenant_id": "01j...",
      "name": "kisi",
      "type": "rest_api",
      "base_url": "https://api.kisi.io",
      "available_operations": ["unlock", "list_doors"],
      "dynamic_secret_type": "none",
      "config": {
        "auth_header_name": "Authorization",
        "auth_header_format": "Bearer {api_key}"
      },
      "created_at": "2026-05-07T00:00:00Z",
      "updated_at": "2026-05-07T00:00:00Z"
    }
  ],
  "count": 1
}

Get Service

GET /api/v1/vault/services/{name}

Returns a single service config by name.

Required scope: vault:read

Response 200

Same shape as a single entry in the services array above. Returns 404 if the service is not registered for this tenant.


Reading stored secret values

There is no endpoint that returns a stored secret's plaintext. To use a stored secret:

  1. The agent has an active session (via POST /api/v1/agent/sessions).
  2. The agent calls POST /api/v1/agent/sessions/{id}/credentials requesting specific fields from a service.
  3. Bulwark evaluates the credential vending policy. If denied, the request fails with 403. If approval is required, a CIBA out-of-band flow runs.
  4. On success, only the requested fields are returned to the agent for that one call. Re-use within a session is governed by grant TTL and max-uses.

This indirection is deliberate — it lets the same stored secret be policy-gated, audit-logged, and field-scoped per agent session, rather than handed out wholesale.


Audit events

Every vault operation fires an audit event:

| Operation | Event type | Category | |-----------|------------|----------| | Store secret (POST /vault/secrets) | vault.secret_stored | vault | | Read secret (via credential.Service.Vend) | vault.secret_read | vault |

Direct reads fire with actor_type=system, actor_id=vault because the vault layer doesn't have user context. Higher-level callers (credential vending, gateway proxy) emit their own user/agent-actor audit at THEIR layer, so the combined trail shows both "what was read" and "who initiated the request that caused the read."