---
title: "Credential Vault"
description: "Encrypted storage for external API credentials used by AI agents, with per-agent scoping and automatic egress proxy injection"
canonical: "https://docs.ainative.studio/docs/agent-cloud/credential-vault"
last-updated: "2026-10-13T21:42:35.442Z"
---

# Credential Vault

Source: https://docs.ainative.studio/docs/agent-cloud/credential-vault

> Encrypted storage for external API credentials used by AI agents, with per-agent scoping and automatic egress proxy injection

The Credential Vault provides encrypted storage for external API credentials used by AI agents. Agents operating within AINative Studio often need to call third-party APIs (OpenAI, Stripe, GitHub, etc.), but giving agents direct access to raw API keys creates security risks: keys can be leaked in logs, exfiltrated through prompt injection, or accidentally exposed in agent outputs.

The Credential Vault solves this by:

- Encrypting all credentials at rest with AES-256-GCM
- Never returning raw credential values through any API endpoint
- Scoping credentials to specific agents (per-agent access control)
- Injecting credentials automatically at egress proxy time
- Maintaining a full audit trail of all credential operations

Agents interact with external APIs through the [Egress Proxy](egress-proxy), which retrieves and injects credentials from the vault. The agent never sees the raw secret.

Refs #2287, #2299

## Authentication

All Credential Vault endpoints require a valid Bearer token in the `Authorization` header.

```
Authorization: Bearer <your-jwt-token>
```

Credentials are scoped to the authenticated user. A user can only list, read, rotate, or delete their own credentials.

## Base URL

```
https://api.ainative.studio/api/v1/cloud/credentials
```

## Credential Types

| Type | Value | Header Injected |
|------|-------|-----------------|
| API Key | `api_key` | `X-API-Key: <value>` |
| Bearer Token | `bearer_token` | `Authorization: Bearer <value>` |
| Basic Auth | `basic_auth` | `Authorization: Basic <base64(value)>` |
| OAuth2 Client Credentials | `oauth2_client_credentials` | `Authorization: Bearer <value>` |

## Endpoints

### POST / --- Store a new credential

Store an encrypted credential in the vault. The raw value is encrypted with AES-256-GCM before being written to the database and is never stored in plaintext.

**Request**

```bash
curl -X POST https://api.ainative.studio/api/v1/cloud/credentials \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "OpenAI Production Key",
    "credential_type": "bearer_token",
    "credential_value": "sk-proj-abc123def456ghi789",
    "target_domain": "api.openai.com",
    "agent_ids": ["agent-001", "agent-002"],
    "metadata": {"environment": "production"}
  }'
```

**Request Body**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | Human-readable label (1-128 chars) |
| `credential_type` | string | Yes | One of: `api_key`, `bearer_token`, `basic_auth`, `oauth2_client_credentials` |
| `credential_value` | string | Yes | Raw credential value (1-8192 chars, stored encrypted) |
| `target_domain` | string | No | Domain this credential is intended for (max 253 chars) |
| `agent_ids` | string[] | No | Agent IDs allowed to use this credential. Empty list means all agents. |
| `metadata` | object | No | Arbitrary key-value metadata |

**Response** `201 Created`

```json
{
  "id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
  "name": "OpenAI Production Key",
  "credential_type": "bearer_token",
  "target_domain": "api.openai.com",
  "agent_ids": ["agent-001", "agent-002"],
  "masked_value": "sk-****i789",
  "metadata": {"environment": "production"},
  "created_at": "2026-06-01T12:00:00+00:00",
  "updated_at": "2026-06-01T12:00:00+00:00"
}
```

Note that `credential_value` is never present in any response. Only `masked_value` is returned, showing the first 3 and last 4 characters.

**Errors**

| Status | Reason |
|--------|--------|
| 400 | Invalid `credential_type` or missing required fields |
| 401 | Missing or invalid Bearer token |
| 500 | Internal encryption or storage failure |

---

### GET / --- List credentials (masked)

List all active credentials for the authenticated user. Values are always masked. Soft-deleted credentials are excluded.

**Request**

```bash
curl -X GET https://api.ainative.studio/api/v1/cloud/credentials \
  -H "Authorization: Bearer $TOKEN"
```

**Response** `200 OK`

```json
{
  "credentials": [
    {
      "id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
      "name": "OpenAI Production Key",
      "credential_type": "bearer_token",
      "target_domain": "api.openai.com",
      "agent_ids": ["agent-001", "agent-002"],
      "masked_value": "sk-****i789",
      "metadata": {"environment": "production"},
      "created_at": "2026-06-01T12:00:00+00:00",
      "updated_at": "2026-06-01T12:00:00+00:00"
    },
    {
      "id": "b2c3d4e5-6789-01bc-defg-2345678901bc",
      "name": "GitHub PAT",
      "credential_type": "bearer_token",
      "target_domain": "api.github.com",
      "agent_ids": [],
      "masked_value": "ghp****ab12",
      "metadata": {},
      "created_at": "2026-05-28T09:30:00+00:00",
      "updated_at": "2026-05-28T09:30:00+00:00"
    }
  ],
  "total": 2
}
```

---

### GET /\{credential_id\} --- Get credential metadata

