# 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: ```json { "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: ```json { "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 ```json { "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 ```sql -- 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 keys)` | | `SensitiveKeysRepository.java` | core | Interface: `find()`, `save()` | | `SensitiveKeysMerger.java` | core | Pure function: `merge(List global, List perApp) -> List`. 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] } │<──────────────────────────│ ```