# 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<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] }
  │<──────────────────────────│
```
