# Tenant onboarding contract -- operator/tenant boundary, intake, topology (DOCFIX-091)

Commissioned 2026-07-06 (operator A1 rulings). This is the authoritative statement of
WHO does WHAT at tenant onboarding and why, for Omega Cloud v1 and carried to Roosevelt.
Companions: `scripts/tenant-onboard.sh` (the ONLY sanctioned execution path),
`runbooks/tenant-onboarding-v2-DRAFT.md` (procedure narrative; superseded points noted
in its banner), `runbooks/appendix-C-identity-rbac.md`, `runbooks/appendix-D-magnum-trust-model.md`.
Decisions: D-051 (Domain Manager persona), D-064 (policy mechanics), D-066 (account
model), D-073 (list_trusts hardening, adopted 2026-07-06).

## 1. Identity model -- roles, explicit (D-066 Option-3 triad)

| identity | auth | role grants (EXACT) | purpose |
|---|---|---|---|
| `<client>-domain-admin` | password | `manager` on the domain -- EXACTLY ONE assignment, nothing else | identity CRUD inside the domain: users, projects, in-domain role grants |
| `<client>-cluster` | password | `member` + `load-balancer_member` on the project | cluster lifecycle (trust-capable); OWNS the nova keypair |
| `<client>-svc` | password + UNRESTRICTED app credential | `member` + `load-balancer_member` on the project | non-trust automation: L3 build-out, templates, day-2 API work |

**The domain admin is the `manager` role, NEVER keystone `admin`.** Keystone `admin`
is architecturally not domain-confinable (a domain-scoped admin can escalate beyond
the boundary -- upstream hard-coded limitation). The persona is the SCS Domain Manager
(scs-0302) enforced by the D-051/D-064 policy override. Any `admin` grant anywhere in
a tenant domain is an incident, not a convenience.

**Why three identities, not one.** Keystone refuses trust creation from app-cred
tokens (regardless of `--unrestricted`), so Magnum cluster create MUST run as a
password identity (`-cluster`). Nova keypairs are USER-scoped and Magnum validates
the keypair in the cluster-creator/trustor context, so the keypair MUST be owned by
`-cluster` (a `-svc`-owned key 400s at cluster create). Identity CRUD stays with the
manager only. This split is PRESCRIPTIVE for tenants, not a suggestion.

## 2. Operator-only surface (and why)

| surface | why it is operator-only |
|---|---|
| domain lifecycle (create/disable/delete) | it IS the tenancy boundary |
| manager account provisioning + password reset | no self-service recovery exists; manager lockout is an operator support case (tenant-side bus factor -- tenants should name two custodians) |
| quotas (nova/cinder/neutron/octavia/magnum) | the capacity envelope is a commercial + capacity-planning decision; the manager role cannot self-raise quotas by design |
| external/provider networks (`provider-ext`), FIP pool | shared substrate; cross-tenant capacity |
| public Glance images, flavors | shared substrate; Magnum-capable image/flavor requirements are operator-validated |
| keystone policy layer (policyd-override zip) | it carries the isolation guarantee itself (D-051/D-064/D-073) |
| cloud control plane (juju, vault, ceph, ovn, octavia mgmt, magnum mgmt) | not tenant-visible, ever |

Everything else is tenant self-service via the triad: projects, users, in-domain role
grants (manager); networks/subnets/routers (gateway to `provider-ext`), security
groups, floating IPs, load balancers, Kubernetes clusters, instances, volumes,
Barbican secrets/certificates, app credentials, keypairs (workload identities).
Tenants never receive SSH or console access to operator infrastructure.

## 3. Tenant intake list (what the tenant provides BEFORE onboarding)

1. **Client short-name** -- lowercase, DNS-safe; becomes the `<client>` prefix on the
   domain and every identity/resource name.
2. **Two named credential custodians** + an out-of-band handoff channel (credentials
   are never sent in email/ticket bodies). Custodian #2 covers the manager-lockout
   bus factor.
3. **Quota ask**: instances / vCPU / RAM, volumes + GB, networks/routers/FIPs, load
   balancers, clusters + node counts. (Maps to the stage-1 quota envelope.)
