
**Status:** review complete — feeds tier-2 controlplane at-rest encryption
(`bd_2_0-k5t.7`) and the BYO-controlplane question (`bd_2_0-k5t.10`).

## Why this exists

Architecture invariant 2 keeps **sovereign data** out of the controlplane. This
review applies the same instinct one level down, to **sensitivity**: of the
platform metadata the controlplane legitimately holds, *how much* is org-sensitive,
and for each such column what is the least-privilege way to hold it — encrypt it at
rest, keep only a reference/hash, or relocate it enclave-ward so the controlplane
never holds it at all.

Minimizing what the controlplane stores is the **strongest lever on the
controlplane BYOK problem**: the less org-sensitive content lives centrally, the
smaller the "but can I bring my own key for the management plane?" surface becomes.
The governing rule (see [`architecture.md`](architecture.md), "Data at rest & key
custody") is:

> the controlplane may apply a customer-held key only to fields it **stores and
> returns but never interprets**.

Everything the controlplane must *read to function* (route a publish, resolve a
grant, scope a snapshot) cannot be under a customer-only key without inverting
availability and re-coupling the plane to a customer KMS — so for those the ceiling
is **operator-CMK encryption at rest** (tier 2), not customer BYOK.

## Classification tiers

- **Public** — carries no confidentiality weight (surrogate ids, timestamps,
  status/boolean enums). No special handling.
- **Operational** — the controlplane must interpret it to do its job, and it
  reveals tenant *structure* (opaque coordinates: which orgs/groups/agents/enclaves
  exist and how they map). Sensitive to correlation, but not customer *content*.
  Ceiling: operator-CMK at rest; cannot be customer-BYOK (interpreted) and cannot
  relocate (scoping is the controlplane's function).
- **Org-sensitive** — customer content or identity: policy rule bodies, display
  names, principals, auth bindings. Each gets an explicit recommendation below.

Recommendation vocabulary: **encrypt-at-rest** (operator CMK, tier 2) ·
**reference/hash-only** (hold an opaque handle, not the content) · **relocate**
(push enclave-ward or to Zitadel; the controlplane stops holding it) ·
**BYOK-eligible** (stored+returned but never interpreted → a customer key *could*
apply).

## Per-table classification

Sourced from migrations `00001`–`00005` and `sqlc/controlplane/models.go`. Columns
not listed are surrogate `id`/`created_at`/`updated_at`/status enums → **public**.

### `policy_setting`, `policy_agent_override`
| Column | Class | Note |
|---|---|---|
| `value` (JSONB) | **Org-sensitive** | the policy rule body — customer governance content |
| `org_id`, `group_id`, `agent_id`, `key` | Operational | scope coordinates + setting key; interpreted when assembling a snapshot |

**Recommendation — `value`: encrypt-at-rest (operator CMK).** The controlplane
authors these centrally (platform/org/group scopes with locks) and serializes them
into a `policy.Snapshot` it publishes controlplane→enclave. It stores and forwards
the body but does **not** evaluate it (the resolver runs in the enclave gateway), so
it is close to BYOK-eligible — but the snapshot must be readable to *assemble and
publish*, so the safe ceiling is CMK-at-rest, not a customer-only key. Longer-term
relocation lever: policy *authoring* could move enclave-ward for BYO-enclave
tenants, but that is a redesign, not a column change. Near term: encrypt at rest,
and never log/index the plaintext value.

### `enclave` (+ `subject` from 00005)
| Column | Class | Note |
|---|---|---|
| `subject` | **Org-sensitive** | service-account subject the enclave authenticates as (auth binding) |
| `owning_org_id` | Operational | the single org that owns the enclave — customer→infra placement (`bd_2_0-j9r`: one enclave, one org; NOT NULL) |

**Recommendation — `subject`: keep as an opaque reference; encrypt-at-rest.** It is
an authn identifier the controlplane must interpret to bind an enclave, so it is not
BYOK-eligible and cannot relocate. Ensure it stays an *opaque subject/reference*
(not a secret/token); with that, CMK-at-rest is defense-in-depth against
disk/backup exposure. `owning_org_id` is an operational placement coordinate —
opaque, interpreted, kept; correlation risk is inherent to running a management
plane and is mitigated by using non-descriptive ids. (The `enclave_org` join table
and the `shared` column were removed with shared enclaves, `bd_2_0-j9r`.)

### `org`, `"group"`
| Column | Class | Note |
|---|---|---|
| `name` | **Org-sensitive** | customer org/group **display name** — business data |
| `id`, `org_id` | Operational | tenant coordinate (= policy/grant/enclave scope keys) |
| `zitadel_org_id`, `zitadel_group_id` | Operational | external Zitadel identifiers (already references) |

**Recommendation — `name`: relocate / reference-only (strongest lever).** The
controlplane never *interprets* the display name — it keys everything off `id`. And
Zitadel already owns org/group identity (00003 comment: "this schema deliberately
does NOT model users, passwords, sessions or external identities (Zitadel owns
them)"). So the cleanest minimization is **not to store `name` centrally at all** —
resolve it from Zitadel on demand via `zitadel_org_id`/`zitadel_group_id`. If a
denormalized name must stay for UX, it is the one clearly **BYOK-eligible** column
(stored, returned, never interpreted) and can take a customer key. The `zitadel_*`
ids are opaque external references → keep, low sensitivity.

### `grant_`
| Column | Class | Note |
|---|---|---|
| `principal` | **Org-sensitive** | `user:<zitadel-sub>` or `group:<org>:<group>` — identity reference |
| `role`, `capability`, `scope_kind`, `org_id`, `group_id` | Operational | the authorization decision (who-can-do-what-where) |

**Recommendation — `principal`: already a reference; encrypt-at-rest.** It is a
pseudonymous Zitadel subject (not raw PII), and the controlplane **interprets** it
to make authorization decisions — so it is neither BYOK-eligible nor hashable
(hashing would break subject matching/enumeration against Zitadel). Keep it as the
pseudonymous reference it already is and encrypt at rest. The rest of the grant row
is operational authz structure the controlplane owns and must interpret: CMK-at-rest
only, no relocate/BYOK.

### `policy_pack`, `policy_pack_install`
| Column | Class | Note |
|---|---|---|
| `policy_pack.value` (JSONB) | Operational (platform) | allow/deny rule bundle in a **platform-owned catalog** (00004) |
| `policy_pack.name`, `install.(org_id, group_id, agent_id, pack_name)` | Operational | catalog key + install coordinates |

**Recommendation — no special handling beyond at-rest hygiene.** By doctrine a pack
is a platform-owned reusable catalog, not org content, so `value` is *not*
org-sensitive. If org-authored packs are ever introduced, re-classify their `value`
as org-sensitive and fold into the `policy_setting.value` recommendation. Install
rows are operational scope coordinates.

## Summary — what feeds k5t.7 / k5t.10

**Encrypt-at-rest under the operator CMK (tier 2, `k5t.7`)** — org-sensitive content
the controlplane must interpret, so BYOK is out but at-rest protects disk/backups:
- `policy_setting.value`, `policy_agent_override.value`
- `grant_.principal` (+ the interpreted grant structure, as a table-level choice)
- `enclave.subject`

**Relocate / reference-only (shrink what the controlplane holds — do this first):**
- `org.name`, `"group".name` → resolve from Zitadel via `zitadel_*` ids rather than
  storing centrally. This is the single biggest reduction of central org-sensitive
  content and the strongest answer to the controlplane-BYOK question.

**BYOK-eligible (stored, returned, never interpreted) — only if kept at all:**
- `org.name`, `"group".name` (the sole columns meeting the never-interpreted bar).

**Operational, accept as-is (opaque, interpreted coordinates):**
- all `org_id`/`group_id`/`agent_id`/`enclave_id`/`owning_org_id` scope keys,
  `zitadel_*` ids, `role`/`capability`/`scope_kind`, status/enum
  columns. Mitigation is non-descriptive ids, not encryption.

**Net:** after relocating the two `name` columns, the residual org-sensitive
surface on the controlplane is **policy rule bodies, grant principals/structure, and
the enclave auth subject** — all interpreted, all bounded by operator-CMK-at-rest.
There is no residual column that both (a) is org-sensitive and (b) the controlplane
stores-but-never-interprets *and must keep*, which is why the honest BYOK answer for
sovereign data remains **BYO-enclave**, with tier-2 CMK-at-rest as the controlplane's
confidentiality floor.
