Skip to main content

Credential Vault

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, 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

TypeValueHeader Injected
API Keyapi_keyX-API-Key: <value>
Bearer Tokenbearer_tokenAuthorization: Bearer <value>
Basic Authbasic_authAuthorization: Basic <base64(value)>
OAuth2 Client Credentialsoauth2_client_credentialsAuthorization: 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

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

FieldTypeRequiredDescription
namestringYesHuman-readable label (1-128 chars)
credential_typestringYesOne of: api_key, bearer_token, basic_auth, oauth2_client_credentials
credential_valuestringYesRaw credential value (1-8192 chars, stored encrypted)
target_domainstringNoDomain this credential is intended for (max 253 chars)
agent_idsstring[]NoAgent IDs allowed to use this credential. Empty list means all agents.
metadataobjectNoArbitrary key-value metadata

Response 201 Created

{
"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

StatusReason
400Invalid credential_type or missing required fields
401Missing or invalid Bearer token
500Internal 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

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

Response 200 OK

{
"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

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

Response 200 OK

{
"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

StatusReason
404Credential 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

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

Response 200 OK

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

Errors

StatusReason
404Credential 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

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

FieldTypeRequiredDescription
new_valuestringYesNew credential value (1-8192 chars)

Response 200 OK

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

Errors

StatusReason
404Credential 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.