4. **Tenant CIDR(s)** -- OPTIONAL (D-074): client-choice, RFC1918; fallback
   default 10.100.0.0/24 when the client has no preference. Overlap with OTHER
   TENANTS is explicitly PERMITTED (hard-isolation model; D-074 amends
   D-016's uniqueness carve-out). Stage 4 fails closed only on real subnet
   overlap (containment either way) with OUR reserved ranges (plane /22s,
   capi-mgmt, metadata) or on non-RFC1918. Clients SHOULD still avoid their
   own on-prem/VPN ranges they may later interconnect -- their homework, not
   our guard's.
5. **Workload profile**: Kubernetes yes/no (sizing against the capi templates), LB
   count expectations (amphora capacity), storage expectations.
6. **Authorized requesters** -- who may request quota changes / password resets.
7. (Optional) an SSH public key to import if they prefer their own key over the
   generated `<client>-key`.

## 4. Account creation -- steps, requirements, dangers

`bash scripts/tenant-onboard.sh <client> [stage]` is the only sanctioned path; ad-hoc
onboarding is prohibited (deviations become script changes with a harness). Stages:

| stage | actor/identity | does | danger the script guards |
|---|---|---|---|
| 0 | operator (read-only) | preflight: D-064 policy live (`PO:` active), roles exist, public kube image, clean slate for `<client>` | proceeding onto a half-built or policy-less cloud |
| 1 | operator | domain + manager (+ quota envelope) | admin-instead-of-manager; quota must be set AFTER the project exists (or pre-create it) |
| 2 | manager (their creds) | project + `-cluster` + `-svc` + grants; then an ANTI-ESCALATION self-check (an `admin` grant attempt must be DENIED -- if it lands, STOP: the policy layer is broken) | privilege creep; policy-layer regression |
| 3 | `-svc`, then `-cluster` | `-svc` mints the unrestricted app cred; `-cluster` creates the keypair | THE KEYPAIR TRAP: key must be `-cluster`-owned or stage 6 400s |
| 4 | `-svc` app cred | L3: net/subnet/router/ext-gw to `provider-ext` | CIDR collision (fail-closed guard) |
| 5 | `-svc` app cred | cluster template, image pinned BY UUID, no `--keypair` | keypair on the template would bind the wrong owner; stage 6 supplies it |
| 6 | `-cluster` PASSWORD | cluster create | app-cred cluster create is impossible (keystone blocks trust from app creds) -- do not "simplify" to the app cred |

Credential custody: per-identity 0600 files, never committed, handed off out-of-band
to the named custodians; verify by length/format, never by printing.

## 5. Onboarding confirmation set ("access confirmed" means all of these)

1. **Horizon login** as `<client>-domain-admin`, domain-scoped -- at onboarding.
   VERIFIED LIVE 2026-07-07 (devteam, full three-account walkthrough): login
   succeeds; the dashboard shows the Identity section only; three error popups
   at login (compute usage/limits 403s + a cosmetic follow-on) are EXPECTED for
   this no-project-scope persona and are documented client-side.
2. **API/CLI auth** for all three identities (the script asserts a token per identity
   inline at each stage).
3. **Horizon identity self-service probe** -- manager creates and deletes a probe user
   via the GUI. VERDICT (live 2026-07-07): FAILS as anticipated -- Horizon enforces
   its own local policy copies, which predate the manager persona: no Users panel,
   empty Projects list. Per this clause, CLI IS the documented identity path (client
   docs updated accordingly; the API capability itself is live-proven by stage 2).
   A dashboard-charm policy override is under investigation (see the dashboard
   policy override decision draft); its first live apply was reverted -- panels
   rendered but page-level checks refused, and the Roles panel triggered a session
   logout. Until a revised override passes the browser acceptance walk, this item
   is closed by the CLI-path clause, not open.
4. **Tenant access-proof (the "SSH access" confirmation)**: boot a canary instance on
   the tenant network, attach a FIP, SSH in with the tenant keypair, tear down.
   Proves end-to-end that the tenant can reach workloads inside their domain.
   IMPLEMENTED as onboarding stage 7 (explicit-only canary); executed live for
   foil1 and devteam.

## 6. Hardening against technician knowledge gaps

Existing (in the script today): script-as-procedure; stage-0 fail-closed preflight;
stage-2 anti-escalation self-check; fail-closed CIDR guard; capture-then-test output
validation; whitelist-write 0600 credentials; dynamic ID resolution (no pasted IDs).
Systemic backstop: the D-051/D-064 policy layer bounds what tenant-side identities can
do even if a technician errs; the residual risk is OPERATOR-side error (manual admin
grant, wrong domain reuse, quota fat-finger).

PROPOSED (logged for operator prioritization; none implemented):

- **H1 `tenant-assert.sh`** -- read-only post-onboard verifier: triad exists with the
  EXACT grants of section 1 (and nothing more), keypair owner is `-cluster`, app cred
  owner is `-svc`, manager has exactly one assignment, anti-escalation retest. Also
  serves as an offboard preflight and a periodic all-tenants drift sweep.
- **H2** stage-3 app-cred idempotency re-run guard (already in the logged backlog).
- **H3** canary access-proof stage (section 5 item 4).
- **H4** `keystone-policy-drift.sh` periodic run (script backlog item 6) -- guards the
  policy layer the whole model rests on.
- **H5** runbook rule: NOTHING outside the script; any needed deviation is a script
  change shipped with its harness, not a live improvisation.

**Related:** D-051, D-064, D-066, D-067, D-073, appendix-C, appendix-D,
`scripts/tenant-onboard.sh`, `runbooks/tenant-onboarding-v2-DRAFT.md`.
