- CLAUDE.md: add SensitiveKeysConfig, SensitiveKeysRepository, SensitiveKeysMerger to core admin classes; add SensitiveKeysAdminController endpoint; add PostgresSensitiveKeysRepository; add sensitive keys convention; add admin page to UI structure - Design spec and implementation plan for the feature Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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,
sensitiveKeysis 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
nullif not configured - Response:
Or
{ "keys": ["Authorization", "Cookie", "Set-Cookie", "*password*", "*secret*"] }nullbody (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*"] } pushToAgentsquery param (boolean, defaultfalse):false: saves only, new keys take effect on each app's next config pushtrue: 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": { ... } }pushResultisnullwhenpushToAgents=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
sensitiveKeysfor the merged list (agents don't need to know the split)
PUT /api/v1/config/{application}
sensitiveKeysin the request body represents per-app additions only (not the merged list)globalSensitiveKeysandmergedSensitiveKeysare ignored if sent in the PUT body- On save: only per-app
sensitiveKeysare persisted - On push: server merges global + per-app into the
sensitiveKeysfield 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 cameleer3-common) stores only the per-app additions.
Fan-Out on Global Change
When admin PUTs global keys with pushToAgents=true:
- Save to
server_config - Collect all distinct applications from
application_configtable + live agents in registry - 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 - Return aggregate push results (total apps, total agents, per-app response summary)
- 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 cameleer3-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] }
│<──────────────────────────│