diff --git a/clientdocs/README.md b/clientdocs/README.md new file mode 100644 index 0000000..bc564c6 --- /dev/null +++ b/clientdocs/README.md @@ -0,0 +1,22 @@ +# clientdocs/ -- client-facing document package (INTERNAL README) + +This directory holds everything we hand to a client, separated from internal +docs/runbooks by operator ruling (2026-07-06). Everything here is a DRAFT / +living document: accurate, but without final client polish. + +Rules for this directory: + +- **No internal identifiers.** Decision numbers, fix numbers, ticket ids, + internal hostnames/IPs, and runbook names never appear in client documents. + (This also keeps these files out of the internal numbering scans.) +- **Client language.** Second person, no charm/Juju/deployment internals. + Internal mechanics live in docs/tenant-onboarding-contract.md, which is the + authoritative source these documents are derived from -- when the contract + changes, sweep this directory. +- **Packaging.** A client receives a copy (or export) of the relevant files, + never a repo link. Which file goes when: + - `intake-form.md` -- BEFORE onboarding (their homework). + - `welcome.md` + `self-service-guide.md` -- AT credential handoff. +- **ASCII + LF**, like everything committed. + +Contents: `intake-form.md`, `welcome.md`, `self-service-guide.md`. diff --git a/clientdocs/intake-form.md b/clientdocs/intake-form.md new file mode 100644 index 0000000..4bfc130 --- /dev/null +++ b/clientdocs/intake-form.md @@ -0,0 +1,69 @@ +# Omega Cloud -- Client Intake Form (DRAFT) + +Please complete every section and return it through your account contact. +We use these answers to build your environment right the first time; missing +answers are the most common cause of onboarding delay. + +## 1. Organization + +- Organization name: ______________________________________________ +- Short name (lowercase, letters/digits/hyphen only -- this becomes the prefix + on your cloud accounts and resources, e.g. `acme`): ________________ + +## 2. Credential custodians (exactly two) + +Credentials are handed over out-of-band (never email or ticket text) to the +two people named here. Custodian 2 is your recovery path if custodian 1 is +unavailable -- account password resets require a request from a named +custodian. + +| | Name | Role/title | Contact (phone + secure channel) | +|---|---|---|---| +| Custodian 1 | | | | +| Custodian 2 | | | | + +## 3. Authorized requesters + +Who may request quota changes, password resets, or other operator actions on +your behalf (custodians are included automatically): + +- ______________________________________________ +- ______________________________________________ + +## 4. Capacity request (initial quota) + +| Resource | Requested | +|---|---| +| Virtual machines (instances) | | +| Total vCPUs | | +| Total RAM (GB) | | +| Block storage volumes / total GB | | +| Networks / routers | | +| Public (floating) IPs | | +| Load balancers | | +| Kubernetes clusters / total worker nodes | | + +Quotas can be raised later by request; they are your capacity envelope, not a +hard commitment. + +## 5. Network ranges + +Private address range(s) you want your cloud networks to use (RFC1918, e.g. +`10.x.x.0/24`). IMPORTANT: choose ranges that do NOT overlap your on-premises +or VPN networks if you ever plan to connect them to this environment. + +- Primary range: ______________________ +- Additional ranges (optional): ______________________ + +## 6. Workload profile + +- Kubernetes needed? (yes/no) -- if yes: expected cluster count and node sizing: + ______________________________________________ +- Expected load balancer count: ______________ +- Storage expectations (databases, backups, large media, etc.): + ______________________________________________ + +## 7. Optional + +- SSH public key to import for your team (otherwise we generate one and hand + it to your custodians): ______________________________________________ diff --git a/clientdocs/self-service-guide.md b/clientdocs/self-service-guide.md new file mode 100644 index 0000000..22ce701 --- /dev/null +++ b/clientdocs/self-service-guide.md @@ -0,0 +1,72 @@ +# Omega Cloud -- Self-Service Guide (DRAFT) + +How to run your environment day to day. You can do everything here yourself, +through the web dashboard or the OpenStack command-line client. + +## Your three accounts, and what each is for + +| Account | Use it for | Do NOT use it for | +|---|---|---| +| `-domain-admin` | Creating/removing team users and projects, granting roles inside your domain | Running workloads, automation | +| `-cluster` | Creating and deleting Kubernetes clusters; it holds the SSH keypair for cluster nodes | General automation, team logins | +| `-svc` | Scripted/automated work via its application credential: networks, VMs, load balancers, day-2 operations | Kubernetes cluster create/delete | + +Three rules that prevent the three most common self-inflicted outages: + +1. **Kubernetes clusters are created with the `-cluster` account, logged in + with its password.** The platform's cluster machinery cannot work through + an application credential -- a cluster create attempted with the `-svc` + application credential will fail every time. +2. **The SSH keypair for cluster nodes belongs to the `-cluster` account.** + Do not recreate it under another account; clusters will refuse to build + with someone else's key. +3. **Your `-domain-admin` account has exactly one job: identity.** Give team + members their own users and grant `member` on the project; don't hand out + the admin account's password. + +## Creating team users (as `-domain-admin`) + +1. Create the user in your domain. +2. Grant them `member` (and `load-balancer_member` if they manage load + balancers) on your project. +3. They log in to the dashboard with your domain name + their username. + +If a dashboard identity page refuses an action your account should be able to +do, use the command-line client for that step and let us know -- the API +always has the full capability set. + +## Building your network (first-time order) + +1. Network, then a subnet on it (use the address range from your intake form). +2. Router; set its gateway to the shared external network (`provider-ext`). +3. Attach your subnet to the router. +4. Security groups: open only what you need (SSH from your ranges, your app + ports). +5. Public (floating) IPs: allocate from the external network and attach to + instances or load balancers. + +## Virtual machines + +Boot from the shared base images or upload your own private images. Use your +SSH keypair; password logins are disabled on the base images. Attach volumes +for data you want to outlive the instance. + +## Load balancers + +Create load balancers on your networks (you hold the `load-balancer_member` +role already). TLS certificates for listeners are stored in the platform's +secrets service under your project -- upload the certificate there and +reference it from the listener. + +## Kubernetes + +Log in as `-cluster` (password), pick the shared cluster template or +your own, and create the cluster. Node counts are limited by your quota. +The cluster gets its own load balancer for the API and can publish Services +of type LoadBalancer through the same mechanism. + +## When you need us instead + +- Quota raise, password reset, custodian change: via an authorized requester. +- Anything about the shared substrate (new base image, new machine size, + external connectivity): via your account contact. diff --git a/clientdocs/welcome.md b/clientdocs/welcome.md new file mode 100644 index 0000000..901a6db --- /dev/null +++ b/clientdocs/welcome.md @@ -0,0 +1,52 @@ +# Welcome to Omega Cloud (DRAFT) + +You now have a private, isolated environment ("domain") on Omega Cloud. This +document explains what we manage, what you manage, and how we work together. + +## What you received at handoff + +- Your **domain**: a hard isolation boundary. Nothing you create inside it is + visible to any other client, and no other client's activity is visible to you. +- Your **administrator account** (`-domain-admin`): manages your + team's users and projects inside your domain. This account and its scope are + described in the Self-Service Guide. +- Two **workload accounts** (`-cluster` and `-svc`) preconfigured the way the + platform requires -- see the Self-Service Guide before changing anything + about them. +- A **capacity envelope (quota)** sized from your intake form. +- Access to shared platform resources: the external network for public + connectivity, base operating-system and Kubernetes images, and a set of + machine sizes (flavors). + +## What we manage (and you cannot) + +- The cloud infrastructure itself, including the isolation between clients. +- Your domain's existence, your administrator account, and password resets + for it (there is deliberately no self-service recovery -- resets require a + request from one of your named credential custodians). +- Quotas. Your administrator cannot raise them; an authorized requester asks + us instead. +- The shared substrate: external networks, public base images, machine sizes. + +## What you manage (self-service, no ticket needed) + +Everything inside your domain: team user accounts, projects, private +networks and routers, firewall rules (security groups), virtual machines, +storage volumes, load balancers, Kubernetes clusters, application secrets +and certificates, and public IP attachments -- all within your quota. + +## How to reach us + +- Quota changes, password resets, custodian changes: request from an + authorized requester (named in your intake form) via your account contact. +- Incidents/outages: your account contact, marked urgent. +- Everything else self-service: see the Self-Service Guide. + +## House rules (the short version) + +1. Never share account passwords between people; your administrator creates + individual users instead. +2. Never ask us to grant "admin" on your accounts -- the platform will refuse, + by design. Your `-domain-admin` account already has every right you need + inside your domain. +3. Keep your two named credential custodians current with us. diff --git a/docs/handoff-20260705-open-items.md b/docs/handoff-20260705-open-items.md index fa39be8..145082a 100644 --- a/docs/handoff-20260705-open-items.md +++ b/docs/handoff-20260705-open-items.md @@ -41,6 +41,8 @@ - **d011-04** OCCM LB-name-contains-service-name assumption; agnhost `/hostname` round-robin behavior. - **d011-04** sequence: run non-disruptively first (RR + member failover), THEN `--include-disruptive` once N+1 amphora headroom is confirmed (a cloud at its scheduler ceiling cannot heal its own LBs). + OPERATOR RULING 2026-07-06 (A2): the disruptive half is PRE-APPROVED conditional on the live + headroom check passing -- proceed in the same window, no second ask. - **d011-05** needs a FOIL (2nd) tenant for the P3 isolation check -- onboard one or set `VR_FOIL_APPCRED` (acme, the former foil, was offboarded). @@ -81,7 +83,7 @@ scripts + internal docs + drafted client-facing docs (drafts/living, no final polish needed). Items (H-numbers per docs/tenant-onboarding-contract.md section 6): - H1 `tenant-assert.sh` post-onboard verifier, with harness (also offboard preflight + - periodic drift sweep). + periodic drift sweep). DELIVERED addendum 22; live validation pending (foil onboarding). - H2 stage-3 app-cred idempotency re-run guard in tenant-onboard.sh. - H3 canary access-proof stage (boot canary + FIP + SSH via tenant keypair + teardown) -- the ruled meaning of "confirm tenant SSH access into their domain" (A1.4 confirmed). @@ -89,7 +91,8 @@ - H5 no-ad-hoc rule embedded in the onboarding runbook. - Horizon manager-role GUI identity probe (contract section 5.3) validated live. - Client-facing draft package (intake form, welcome/engagement doc, self-service guide) - -- repo location per the structure ruling (pending). + -- RULED: top-level clientdocs/; drafts DELIVERED addendum 22 (living docs, sweep on + contract-doc changes). - Full workflow validated on a live onboarding end-to-end (the d011-05 foil tenant is the natural first candidate). diff --git a/docs/session-ledger.md b/docs/session-ledger.md index ee88997..ce9ab32 100644 --- a/docs/session-ledger.md +++ b/docs/session-ledger.md @@ -31,8 +31,8 @@ - **OPEN security rows:** SEC-001 (rotate at v1 close, re-ruled 2026-07-06), SEC-003 (custodians + second-person unseal -- DEFERRED; keeps d011-06 MANUAL), SEC-004 (flip repo to private at v1 close -- deferral reaffirmed). -- **Next-free numbers:** D = 074, DOCFIX = 092, BUNDLEFIX = 012. - (Scan and the addendum-21 changelog next-free pointer agree.) +- **Next-free numbers:** D = 074, DOCFIX = 093, BUNDLEFIX = 012. + (Scan and the addendum-22 changelog next-free pointer agree.) --- @@ -206,7 +206,13 @@ interpretation CONFIRMED + adopted as pre-deployment task. Full onboarding completion package (scripts + internal docs + client-facing drafts + live end-to-end validation) added to handoff section 6 v1-close checklist. - Client-docs repo structure recommendation put to operator (pending). +- RULED (2026-07-06 cont.): clientdocs/ TOP-LEVEL for client-facing drafts; + build INTERLEAVES with batch-3 (foil onboarding = live workflow validation); + A2 disruptive half PRE-APPROVED conditional on live N+1 headroom PASS. +- DELIVERED (addendum 22): H1 scripts/tenant-assert.sh + 7-case harness (green); + clientdocs/ package DOCFIX-092 (README/intake/welcome/self-service drafts). + Remaining in the interleave before foil onboarding: H2 idempotency guard, + H3 canary stage, D-073 list_trusts zip implementation (then gated live apply). diff --git a/docs/v1-redeploy-changelog.md b/docs/v1-redeploy-changelog.md index d937139..d6d60e5 100644 --- a/docs/v1-redeploy-changelog.md +++ b/docs/v1-redeploy-changelog.md @@ -2091,3 +2091,33 @@ REVERT: git rm docs/tenant-onboarding-contract.md; git checkout HEAD~ -- runbooks/tenant-onboarding-v2-DRAFT.md docs/session-ledger.md docs/v1-redeploy-changelog.md Next-free after this push (per scan, keep this line unwrapped): D-074, DOCFIX-092, BUNDLEFIX-012. + +### 2026-07-06 (addendum 22, jumphost stream) -- H1 tenant-assert.sh + clientdocs/ package (DOCFIX-092); A2 + sequencing rulings + +Three operator rulings recorded: (1) client-facing docs live in TOP-LEVEL `clientdocs/` +(hard internal/deliverable separation; keeps client prose out of the docs/+runbooks/ +identifier scans); (2) onboarding-package build INTERLEAVES with batch-3 (foil-tenant +onboarding doubles as the live end-to-end workflow validation); (3) A2: d011-04 +disruptive half (amphora failover) PRE-APPROVED conditional on the live N+1 headroom +check passing -- one window, no second ask; the headroom HOLD remains the safety gate. + +Delivered: +- NEW scripts/tenant-assert.sh (H1) -- read-only post-onboard topology verifier: + exact triad assignment sets (IDs not names), explicit admin-absence check + (escalation wording), keypair-trap check (owner must be -cluster), app-cred + ownership, handover-file mode 600 (existence+mode only, content never read). + Exit 0 PASS / 1 HOLD / 2 precondition. Mutates NOTHING. + VERIFY-LIVE flagged in header: `keypair list --user` + `application credential + list --user` as admin are harness-mocked; confirm on first real run. +- NEW tests/tenant-assert/run-tests.sh -- 7 cases (happy, escalation, keypair trap, + missing user, precondition, bad handover mode, absent handover dir = skip-not-fail); + app-cred fake emits stderr noise to exercise the stdout-only JSON capture rule. + ALL PASS; auto-discovered by the gauntlet. +- NEW clientdocs/ (DOCFIX-092): README.md (directory rules: no internal identifiers, + client language, packaging map), intake-form.md (custodians, quota, CIDR, + workload profile), welcome.md (boundary + house rules), self-service-guide.md + (triad usage rules incl. the two platform traps in client language, build order, + when-to-call-us). All drafts/living per ruling. +REVERT: git rm -r clientdocs scripts/tenant-assert.sh tests/tenant-assert; git checkout HEAD~ -- docs/handoff-20260705-open-items.md docs/session-ledger.md docs/v1-redeploy-changelog.md + +Next-free after this push (per scan, keep this line unwrapped): D-074, DOCFIX-093, BUNDLEFIX-012. diff --git a/scripts/tenant-assert.sh b/scripts/tenant-assert.sh new file mode 100644 index 0000000..d720a5e --- /dev/null +++ b/scripts/tenant-assert.sh @@ -0,0 +1,165 @@ +#!/usr/bin/env bash +# scripts/tenant-assert.sh -- read-only post-onboard topology verifier. +# Implements H1 of docs/tenant-onboarding-contract.md section 6 (DOCFIX-091; +# adopted 2026-07-06 as a pre-v1-close item). Verifies a tenant's identity +# topology matches the onboarding contract EXACTLY (D-066 triad, D-051 manager +# persona): +# - domain exists +# - -domain-admin has EXACTLY ONE assignment: manager on the domain +# - -cluster and -svc each hold EXACTLY {member, load-balancer_member} +# on -prod and NOTHING else +# - the admin role appears NOWHERE on the triad (explicit check) +# - keypair -key is owned by -cluster (the stage-6 trustor requirement) +# - app cred -svc-cred is owned by -svc +# - local handover files (if the handover dir exists here) are present, 0600 +# Mutates NOTHING. Evidence only; remediation stays a gated human step. +# Also serves as: offboard preflight; periodic all-tenants drift sweep member. +# VERIFY-LIVE (first real run): `keypair list --user` (nova microversion) and +# `application credential list --user` under the admin identity are mocked in +# the harness -- confirm both on the real cloud before trusting their PASS. +# Usage: bash scripts/tenant-assert.sh +# Exit: 0 PASS | 1 HOLD (>=1 assert failed) | 2 precondition missing. +# ASCII + LF. +set -uo pipefail +IFS=$'\n\t' + +CLIENT="${1:-}" +[ -n "$CLIENT" ] || { echo "usage: bash scripts/tenant-assert.sh " >&2; exit 2; } +command -v jq >/dev/null 2>&1 || { echo "PRECOND: jq required" >&2; exit 2; } +[ -s "$HOME/admin-openrc" ] || { echo "PRECOND: ~/admin-openrc missing" >&2; exit 2; } +for v in $(env | awk -F= '/^OS_/{print $1}'); do unset "$v"; done +# shellcheck disable=SC1090 +source "$HOME/admin-openrc" + +TMPD="$(mktemp -d)"; trap 'rm -rf "$TMPD"' EXIT +FAILS=0 +ok(){ echo " ok: $*"; } +bad(){ echo " FAIL: $*"; FAILS=$((FAILS+1)); } +is_id(){ [[ "${1:-}" =~ ^[0-9a-f]{32}$ ]]; } + +vcap(){ # vcap -- single-value capture; stderr to file, never merged + openstack "$@" -f value "$TMPD/err" || true +} +jcap(){ # jcap -- stdout-only JSON capture (house stderr rule) + local f="$1"; shift + openstack "$@" -f json "$f" 2>"$TMPD/err" || true + if ! jq -e . "$f" >/dev/null 2>&1; then + bad "openstack $* -- bad/empty JSON; stderr was:"; sed 's/^/ /' "$TMPD/err" + return 1 + fi +} + +echo "== tenant-assert: $CLIENT (read-only) ==" + +# ---- cloud-level role ids (stable identities, not names-in-rows) ---- +R_MANAGER="$(vcap role show manager -c id)" +R_MEMBER="$(vcap role show member -c id)" +R_LB="$(vcap role show load-balancer_member -c id)" +R_ADMIN="$(vcap role show admin -c id)" +for pair in "manager:$R_MANAGER" "member:$R_MEMBER" "load-balancer_member:$R_LB" "admin:$R_ADMIN"; do + is_id "${pair#*:}" || bad "cloud role '${pair%%:*}' unresolved (cloud-level problem)" +done +[ "$FAILS" -eq 0 ] || { echo "TENANT-ASSERT $CLIENT: HOLD ($FAILS failed -- roles unresolved)"; exit 1; } + +# ---- domain / users / project ---- +DOM="$(vcap domain show "$CLIENT" -c id)" +if ! is_id "$DOM"; then + bad "domain '$CLIENT' not found" + echo "TENANT-ASSERT $CLIENT: HOLD ($FAILS failed -- no domain, nothing further checkable)" + exit 1 +fi +ok "domain $CLIENT ($DOM)" + +MUID="$(vcap user show "${CLIENT}-domain-admin" --domain "$DOM" -c id)" +CUID="$(vcap user show "${CLIENT}-cluster" --domain "$DOM" -c id)" +SUID="$(vcap user show "${CLIENT}-svc" --domain "$DOM" -c id)" +is_id "$MUID" && ok "user ${CLIENT}-domain-admin" || bad "user ${CLIENT}-domain-admin missing" +is_id "$CUID" && ok "user ${CLIENT}-cluster" || bad "user ${CLIENT}-cluster missing" +is_id "$SUID" && ok "user ${CLIENT}-svc" || bad "user ${CLIENT}-svc missing" + +PID="$(vcap project show "${CLIENT}-prod" --domain "$DOM" -c id)" +is_id "$PID" && ok "project ${CLIENT}-prod ($PID)" || bad "project ${CLIENT}-prod missing" + +# ---- exact assignment sets (IDs, whole-output validation) ---- +assert_exact_pair(){ #