Secure Vault
This document describes the Hippocortex Vault architecture, encryption model, permission tiers, and integration with the memory pipeline.
Overview
The Vault is an encrypted secrets manager built into the Hippocortex platform. It detects, stores, and protects sensitive credentials (API keys, passwords, tokens, connection strings) that agents encounter during operation.
Key properties:
- AES-256-GCM envelope encryption with per-item random IVs
- Deny-by-default access with explicit permission grants
- Full audit trail for every access and mutation
- Automatic secret detection in captured memories
- Vault references replace secrets in stored memories
Encryption Model
Envelope Encryption
The Vault uses a single master key architecture with AES-256-GCM:
VAULT_MASTER_KEY (environment variable, 256-bit)
|
v
+------------------------------------------+
| For each vault item: |
| |
| 1. Generate random 12-byte IV |
| 2. Encrypt plaintext with: |
| - Algorithm: AES-256-GCM |
| - Key: VAULT_MASTER_KEY |
| - IV: random 12 bytes |
| 3. Store: |
| - encrypted_value (ciphertext, b64) |
| - iv (initialization vector, b64) |
| - auth_tag (GCM auth tag, b64) |
| - encryption_key_id ("v1") |
+------------------------------------------+
Encryption Properties
| Property | Guarantee |
|---|---|
| Confidentiality | AES-256 provides 256-bit symmetric encryption |
| Integrity | GCM authentication tag detects any tampering |
| Uniqueness | Random 12-byte IV per item means identical plaintexts produce different ciphertexts |
| Key rotation readiness | encryption_key_id field tracks which key version encrypted each value |
Data at Rest
Encrypted vault items are stored in PostgreSQL:
CREATE TABLE vault_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
vault_id UUID NOT NULL REFERENCES vaults(id),
tenant_id UUID NOT NULL,
title TEXT NOT NULL,
type TEXT NOT NULL, -- api_key, password, token, etc.
encrypted_value TEXT NOT NULL, -- AES-256-GCM ciphertext (base64)
iv TEXT NOT NULL, -- initialization vector (base64)
auth_tag TEXT NOT NULL, -- GCM authentication tag (base64)
encryption_key_id TEXT NOT NULL DEFAULT 'v1',
tags TEXT[] DEFAULT '{}',
notes_encrypted TEXT, -- optional encrypted notes
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
The VAULT_MASTER_KEY is never stored in the database. It exists only in the runtime environment of the API and worker containers.
Decryption Flow
Client requests reveal
|
v
[Auth: is user authenticated?]
|
v
[Permission: does user have reveal/editor/admin on this vault?]
|
v
[Load encrypted item from PostgreSQL]
|
v
[Decrypt with VAULT_MASTER_KEY + stored IV]
|
v
[Verify GCM auth tag (integrity check)]
|
v
[Create audit log entry]
|
v
[Return plaintext to client]
|
v
[Client displays for 30 seconds, then hides]
Permission Tiers
Vault access uses a 5-tier permission model, separate from organization roles.
Permission Hierarchy
admin > editor > reveal > viewer > no_access
| Level | Description |
|---|---|
admin | Full vault control: read, write, reveal, manage permissions, delete vault |
editor | Read, write, and reveal secrets. Cannot manage permissions or delete vault. |
reveal | Read metadata and reveal (decrypt) secret values. Cannot write. |
viewer | Read metadata only (titles, types, tags). Cannot see or reveal values. |
no_access | No access. Used to explicitly deny a principal who would otherwise inherit access. |
Principal Types
Permissions can be granted to four types of principals:
| Type | Description | Example |
|---|---|---|
user | Individual user | Grant Alice editor access |
team | All members of a team | Grant Engineering team viewer access |
role | All users with a specific org role | Grant all admin users admin vault access |
agent | Machine agent identity | Grant Support Agent reveal access |
Permission Resolution
When a user has multiple permission sources, the system resolves to the most specific:
1. Direct user permission (most specific, wins if present)
2. Team membership permission (checked if no direct permission)
3. Role-based permission (checked if no team permission)
4. Default: no_access (deny by default)
This is a first-match model, not a highest-privilege model. A direct viewer grant overrides a team editor grant.
Permission Check Function
// From apps/cloud-api/src/storage/postgres/vault-permissions.ts
function permissionMeetsMinimum(
level: string,
minLevel: VaultPermissionLevel
): boolean {
const HIERARCHY = ["admin", "editor", "reveal", "viewer", "no_access"];
const levelIdx = HIERARCHY.indexOf(level);
const minIdx = HIERARCHY.indexOf(minLevel);
return levelIdx <= minIdx; // lower index = higher privilege
}
Integration with Memory Pipeline
Automatic Secret Detection
The secret detection engine (apps/cloud-api/src/engine/secret-detector.ts) scans all captured text for credentials. When secrets are found:
- A vault item is created with the detected value
- The secret in the memory is replaced with a vault reference
- The memory is stored with the redacted content
Captured text: "Use API key sk-abc123xyz for authentication"
|
v
[Secret detection engine]
|
v
Detected: sk-abc123xyz (OpenAI API key, confidence: 0.85)
|
+-----------+-----------+
| |
v v
[Create vault item] [Redact in memory]
title: "OpenAI API Key" content: "Use API key [vault:item_id]
value: sk-abc123xyz for authentication"
type: api_key
Vault References in Context
When the retrieval engine assembles context packs, vault references are preserved:
{
"content": "The production database uses [vault:item_abc123] for authentication",
"vaultRefs": [
{
"ref": "[vault:item_abc123]",
"itemId": "item_abc123",
"title": "PostgreSQL Connection",
"type": "database_credentials",
"canReveal": true
}
]
}
The consuming agent can then decide whether to reveal the secret (if it has permission) or work with the reference.
Audit Trail
Every vault operation is logged:
| Event | Logged Fields |
|---|---|
vault_created | vaultId, createdBy, name |
item_created | vaultId, itemId, createdBy, type, tags (never the value) |
item_updated | vaultId, itemId, updatedBy, changedFields |
item_deleted | vaultId, itemId, deletedBy |
item_revealed | vaultId, itemId, revealedBy, ipAddress, userAgent |
permission_granted | vaultId, principalType, principalId, level, grantedBy |
permission_revoked | vaultId, principalType, principalId, revokedBy |
Audit Storage
Audit entries are stored in the vault_access_logs table:
CREATE TABLE vault_access_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
vault_id UUID NOT NULL,
item_id UUID,
action TEXT NOT NULL,
actor_id TEXT NOT NULL,
actor_type TEXT NOT NULL, -- 'user' or 'agent'
ip_address TEXT,
user_agent TEXT,
details JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT now()
);
Audit logs are immutable and cannot be deleted through the API.
Security Considerations
Key Management
- The
VAULT_MASTER_KEYmust be a cryptographically random 256-bit key - It is stored as an environment variable, loaded at container startup
- It is never logged, never included in error messages, never exposed through the API
- Key rotation requires re-encrypting all vault items (planned feature)
Threat Model
| Threat | Mitigation |
|---|---|
| Database compromise | Encrypted values are useless without VAULT_MASTER_KEY |
| API server compromise | Attacker needs valid auth + vault permissions to reveal |
| Memory dump | VAULT_MASTER_KEY in process memory; mitigated by container isolation |
| Insider threat | Audit trail logs every reveal with actor identity |
| Brute force | AES-256 is computationally infeasible to brute-force |
| Tampering | GCM auth tag detects any modification to ciphertext |
What the Vault Does NOT Protect Against
- Compromise of the
VAULT_MASTER_KEYitself (if the env var is leaked, all vault items are compromised) - A user with
adminvault permission choosing to exfiltrate revealed values - Side-channel attacks on the encryption process