Skip to content

Secrets Vault

The Shield Secrets Vault provides encrypted secret storage with tenant isolation, versioning, and automatic rotation. It serves as the centralized secret management layer for the ContextUnity service mesh.

Architecture

Project SDK Shield Service
│ │
├── GetSecret(path) ──────────▶│ → SecretBackend.get()
├── PutSecret(path, value) ───▶│ → SecretBackend.put() + encrypt
├── ListSecrets(prefix) ──────▶│ → SecretBackend.list()
└── RotateSecret(path) ───────▶│ → SecretBackend.rotate()
┌────┴────┐
│ Backend │
├─────────┤
│ EnvStore │ ← Default (env vars)
│ PgStore │ ← Planned (PostgreSQL)
│ Vault │ ← Planned (HashiCorp)
└─────────┘

Secret Model

Every secret carries metadata alongside its value:

FieldTypeDescription
pathstrHierarchical path (e.g. project/api-keys/openai)
versionintMonotonically increasing version number
created_atdatetimeWhen this version was created
updated_atdatetimeLast modification timestamp
expires_atdatetime?Optional TTL for auto-expiry
created_bystrIdentity that created this secret
tenant_idstrTenant isolation boundary
tagsdictArbitrary key-value metadata
encryption_backendstrWhich encryption was used (fernet, none)

Backend Factory

The backend is selected via configuration:

from contextunity.shield.secrets.factory import create_secret_backend
# Environment-variable backed (default)
backend = create_secret_backend("env")
# PostgreSQL backed (planned)
backend = create_secret_backend("postgres", dsn="postgresql://...")
# HashiCorp Vault (planned)
backend = create_secret_backend("vault", url="https://vault.example.com")

EnvSecretStore (Default)

The default backend reads secrets from environment variables. Secret paths map to env var names:

Secret PathEnvironment Variable
project/api-keys/openaiPROJECT_API_KEYS_OPENAI
shield/master-keySHIELD_MASTER_KEY

Suitable for development and single-node deployments where secrets are managed via .env files or systemd-creds.

gRPC RPCs

RPCPermissionDescription
GetSecretshield:secrets:readRetrieve a secret by path
PutSecretshield:secrets:writeStore or update a secret
ListSecretsshield:secrets:readList secrets by prefix
RotateSecretshield:secrets:writeCreate a new version

All RPCs require a valid ContextToken with the appropriate permission scope. Tenant isolation is enforced at the backend level.

Protected Namespaces

Shield applies additional rules for high-value namespaces:

  • */api_keys/* reads require coarse shield:secrets:read, tenant scope, and exact shield:secrets:read:{tenant}/api_keys/{suffix}.
  • {project}/prompts/{node} reads require coarse shield:secrets:read, tenant scope, verified token project_id == project, and agent_id == node:{project}:{node}.
  • Writes use coarse shield:secrets:write plus tenant scope. Do not add per-prompt read/write scopes to project policy.

Router uses this contract for Shield-backed prompts: SDK bootstrap publishes canonical prompt text to Shield, Router registration carries only prompt_ref + prompt_version, and Router fetches the prompt at execution time through the attenuated node token.

Encryption at Rest

Secrets are encrypted using Fernet (AES-128-CBC + HMAC-SHA256) before storage:

  • SHIELD_ENCRYPTION_KEY — Fernet key for secret value encryption
  • SHIELD_MASTER_KEY — Fernet key for PKI private key encryption

In production, these keys should be provisioned via systemd-creds rather than .env files. See Systemd Deployment for hardening details.

SDK Integration

During Project Bootstrap, the SDK syncs declared runtime material into Shield:

  1. SDK resolves configured env references locally.
  2. SDK presents a Shield session token with coarse shield:secrets:write.
  3. Shield stores API keys under {tenant}/api_keys/{suffix} and prompts under {project}/prompts/{node} for each allowed tenant.
  4. Router reads API keys and prompts at execution time using an attenuated node token.

This keeps values out of Router registration bundles while preserving tenant-scoped runtime access.