06

Capability Profiles: Verbatim Parity First, Then Trim

If `access` is the only grant surface, then the *catalog of profiles* behind it is where least privilege is actually won or lost. This day designs that catalog: how to package today's messy real-world permissions into named profiles without dropping anything, how to split them so you can tighten access later without breaking anyone now, and how to make one list of profiles the single source of truth for the composition, the schema validation, and the admission policy at once.

The migration trap this avoids

The obvious way to introduce per-service identity is to also right-size permissions at the same time — mint the new identity and give it only what you think the service needs. This is how migrations break production: your guess is wrong, some rarely-used permission is missing, and the failure shows up hours later under load. The discipline that avoids it is a hard rule: the first cutover grants byte-for-byte what the service holds today, and trimming happens later, per service, under observation.

Rule of thumb: never re-identify and re-scope in the same step — achieve parity first (new identity, same access, nothing breaks), then trim toward least privilege as a separate, reversible, observed change.

The catalog: named profiles, not raw permissions

A profile is a named bundle of grants — a menu item. A claim's access list references profiles (or the packages that group them), and the composition expands each into the concrete provider grants for its cloud. The same profile can map to a GCP role set and an AWS service set, so one name means "this capability" on either cloud.

ProfilePackageGCP roles (example)AWS services (example)
object-storeStandardstorage.objectAdmins3:*
kmsStandardcloudkms.cryptoKeyEncrypterDecrypterkms:*
messagingStandardpubsub.editor, cloudtasks.enqueuersns:, sqs:
ai-inferenceStandardaiplatform.userbedrock:*
observabilityStandardlogging.logWriter, monitoring.viewercloudwatch:, logs:
secrets-readStandardsecretsmanager (read)
db-adminElevatedalloydb.admin
kms-adminElevatedcloudkms.admin
impersonationElevatediam.serviceAccountTokenCreator
computeElevatedec2:, lambda:, ecr:*
secrets-writeElevatedsecretsmanager (write)

(Representative rows — a real catalog maps every service prefix and role the fleet uses today so the union equals the live baseline exactly.) A dash means that cloud has no IAM-plane equivalent for that capability (for example, a database reached over the network with a secret, not an IAM grant). Rule of thumb: model capabilities as named, cross-cloud profiles rather than raw permissions — the name is what makes a grant reviewable, allowlistable, and portable across clouds even when the underlying permissions aren't.

The split *is* the trim seam

Profiles are grouped into two packages, and the grouping is not cosmetic — it is where future tightening happens. Standard is the confident data-plane core you expect services to keep long-term (object storage, KMS encrypt/decrypt, messaging, logging). Elevated is the broad, admin, or legacy access that is the first thing you'll want to remove (admin roles, impersonation, wide compute). The default cutover claim asks for [standard, elevated] — full parity — and later trimming shrinks each service's Elevated set while Standard stays untouched.

Granular profiles live inside each package so trimming is per-capability: you drop kms-admin from one service's Elevated set without touching its object-store or anyone else's grants. Rule of thumb: design the packaging around how you'll tighten later — put the keep-forever capabilities in one bucket and the trim-me-first capabilities in another, so least-privilege work is a series of small, safe subtractions instead of one scary re-scope.

The ceiling: what no profile may contain

Parity means "everything the service has today" — but some things were never on the app identity, and those become the enforced ceiling rather than a grant. No profile may contain a takeover role (owner/editor, project-IAM-admin, IAM-granting admin roles) or the escalation primitives (assuming arbitrary roles, org/account administration). These are excluded by construction, so even [standard, elevated] can't reach them.

Rule of thumb: distinguish "everything the workload has" from "everything the workload could ask for" — parity copies the former exactly, but the profiles must structurally exclude the escalation primitives, so the ceiling is defined by what no profile contains, not by hoping nobody requests it.

One catalog, three consumers

The catalog earns its keep by being the single source of truth for three different mechanisms. The profile list is authored once (as chart values) and rendered into: the composition's expansion logic, the XRD's CEL allowlist (which rejects an access entry that isn't a known profile), and the admission policy (which rejects rendered resources outside the allowlist). One edit — adding or trimming a profile — propagates to all three, so they can never drift apart.

ConsumerUses the catalog to…
Composition functionExpand each requested profile into provider grants
XRD CEL validationReject a claim whose access isn't ⊆ known profiles
Admission policyReject rendered resources that exceed the allowlist

Rule of thumb: when the same list drives generation and validation and admission, author it once and render the rest from it — three hand-maintained copies of an allowlist will disagree, and the disagreement is exactly where an escalation slips through.

Key takeaways

  • Parity before trim: the first cutover grants byte-for-byte today's access; right-sizing is a separate, per-service, observed step — never re-identify and re-scope at once.
  • Model access as named, cross-cloud profiles, not raw permissions; the name is what makes a grant reviewable, allowlistable, and portable.
  • The Standard/Elevated split is the trim seam: Standard is the keep-forever data-plane core, Elevated is the trim-me-first broad/admin/legacy set; granular profiles inside each make trimming per-capability.
  • Profiles structurally exclude takeover and escalation primitives, so the ceiling is defined by what no profile can contain — not by request-time hope.
  • One catalog is the single source of truth for the composition, the schema allowlist, and the admission policy, so they can't drift.

Checklist

  • [ ] I can explain why granting parity first (then trimming) avoids the classic migration failure.
  • [ ] I can describe what a capability profile is and why named profiles beat raw permissions in a claim.
  • [ ] I can explain why the Standard/Elevated split is chosen around future trimming rather than current convenience.
  • [ ] I can explain the difference between copying today's access exactly and structurally excluding escalation primitives.
  • [ ] I can name the three consumers of the catalog and why a single source of truth prevents escalations from drift.