Files
cameleer-server/docs/superpowers/specs/2026-04-14-sensitive-keys-server-design.md
hsiegeln cb3ebfea7c
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Failing after 18s
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped
chore: rename cameleer3 to cameleer
Rename Java packages from com.cameleer3 to com.cameleer, module
directories from cameleer3-* to cameleer-*, and all references
throughout workflows, Dockerfiles, docs, migrations, and pom.xml.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 15:28:42 +02:00

9.8 KiB

Sensitive Keys Server-Side Support

Date: 2026-04-14 Status: Approved

Context

The agent team is unifying sensitiveHeaders and sensitiveProperties into a single sensitiveKeys field on ApplicationConfig (see agent spec: "Sensitive Keys Unification + SSE Support + Pattern Matching"). The server must store, merge, and push these keys to agents.

Key requirements beyond the agent contract:

  • Global enforced baseline that admins control
  • Per-app additions (cannot weaken the global baseline)
  • Immediate fan-out option when global keys change
  • SaaS tenant admins configure via the same REST API (no special protocol)

Design Decisions

Decision Choice Rationale
Override hierarchy Global enforced + per-app additive Security: devs cannot accidentally un-mask patterns
Merge semantics Union only, no remove Simplicity; weakening the baseline is not a valid use case
Server defaults None (null until configured) Agents have built-in defaults; avoids drift between server and agent hardcoded lists
Fan-out on global change Admin chooses via pushToAgents param Flexibility; some admins want to batch changes
SaaS propagation SaaS platform calls the same admin API No special SaaS-aware code in the server
Storage Existing server_config table Follows OIDC and thresholds pattern

Merge Rule

final_keys = global_keys UNION per_app_keys
  • Global keys are always included (enforced baseline)
  • Per-app keys are additive only
  • Deduplication is case-insensitive (preserves the casing of the first occurrence)
  • If no global config exists AND no per-app keys exist, sensitiveKeys is omitted from the payload (agents use built-in defaults)
  • If global config exists but is an empty list [], the server sends [] (agents mask nothing — explicit opt-out)

API

Global Sensitive Keys (new)

GET /api/v1/admin/sensitive-keys

  • Auth: ADMIN only
  • Returns the stored global keys or null if not configured
  • Response:
    {
      "keys": ["Authorization", "Cookie", "Set-Cookie", "*password*", "*secret*"]
    }
    
    Or null body (204 No Content) when not configured.

PUT /api/v1/admin/sensitive-keys?pushToAgents=false

  • Auth: ADMIN only
  • Request body:
    {
      "keys": ["Authorization", "Cookie", "Set-Cookie", "*password*", "*secret*"]
    }
    
  • pushToAgents query param (boolean, default false):
    • false: saves only, new keys take effect on each app's next config push
    • true: saves, then fans out config-update to all LIVE agents (recomputes merged list per app)
  • Response: saved config + optional push results
    {
      "keys": ["Authorization", "Cookie", "Set-Cookie", "*password*", "*secret*"],
      "pushResult": { ... }
    }
    
    pushResult is null when pushToAgents=false.

Per-App Config (existing endpoint, enhanced behavior)

