Skip to content

Secrets Architecture

┌─────────────────────────────────────────────────────────────────────────────┐
│ SPIRE (Identity Layer) │
│ │
│ SPIRE Server ──issues──► SPIRE Agent ──attests──► spiffe-helper │
│ trust domain: Workload API writes to disk: │
│ tappass.internal socket │
│ /run/spire/certs/ │
│ svid.pem (1h TTL) │
│ svid_key.pem │
│ svid_bundle.pem │
│ jwt_svid.token (5m TTL)│
│ │ │
│ auto-rotates │
│ hourly │
└───────────────────────────────────────────────────────────┼─────────────────┘
│ read-only mount
┌───────────────────────────────────────────────────────────┼─────────────────┐
│ CyberArk Conjur (Secrets Vault) │ │
│ │ │
│ ┌─────────────────────┐ ┌────────────────────────▼───────────┐ │
│ │ Conjur Server │◄───────►│ TapPass ConjurBackend │ │
│ │ │ │ │ │
│ │ Stores: │ auth: │ 1. Read jwt_svid.token from disk │ │
│ │ TAPPASS_ADMIN_ │ JWT │ 2. POST /authn-jwt → Conjur token │ │
│ │ API_KEY │ or │ 3. GET /secrets → secret value │ │
│ │ TAPPASS_JWT_SECRET │ API │ 4. Cache in memory (5min TTL) │ │
│ │ TAPPASS_VAULT_KEY │ key │ │ │
│ │ ANTHROPIC_API_KEY │ │ On error: │ │
│ │ OPENAI_API_KEY │ │ → Return stale cached value │ │
│ │ SSO_CLIENT_SECRET │ │ → Log warning │ │
│ │ POSTGRES_PASSWORD │ │ → Never crash │ │
│ │ WEBHOOK_SECRETS │ │ │ │
│ │ ... │ │ Performance: │ │
│ │ │ │ → Cache hit: 2.4μs │ │
│ │ Audit log: │ │ → Conjur fetch: ~30ms │ │
│ │ who read what when │ │ → 10k reads/sec sustained │ │
│ └─────────────────────┘ └────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ │
│ TapPass secrets.get("name") │
│ │
│ ┌────────────────────┐ ┌──────────────────────────────┐ │
│ │ Is it a secret? │─── yes ───────►│ Vault Backend │ │
│ │ │ │ (Conjur / future: Vault, │ │
│ │ _API_KEY suffix? │ │ AWS SM, Azure KV, ...) │ │
│ │ _SECRET suffix? │ │ │ │
│ │ _PASSWORD suffix? │ │ Cache → fetch → stale → │ │
│ │ _TOKEN suffix? │ │ fallback │ │
│ │ Known secret name? │ └──────────────────────────────┘ │
│ │ │ │
│ │ │─── no ────────► os.environ.get(name) │
│ │ │ (instant, no HTTP, no vault) │
│ └────────────────────┘ │
│ │
│ Only secrets hit the vault. Config never does. │
│ 28 secret names → vault. 39 config names → env. Zero wasted calls. │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ │
│ Consumers — what reads secrets and what reads config │
│ │
│ SECRETS (via vault): CONFIG (via os.environ): │
│ │
│ ┌─────────────────────┐ ┌──────────────────────┐ │
│ │ LLM Gateway │ │ Server config │ │
│ │ ANTHROPIC_API_KEY │ │ TAPPASS_URL │ │
│ │ OPENAI_API_KEY │ │ TAPPASS_PORT │ │
│ │ MISTRAL_API_KEY │ │ TAPPASS_HOST │ │
│ │ DEEPSEEK_API_KEY │ │ TAPPASS_PRODUCTION │ │
│ │ GROQ_API_KEY │ └──────────────────────┘ │
│ └─────────────────────┘ │
│ ┌─────────────────────┐ ┌──────────────────────┐ │
│ │ Vault (encryption) │ │ Database / Redis │ │
│ │ TAPPASS_VAULT_KEY │ │ DATABASE_URL │ │
│ │ TAPPASS_JWT_SECRET │ │ TAPPASS_KV_URL │ │
│ │ TAPPASS_VAULT_DEK │ │ SUPABASE_URL │ │
│ └─────────────────────┘ └──────────────────────┘ │
│ ┌─────────────────────┐ ┌──────────────────────┐ │
│ │ Auth / Identity │ │ Policy / OPA │ │
│ │ TAPPASS_ADMIN_ │ │ TAPPASS_OPA_URL │ │
│ │ API_KEY │ │ TAPPASS_OPA_MODE │ │
│ │ SSO_CLIENT_SECRET │ │ TAPPASS_OPA_CACHE │ │
│ │ TAPPASS_ED25519_KEY│ └──────────────────────┘ │
│ └─────────────────────┘ │
│ ┌─────────────────────┐ ┌──────────────────────┐ │
│ │ Webhooks / Email │ │ SPIRE / Identity │ │
│ │ STRIPE_WEBHOOK_ │ │ SPIFFE_CERT_DIR │ │
│ │ SECRET │ │ SPIFFE_ENDPOINT_ │ │
│ │ REVOLUT_WEBHOOK_ │ │ SOCKET │ │
│ │ SECRET │ │ TAPPASS_SPIFFE_ │ │
│ │ RESEND_API_KEY │ │ TRUST_DOMAINS │ │
│ └─────────────────────┘ └──────────────────────┘ │
│ ┌─────────────────────┐ ┌──────────────────────┐ │
│ │ Pipeline / PII │ │ Observability │ │
│ │ PII_ENCRYPTION_KEY │ │ OTEL_EXPORTER_URL │ │
│ │ AZURE_CS_KEY │ │ TAPPASS_ALERT_URL │ │
│ └─────────────────────┘ └──────────────────────┘ │
│ │
│ Total: ~28 secrets via vault Total: ~39 config via env │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ │
│ Deployment Modes │
│ │
│ install.sh → Step 7: "Secrets Management" │
│ │
│ ┌────────────────────────────┐ ┌────────────────────────────────────┐ │
│ │ Mode A: Env vars │ │ Mode B: CyberArk Conjur │ │
│ │ (default) │ │ (enterprise) │ │
│ │ │ │ │ │
│ │ .env.prod: │ │ .env.prod: │ │
│ │ all secrets + config │ │ config only │ │
│ │ chmod 600 │ │ + CONJUR_APPLIANCE_URL │ │
│ │ │ │ + CONJUR_ACCOUNT │ │
│ │ Containers: │ │ NO secrets in file │ │
│ │ tappass, opa, postgres, │ │ │ │
│ │ redis, spire, tunnel │ │ Containers: │ │
│ │ │ │ all above + conjur, │ │
│ │ deploy.sh uses: │ │ conjur-postgres │ │
│ │ docker-compose.prod.yml │ │ │ │
│ │ │ │ deploy.sh auto-detects: │ │
│ │ │ │ + docker-compose.conjur.yml │ │
│ └────────────────────────────┘ └────────────────────────────────────┘ │
│ │
│ Same code. Same binary. Backend auto-detected from CONJUR_APPLIANCE_URL. │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ Security Properties │
│ │
│ Process: UID 999 (tappass), not root │
│ Capabilities: All inheritable dropped │
│ Core dumps: Disabled (RLIMIT_CORE=0) │
│ Cache: Bounded (500), jittered TTL, LRU eviction │
│ Errors: Stale fallback (no outage on Conjur downtime) │
│ Auth: SPIFFE JWT → API key fallback → fail │
│ Rotation: Auto within 5min TTL, or immediate via /admin/invalidate │
│ Config/Secret: Separated — config never hits vault, secrets never in env │
│ Audit: Every Conjur fetch logged (who, what, when, result) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