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:
| Field | Type | Description |
|---|---|---|
path | str | Hierarchical path (e.g. project/api-keys/openai) |
version | int | Monotonically increasing version number |
created_at | datetime | When this version was created |
updated_at | datetime | Last modification timestamp |
expires_at | datetime? | Optional TTL for auto-expiry |
created_by | str | Identity that created this secret |
tenant_id | str | Tenant isolation boundary |
tags | dict | Arbitrary key-value metadata |
encryption_backend | str | Which 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 Path | Environment Variable |
|---|---|
project/api-keys/openai | PROJECT_API_KEYS_OPENAI |
shield/master-key | SHIELD_MASTER_KEY |
Suitable for development and single-node deployments where secrets are managed via .env files or systemd-creds.
gRPC RPCs
| RPC | Permission | Description |
|---|---|---|
GetSecret | shield:secrets:read | Retrieve a secret by path |
PutSecret | shield:secrets:write | Store or update a secret |
ListSecrets | shield:secrets:read | List secrets by prefix |
RotateSecret | shield:secrets:write | Create 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 coarseshield:secrets:read, tenant scope, and exactshield:secrets:read:{tenant}/api_keys/{suffix}.{project}/prompts/{node}reads require coarseshield:secrets:read, tenant scope, verified tokenproject_id == project, andagent_id == node:{project}:{node}.- Writes use coarse
shield:secrets:writeplus 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 encryptionSHIELD_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:
- SDK resolves configured env references locally.
- SDK presents a Shield session token with coarse
shield:secrets:write. - Shield stores API keys under
{tenant}/api_keys/{suffix}and prompts under{project}/prompts/{node}for each allowed tenant. - 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.