GET /api/v1/config/{application}

  • sensitiveKeys: the per-app additions only (what the UI edits and PUTs back)
  • globalSensitiveKeys: read-only field, the current global baseline (for UI to render greyed-out pills)
  • mergedSensitiveKeys: the full merged list (global + per-app) — informational, shows what agents actually receive
  • Note: the agent-facing SSE payload uses sensitiveKeys for the merged list (agents don't need to know the split)

PUT /api/v1/config/{application}

  • sensitiveKeys in the request body represents per-app additions only (not the merged list)
  • globalSensitiveKeys and mergedSensitiveKeys are ignored if sent in the PUT body
  • On save: only per-app sensitiveKeys are persisted
  • On push: server merges global + per-app into the sensitiveKeys field of the SSE payload sent to agents

Storage

No new tables. Uses existing PostgreSQL schema.

Global Keys

-- server_config table
-- config_key = 'sensitive_keys'
-- config_val example:
{ "keys": ["Authorization", "Cookie", "Set-Cookie", "*password*", "*secret*"] }

Per-App Keys

Stored in application_config.config_val as part of the existing ApplicationConfig JSONB. The sensitiveKeys field (added by the agent team in cameleer-common) stores only the per-app additions.

Fan-Out on Global Change

When admin PUTs global keys with pushToAgents=true:

  1. Save to server_config
  2. Collect all distinct applications from application_config table + live agents in registry
  3. For each application: a. Load per-app config (or defaults if none) b. Merge global + per-app keys c. Set merged keys on the ApplicationConfig d. Push via existing pushConfigToAgents() mechanism
  4. Return aggregate push results (total apps, total agents, per-app response summary)
  5. Audit log entry with results

Server Code Changes

New Files

File Module Purpose
SensitiveKeysConfig.java core record SensitiveKeysConfig(List<String> keys)
SensitiveKeysRepository.java core Interface: find(), save()
SensitiveKeysMerger.java core Pure function: merge(List<String> global, List<String> perApp) -> List<String>. Union, case-insensitive dedup, preserves first-seen casing.
PostgresSensitiveKeysRepository.java app/storage Read/write server_config key "sensitive_keys" (follows PostgresThresholdRepository pattern)
SensitiveKeysAdminController.java app/controller GET/PUT /api/v1/admin/sensitive-keys, fan-out logic, audit logging

Modified Files

File Change
ApplicationConfigController.java Inject SensitiveKeysRepository. On GET: merge global keys into response, add globalSensitiveKeys field. On PUT: merge before SSE push.
StorageBeanConfig.java Wire PostgresSensitiveKeysRepository bean

No Schema Migration Required

Uses existing server_config table (JSONB key-value store). The sensitiveKeys field on ApplicationConfig is added by the agent team in cameleer-common — the server just reads/writes it as part of the existing JSONB blob.

Audit

Action Category Detail
view_sensitive_keys CONFIG (none)
update_sensitive_keys CONFIG { keys: [...], pushToAgents: true/false, appsPushed: N, totalAgents: N }

Per-app changes are covered by the existing update_app_config audit entry.

UI

Global Sensitive Keys Admin Page

  • Location: Admin sidebar, new entry "Sensitive Keys"
  • Access: ADMIN only (sidebar entry hidden for non-ADMIN)
  • Components:
    • Info banner at top: "Agents ship with built-in defaults (Authorization, Cookie, Set-Cookie, X-API-Key, X-Auth-Token, Proxy-Authorization). Configuring keys here replaces agent defaults for all applications. Leave unconfigured to use agent defaults."
    • Tag/pill editor for the keys list. Type a key or glob pattern, press Enter to add as a pill. Each pill has an X to remove. Supports glob patterns (*password*, X-Internal-*).
    • "Push to all connected agents immediately" toggle (default off)
    • Save button
  • Empty state: Info banner + empty editor. Clear that agents use their own defaults.

Per-App Sensitive Keys (existing app config page)

  • Location: Within the existing per-app config editor, new section "Additional Sensitive Keys"
  • Components:
    • Read-only pills showing current global keys (greyed out, no X button, visually distinct)
    • Editable tag/pill editor for per-app additions (normal styling, X to remove)
    • Info note: "Global keys (shown in grey) are enforced by your administrator and cannot be removed. Add application-specific keys below."
  • When no global keys configured: Section shows only the editable per-app editor with a note: "No global sensitive keys configured. Agents use their built-in defaults."

SaaS Integration

No server-side changes needed for SaaS. The SaaS platform propagates tenant-level sensitive keys by calling the standard admin API:

PUT https://{tenant-server}/api/v1/admin/sensitive-keys?pushToAgents=true
Authorization: Bearer {platform-admin-token}
{
  "keys": ["Authorization", "Cookie", "*password*", "*secret*"]
}

Each tenant server handles merge + fan-out to its own agents independently.

Sequence Diagrams

Admin Updates Global Keys (with push)

Admin UI                    Server                      Agents
   │                          │                           │
   │ PUT /admin/sensitive-keys│                           │
   │  { keys: [...] }        │                           │
   │  ?pushToAgents=true      │                           │
   │─────────────────────────>│                           │
   │                          │ save to server_config     │
   │                          │                           │
   │                          │ for each app:             │
   │                          │   merge(global, per-app)  │
   │                          │   CONFIG_UPDATE SSE ──────>│
   │                          │                    ACK <───│
   │                          │                           │
   │ { keys, pushResult }    │                           │
   │<─────────────────────────│                           │

Agent Startup

Agent                       Server
  │                           │
  │ GET /config/{app}         │
  │──────────────────────────>│
  │                           │ load per-app config
  │                           │ load global sensitive keys
  │                           │ merge(global, per-app)
  │                           │
  │ { ..., sensitiveKeys: [merged] }
  │<──────────────────────────│