Retrieve metadata for a single credential. The raw value is never returned.

**Request**

```bash
curl -X GET https://api.ainative.studio/api/v1/cloud/credentials/a1b2c3d4-5678-90ab-cdef-1234567890ab \
  -H "Authorization: Bearer $TOKEN"
```

**Response** `200 OK`

```json
{
  "id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
  "name": "OpenAI Production Key",
  "credential_type": "bearer_token",
  "target_domain": "api.openai.com",
  "agent_ids": ["agent-001", "agent-002"],
  "masked_value": "sk-****i789",
  "metadata": {"environment": "production"},
  "created_at": "2026-06-01T12:00:00+00:00",
  "updated_at": "2026-06-01T12:00:00+00:00"
}
```

**Errors**

| Status | Reason |
|--------|--------|
| 404 | Credential not found or belongs to another user |

---

### DELETE /\{credential_id\} --- Revoke/delete a credential

Soft-delete a credential. The encrypted value remains in the database (for audit purposes) but the credential can no longer be used by the egress proxy or listed via the API.

**Request**

```bash
curl -X DELETE https://api.ainative.studio/api/v1/cloud/credentials/a1b2c3d4-5678-90ab-cdef-1234567890ab \
  -H "Authorization: Bearer $TOKEN"
```

**Response** `200 OK`

```json
{
  "status": "deleted",
  "id": "a1b2c3d4-5678-90ab-cdef-1234567890ab"
}
```

**Errors**

| Status | Reason |
|--------|--------|
| 404 | Credential not found, already deleted, or belongs to another user |

---

### POST /\{credential_id\}/rotate --- Rotate credential value

Replace the encrypted value of an existing credential with a new one. The old encrypted value is overwritten. The credential ID, name, and all other metadata remain the same.

**Request**

```bash
curl -X POST https://api.ainative.studio/api/v1/cloud/credentials/a1b2c3d4-5678-90ab-cdef-1234567890ab/rotate \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "new_value": "sk-proj-newkey-xyz789abc456"
  }'
```

**Request Body**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `new_value` | string | Yes | New credential value (1-8192 chars) |

**Response** `200 OK`

```json
{
  "id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
  "name": "OpenAI Production Key",
  "masked_value": "sk-****c456",
  "rotated_at": "2026-06-01T14:30:00+00:00"
}
```

**Errors**

| Status | Reason |
|--------|--------|
| 404 | Credential not found or belongs to another user |

---

## Security Model

### Encryption

- **Algorithm**: AES-256-GCM (authenticated encryption with associated data)
- **Key derivation**: SHA-256 hash of the `ENCRYPTION_SECRET` environment variable produces the 256-bit key
- **Nonce**: 96-bit (12-byte) random nonce per encryption, as recommended by NIST SP 800-38D
- **Storage format**: Base64-encoded concatenation of `nonce || ciphertext`
- **Integrity**: GCM mode provides built-in authentication, detecting any tampering with the ciphertext

### Per-Agent Scoping

When you store a credential with a non-empty `agent_ids` list, only those specific agents can use the credential through the egress proxy. If `agent_ids` is empty, any agent owned by the user can use the credential.

The egress proxy calls `decrypt_for_egress(credential_id, agent_id)` internally. If the requesting agent is not in the allowed list, decryption is denied and the request fails with a 403.

### Masking

The API never returns raw credential values. The `masked_value` field shows the first 3 and last 4 characters of the original value (e.g., `sk-****i789`). For credentials 8 characters or shorter, the masked value is simply `****`.

### Audit Logging

Every credential operation (store, delete, rotate) is recorded in the `credential_vault_audit_log` table with:

- Credential ID
- Actor ID (the authenticated user)
- Action performed
- Timestamp
- Relevant metadata (name, type, domain -- never the raw value)

Raw credential values are never written to any log, database column (other than `encrypted_value`), or application output.

### Data at Rest

Credentials are stored in the `credential_vault` table. The `encrypted_value` column contains the AES-256-GCM ciphertext. Soft-deleted credentials (`deleted_at IS NOT NULL`) are excluded from all queries and cannot be used for egress proxy injection.

## How Credentials Flow to the Egress Proxy

```
Agent                Egress Proxy              Credential Vault
  |                       |                          |
  |-- POST /egress/request -->                       |
  |   (credential_id)    |                          |
  |                       |-- decrypt_for_egress --> |
  |                       |   (credential_id,        |
  |                       |    agent_id)             |
  |                       |<-- {type, value} --------|
  |                       |                          |
  |                       |-- inject auth header     |
  |                       |-- forward to target API  |
  |                       |                          |
  |<-- proxied response --|                          |
```

1. Agent submits an outbound request to the egress proxy, referencing a `credential_id`.
2. The egress proxy calls `decrypt_for_egress()` with both the credential ID and the agent ID.
3. If the agent is authorized, the vault decrypts the value and returns it (in memory only).
4. The proxy injects the appropriate auth header based on `credential_type`.
5. The proxy executes the request against the target API.
6. The agent receives the response. At no point does the agent see the raw credential.

For full egress proxy documentation, see [Egress Proxy](egress-proxy).
