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

PropertyGuarantee
ConfidentialityAES-256 provides 256-bit symmetric encryption
IntegrityGCM authentication tag detects any tampering
UniquenessRandom 12-byte IV per item means identical plaintexts produce different ciphertexts
Key rotation readinessencryption_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
LevelDescription
adminFull vault control: read, write, reveal, manage permissions, delete vault
editorRead, write, and reveal secrets. Cannot manage permissions or delete vault.
revealRead metadata and reveal (decrypt) secret values. Cannot write.
viewerRead metadata only (titles, types, tags). Cannot see or reveal values.
no_accessNo access. Used to explicitly deny a principal who would otherwise inherit access.

Principal Types

Permissions can be granted to four types of principals:

TypeDescriptionExample
userIndividual userGrant Alice editor access
teamAll members of a teamGrant Engineering team viewer access
roleAll users with a specific org roleGrant all admin users admin vault access
agentMachine agent identityGrant 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:

  1. A vault item is created with the detected value
  2. The secret in the memory is replaced with a vault reference
  3. 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:

EventLogged Fields
vault_createdvaultId, createdBy, name
item_createdvaultId, itemId, createdBy, type, tags (never the value)
item_updatedvaultId, itemId, updatedBy, changedFields
item_deletedvaultId, itemId, deletedBy
item_revealedvaultId, itemId, revealedBy, ipAddress, userAgent
permission_grantedvaultId, principalType, principalId, level, grantedBy
permission_revokedvaultId, 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_KEY must 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

ThreatMitigation
Database compromiseEncrypted values are useless without VAULT_MASTER_KEY
API server compromiseAttacker needs valid auth + vault permissions to reveal
Memory dumpVAULT_MASTER_KEY in process memory; mitigated by container isolation
Insider threatAudit trail logs every reveal with actor identity
Brute forceAES-256 is computationally infeasible to brute-force
TamperingGCM auth tag detects any modification to ciphertext

What the Vault Does NOT Protect Against

  • Compromise of the VAULT_MASTER_KEY itself (if the env var is leaked, all vault items are compromised)
  • A user with admin vault permission choosing to exfiltrate revealed values
  • Side-channel attacks on the encryption process