diff --git a/.claude/skills/openstack-cloud-ops/references/script-authoring.md b/.claude/skills/openstack-cloud-ops/references/script-authoring.md index bfe5d1c..2ff7aa2 100644 --- a/.claude/skills/openstack-cloud-ops/references/script-authoring.md +++ b/.claude/skills/openstack-cloud-ops/references/script-authoring.md @@ -194,7 +194,12 @@ **Static contract:** `bash scripts/repo-lint.sh` must exit 0 before anything is delivered (L1 ASCII/LF, L2 stale tokens, L3 ghost refs, L4 deprecated refs, -L5 numbering, L6 bare invocations). A guard script that must NAME stale tokens +L5 numbering, L6 bare invocations, L7 clientdocs sweep receipt). Changing any +tenant-facing surface (tenant-*.sh, the onboarding contract, tenant runbooks, +the policy zip, clientdocs/ itself) trips lint L7 until clientdocs/ has been +reviewed for staleness and the receipt is re-recorded: +`bash scripts/repo-lint.sh --record-clientdocs-sweep` (the sweep IS the review; +recording without reviewing defeats the rule). A guard script that must NAME stale tokens opts out per-file with a `repo-lint: allow-stale-tokens` marker in its header. Committed binaries (e.g. policies/overrides.zip) need a `*.zip binary` .gitattributes rule or the LF normalization corrupts them in transit. diff --git a/clientdocs/README.md b/clientdocs/README.md index bc564c6..f030dcb 100644 --- a/clientdocs/README.md +++ b/clientdocs/README.md @@ -17,6 +17,21 @@ never a repo link. Which file goes when: - `intake-form.md` -- BEFORE onboarding (their homework). - `welcome.md` + `self-service-guide.md` -- AT credential handoff. + - `handover-pack.md` + `acceptance-checklist.md` -- AT credential handoff, + instantiated per client (fill every `{{PLACEHOLDER}}`, remove the + TEMPLATE NOTE banner). + - `ci-integration-guide.md` -- with the handover pack for any client that + connects CI/automation (Jenkins etc.); same instantiation rule. +- **Templates.** `handover-pack.md`, `ci-integration-guide.md`, and + `acceptance-checklist.md` are reusable templates: `{{PLACEHOLDER}}` fields + are operator-filled at instantiation; a delivered copy contains none. - **ASCII + LF**, like everything committed. +- **Sweep-on-change is enforced.** `sweep-receipt.txt` (INTERNAL, never + shipped) pins the reviewed state of every tenant-facing file; repo lint + fails when any of them drifts, until this directory has been reviewed for + staleness and the receipt is re-recorded: + `bash scripts/repo-lint.sh --record-clientdocs-sweep`. -Contents: `intake-form.md`, `welcome.md`, `self-service-guide.md`. +Contents: `intake-form.md`, `welcome.md`, `self-service-guide.md`, +`handover-pack.md`, `ci-integration-guide.md`, `acceptance-checklist.md`; +internal-only: `sweep-receipt.txt`. diff --git a/clientdocs/acceptance-checklist.md b/clientdocs/acceptance-checklist.md new file mode 100644 index 0000000..35d8938 --- /dev/null +++ b/clientdocs/acceptance-checklist.md @@ -0,0 +1,120 @@ +# Omega Cloud -- Acceptance Checklist (DRAFT TEMPLATE) + +TEMPLATE NOTE (removed before delivery): fields written as {{THIS}} are filled +in by us per client at handover. + +The definition of "your environment works": every row below passes. Run it +once at onboarding -- with us on the line if you like -- and re-run any row +whenever you suspect a problem. Rows marked "You" are fully self-service; +rows marked "Us" need a request through your account contact. Rows marked +"expected refusal" MUST fail -- they prove the platform's boundaries are +protecting you. + +Unless a row says otherwise, run the checks as your automation credential +(`OS_CLOUD={{TENANT_SHORT_NAME}}`, per the CI/Automation Integration Guide). +Substitute any small shared machine size and base image from `openstack +flavor list` / `openstack image list`. + +## A. Sign-in and discovery + +| # | Proves | Minimal check | Expected result | Who | +|---|---|---|---|---| +| A1 | Pipeline sign-in (application credential) | `openstack token issue` | a token, scoped to your project | You | +| A2 | Cluster account sign-in (password) | as `{{TENANT_SHORT_NAME}}-cluster`: `openstack token issue` | a token, scoped to your project | You | +| A3 | Admin account sign-in (password, dashboard) | log in to {{DASHBOARD_URL}} as `{{TENANT_SHORT_NAME}}-domain-admin` with domain `{{TENANT_SHORT_NAME}}` | dashboard opens in your domain | You | +| A4 | Endpoint discovery | `openstack catalog list` | every service used in sections B-H appears with an endpoint | You | +| A5 | Quota envelope visible | `openstack limits show --absolute` | limits match your agreed quota | You | +| A6 | Identity boundary (expected refusal) | with the application credential: `openstack user create ci-probe` | permission DENIED -- identity work belongs to `-domain-admin` only | You | + +## B. Identity self-service (as `{{TENANT_SHORT_NAME}}-domain-admin`) + +| # | Proves | Minimal check | Expected result | Who | +|---|---|---|---|---| +| B1 | You can manage your own team | create user `ci-probe-user` in your domain, grant `member` on `{{TENANT_SHORT_NAME}}-prod`, then delete the user | create, grant, and delete all succeed | You | +| B2 | Escalation is blocked (expected refusal) | try to grant `admin` on your project to any of your users | grant refused / does not appear in `openstack role assignment list` | You | + +## C. Networking + +| # | Proves | Minimal check | Expected result | Who | +|---|---|---|---|---| +| C1 | Handover network intact | `openstack network show {{TENANT_SHORT_NAME}}-net` and `openstack router show {{TENANT_SHORT_NAME}}-router` | both exist; router's external gateway is on `provider-ext` | You | +| C2 | Network create/delete | create network `ci-accept-net` + a subnet on it (non-overlapping range), then delete both | both succeed | You | +| C3 | Firewall self-service | create security group `ci-accept-sg` with a TCP/22 rule, then delete it | both succeed | You | +| C4 | Public IP allocation | `openstack floating ip create provider-ext`, then delete it | an address is allocated and released | You | + +## D. Compute + +| # | Proves | Minimal check | Expected result | Who | +|---|---|---|---|---| +| D1 | Shared images and sizes visible | `openstack image list` and `openstack flavor list` | the shared base images and machine sizes appear | You | +| D2 | Server lifecycle | boot `ci-accept-vm` on `{{TENANT_SHORT_NAME}}-net` with keypair `{{TENANT_SHORT_NAME}}-key`; wait; delete | status reaches ACTIVE; delete completes | You | +| D3 | End-to-end reachability | attach a floating IP to `ci-accept-vm` and SSH in with your keypair (allow TCP/22 in its security group first) | SSH login succeeds | You | + +## E. Block storage + +| # | Proves | Minimal check | Expected result | Who | +|---|---|---|---|---| +| E1 | Volume lifecycle | `openstack volume create --size 1 ci-accept-vol` | status reaches `available` | You | +| E2 | Attach/detach | attach `ci-accept-vol` to `ci-accept-vm`, verify `in-use`, detach, delete | all four steps succeed | You | + +## F. Load balancers + +| # | Proves | Minimal check | Expected result | Who | +|---|---|---|---|---| +| F1 | Load balancer lifecycle | `openstack loadbalancer create --name ci-accept-lb --vip-subnet-id {{TENANT_SHORT_NAME}}-subnet --wait`, then delete (`--cascade`) | provisioning ACTIVE and operating ONLINE; delete completes | You | +| F2 | Listener/pool/member | on `ci-accept-lb` before deleting: add a listener (TCP/80), a pool, and `ci-accept-vm` as a member | all three create calls succeed | You | + +Note: load balancers take several minutes to build; that is normal. + +## G. Secrets and certificates + +| # | Proves | Minimal check | Expected result | Who | +|---|---|---|---|---| +| G1 | Secret lifecycle | `openstack secret store --name ci-accept-secret --payload check123`, retrieve it, delete it | stored, retrieved intact, deleted | You | + +This is the same store that holds TLS certificates for load balancer +listeners, so G1 passing means your TLS workflow will work. + +## H. Kubernetes clusters + +Run H2/H3 as `{{TENANT_SHORT_NAME}}-cluster` signed in with its PASSWORD -- +that is a platform requirement, not a preference. Cluster builds take tens +of minutes; agree timing with us if you are running the whole checklist in +one sitting. + +| # | Proves | Minimal check | Expected result | Who | +|---|---|---|---|---| +| H1 | Your cluster template exists | `openstack coe cluster template show {{TENANT_SHORT_NAME}}-k8s` | template found | You | +| H2 | Cluster lifecycle | `openstack coe cluster create ci-accept-k8s --cluster-template {{TENANT_SHORT_NAME}}-k8s --keypair {{TENANT_SHORT_NAME}}-key --master-count 1 --node-count 1`; wait; fetch kubeconfig (`openstack coe cluster config`); delete | CREATE_COMPLETE; `kubectl get nodes` answers; delete completes | You | +| H3 | Cluster boundary (expected refusal) | attempt the same create with the application credential | refused -- clusters require the `-cluster` password login | You | + +## I. Object storage + +The platform runs an object storage service (S3/Swift compatible). Whether +it is switched on for your accounts is confirmed at handover -- ask your +account contact before building on it. + +| # | Proves | Minimal check | Expected result | Who | +|---|---|---|---|---| +| I1 | Object storage enabled for you | `openstack container create ci-accept-bucket`, upload/download a small object, delete both | succeeds IF enabled for your accounts; otherwise ask us | You (after Us confirming) | + +## J. Operator-side paths (request drills) + +| # | Proves | Minimal check | Expected result | Who | +|---|---|---|---|---| +| J1 | Quota change path | an authorized requester asks {{ACCOUNT_CONTACT}} for a (small, real) quota change | change is applied and visible in `openstack limits show --absolute` | Us | +| J2 | Escalation path | send a test "urgent" notice to {{ACCOUNT_CONTACT}} as agreed at handover | acknowledged within the agreed response window | Us | + +## Sign-off + +All rows above passed on: ______________ + +For {{TENANT_SHORT_NAME}}: ______________________ (name, role) + +For Omega Cloud: ______________________ (name, role) + +Leftover check: after sign-off, run `openstack server list`, `openstack +volume list`, `openstack loadbalancer list`, `openstack floating ip list`, +and `openstack coe cluster list` -- nothing named `ci-accept-*` should +remain, and your quota consumption should be back to its pre-checklist +level. diff --git a/clientdocs/ci-integration-guide.md b/clientdocs/ci-integration-guide.md new file mode 100644 index 0000000..a7f61e3 --- /dev/null +++ b/clientdocs/ci-integration-guide.md @@ -0,0 +1,195 @@ +# Omega Cloud -- CI/Automation Integration Guide (DRAFT TEMPLATE) + +TEMPLATE NOTE (removed before delivery): fields written as {{THIS}} are filled +in by us per client; fields marked "you fill" come from the credential files +delivered to your custodians and are entered by you, on your side, into your +CI system's secret store. + +How to connect a CI system (Jenkins, GitLab CI, GitHub Actions, or plain +scripts) to your Omega Cloud environment. Read the Handover Pack first -- +sections 4 (credential handling) and 6 (boundary items) are assumed here. + +## 1. The one rule: pipelines use the application credential + +All automation authenticates as the `{{TENANT_SHORT_NAME}}-svc` account's +**application credential** (`{{TENANT_SHORT_NAME}}-svc-cred`). Never a +password. Why this pattern: + +- **Scoped**: it acts only inside your project, with exactly the rights of + the `-svc` account (`member` + `load-balancer_member`). It cannot touch + identity (users, roles), quotas, or anything outside your domain -- so a + leaked pipeline credential is contained by design. +- **Revocable without collateral**: delete the application credential and + every pipeline using it stops -- but the `-svc` account, its password, and + your other credentials are untouched. Rotations never require us. +- **No interactive dependency**: it does not expire with a password change. + +The corollary boundaries (platform-enforced, not policy suggestions): + +- The application credential **cannot create or delete Kubernetes clusters** + (section 5 below). +- It cannot create users, grant roles, or change quotas. Pipelines that try + will get a permission error -- that error is correct behavior. + +## 2. Storing the credential in your CI system + +You received the credential's `id` and `secret` in the delivered file +`{{TENANT_SHORT_NAME}}-svc-appcred.txt`. Put both into your CI system's +native secret store (in Jenkins: Manage Jenkins > Credentials, as +"secret text" entries or a username/password pair), then inject them into +jobs as environment variables. Never: + +- commit them to a repository (including a private one), +- bake them into a container or VM image, +- echo them in build output (mask them in your CI configuration). + +## 3. clouds.yaml for pipelines + +The OpenStack CLI and SDKs read `clouds.yaml`. A working template -- values +in angle brackets are yours to fill from the delivered credential file, and +the secret should be injected at job runtime, not written to disk in a +repository: + + clouds: + {{TENANT_SHORT_NAME}}: + auth_type: v3applicationcredential + auth: + auth_url: {{AUTH_URL}} + application_credential_id: + application_credential_secret: + region_name: {{REGION}} + identity_api_version: 3 + cacert: + +Then in a pipeline step: + + export OS_CLOUD={{TENANT_SHORT_NAME}} + openstack token issue + +A token comes back scoped to your project; that is your smoke test. +Equivalent environment-variable form (no clouds.yaml file needed): + + export OS_AUTH_TYPE=v3applicationcredential + export OS_AUTH_URL={{AUTH_URL}} + export OS_IDENTITY_API_VERSION=3 + export OS_CACERT= + export OS_APPLICATION_CREDENTIAL_ID= + export OS_APPLICATION_CREDENTIAL_SECRET= + +The same values work for Terraform/OpenTofu's OpenStack provider, Ansible's +openstack.cloud collection, and the OpenStack SDK -- they all understand +`clouds.yaml` and the `OS_*` variables. Do not hardcode service endpoint +URLs anywhere: authenticate against `{{AUTH_URL}}` and let the client +library discover the rest from the service catalog (`openstack catalog +list` shows what it sees). + +## 4. Least privilege: mint narrower credentials per pipeline + +Your handover credential is intentionally capable (it can do everything the +`-svc` account can). For production-grade hygiene, use it only as the +"root" automation credential and mint narrower ones per pipeline: sign in +as `{{TENANT_SHORT_NAME}}-svc` (password, interactively -- not from CI) and +create additional application credentials with an expiry date and, where +useful, a reduced role list: + + openstack application credential create pipeline-nightly \ + --role member --expiration 2027-01-01T00:00:00 + +Give each pipeline its own credential so you can revoke one without +stopping the others, and so your audit trail says which pipeline did what. +Deleting a credential is immediate and self-service: + + openstack application credential delete pipeline-nightly + +## 5. Kubernetes clusters and CI + +The platform's cluster machinery requires a password login (`-cluster` +account); an application credential cannot create or delete clusters. This +shapes the recommended CI pattern: + +- **Recommended**: create long-lived clusters manually (or on a controlled + schedule) as `{{TENANT_SHORT_NAME}}-cluster`, outside CI. Pipelines then + deploy INTO the cluster using its kubeconfig, stored in your CI secret + store like any other credential. +- **Not recommended**: putting the `-cluster` password into CI so pipelines + can create clusters. If your workflow genuinely needs cluster-per-run, + raise it with your account contact first -- there are capacity and quota + implications too. + +Everything else in this guide (networks, VMs, volumes, load balancers, +secrets, floating IPs) is fully pipeline-drivable with the application +credential. + +## 6. Quota and pacing expectations + +- **Your quota is the hard envelope.** Every create call is checked against + it server-side. Pipelines must treat quota-exceeded errors as expected, + actionable failures (clean up leaked resources, or request a raise via an + authorized requester) -- not as retryable platform faults. `openstack + quota show` and `openstack limits show --absolute` report your envelope + and current consumption; a pre-flight check in long pipelines is cheap + insurance. +- **Pace your polling.** Waiting on a server to go ACTIVE or a load + balancer to go ONLINE should poll at 10-second intervals or slower (or + use the CLI `--wait` flags, which do this for you). Tight polling loops + add load and gain nothing. +- **Serialize heavyweight creates.** Load balancers and Kubernetes clusters + take minutes to build; launching many concurrently mostly makes them + queue. Prefer sequential creation with waits. +- **Clean up what you create.** Tag or name every pipeline-created resource + with the job/build id (for example `ci-{{TENANT_SHORT_NAME}}-build123-web`) + and make teardown a step that runs on failure too. A periodic sweep job + that deletes resources older than your longest pipeline is the cheapest + way to keep quota headroom. Floating IPs count against quota even when + detached -- release them. + +## 7. The worked sequence your pipelines should exercise + +This is the canonical order of operations for an infrastructure pipeline on +this platform -- the same order the Acceptance Checklist tests. Each step +lists the surface it proves. Names below use a `ci-` prefix; substitute +your build id convention. + +1. **Authenticate + discover** -- `openstack token issue`, then + `openstack catalog list`. Proves credential and endpoint discovery. +2. **Network build-out** -- create network, subnet (a range inside your + allocation that does not collide with `{{TENANT_SHORT_NAME}}-subnet`), + router with gateway to `provider-ext`, attach subnet. Or reuse the + handover network for simple runs. Proves networking self-service. +3. **Security group** -- create a group and rules for exactly the ports the + deployment needs. Proves firewall self-service. +4. **Compute** -- boot a server from a shared base image on your network + with your keypair; wait for ACTIVE. Proves compute + image access. +5. **Block storage** -- create a volume, attach it to the server, verify it + is in-use, detach. Proves the storage path end to end. +6. **Public access** -- allocate a floating IP from `provider-ext`, attach + it, verify reachability (for example SSH or your app's health port). + Proves the north-south path. +7. **Load balancer** -- create a load balancer on your subnet with a + listener, pool, and the server as a member; wait for ACTIVE/ONLINE. + Proves the load-balancing service (your accounts already hold the + required role). +8. **Secrets** -- store, retrieve, and delete a test secret (this is also + where TLS certificates for load balancer listeners live). Proves the + secrets service. +9. **Teardown in reverse order** -- member/pool/listener/load balancer, + floating IP (release it), server, volume, security group, router + interface/router/subnet/network. Verify with the corresponding `list` + commands that nothing is left. Proves your cleanup discipline and + returns your quota. + +Run the full sequence once at onboarding (that is the Acceptance Checklist) +and keep a trimmed version as your pipeline's recurring smoke test. + +## 8. When a pipeline call fails + +1. Re-run `openstack token issue` -- authentication failures explain most + sudden breakage (revoked/expired credential, missing CA bundle in the + job environment). +2. Check quota (`openstack limits show --absolute`) -- the second most + common cause. +3. A permission error on identity, quota, or cluster operations is the + platform working as designed (sections 1 and 5), not an outage. +4. Anything else that looks like a platform fault: capture the exact + command and the exact error text and send them to {{ACCOUNT_CONTACT}}. + Verbatim errors get fast answers; paraphrased ones do not. diff --git a/clientdocs/handover-pack.md b/clientdocs/handover-pack.md new file mode 100644 index 0000000..96cae29 --- /dev/null +++ b/clientdocs/handover-pack.md @@ -0,0 +1,132 @@ +# Omega Cloud -- Client Handover Pack (DRAFT TEMPLATE) + +TEMPLATE NOTE (removed before delivery): fields written as {{THIS}} are filled +in by us for each client at handover. Everything else is standard. + +This document records exactly what you received when your environment was +handed over on {{HANDOVER_DATE}}, how to reach the platform, how credentials +must be handled, and the small set of platform rules you are asked to +acknowledge. Keep it with your operational documentation; the companions +are the Welcome letter, the Self-Service Guide, the CI/Automation Integration +Guide, and the Acceptance Checklist. + +## 1. Your identifiers + +| Item | Value | +|---|---| +| Client short name (prefix on everything) | `{{TENANT_SHORT_NAME}}` | +| Your domain (the isolation boundary) | `{{TENANT_SHORT_NAME}}` | +| Your project | `{{TENANT_SHORT_NAME}}-prod` | +| Your private network / subnet | `{{TENANT_SHORT_NAME}}-net` / `{{TENANT_SHORT_NAME}}-subnet` ({{TENANT_CIDR}}) | +| Your router (gateway to the shared external network `provider-ext`) | `{{TENANT_SHORT_NAME}}-router` | +| Your SSH keypair (owned by the `-cluster` account) | `{{TENANT_SHORT_NAME}}-key` | +| Your automation credential (application credential on the `-svc` account) | `{{TENANT_SHORT_NAME}}-svc-cred` | + +## 2. Your accounts and their exact rights + +| Account | Signs in with | Rights (exactly these, nothing more) | Purpose | +|---|---|---|---| +| `{{TENANT_SHORT_NAME}}-domain-admin` | password | `manager` on your domain | your team's users, projects, and role grants | +| `{{TENANT_SHORT_NAME}}-cluster` | password | `member` + `load-balancer_member` on your project | Kubernetes cluster create/delete; owns the SSH keypair | +| `{{TENANT_SHORT_NAME}}-svc` | password, plus its application credential | `member` + `load-balancer_member` on your project | scripted/automated work (CI pipelines use the application credential) | + +These rights are deliberate and verified at handover. If any of your accounts +ever appears to hold MORE than the rights above -- in particular anything +called `admin` -- stop and report it to your account contact immediately; that +is a platform incident, not a bonus. + +## 3. Reaching the platform + +- **API endpoint (identity service)**: `{{AUTH_URL}}` +- **Web dashboard**: `{{DASHBOARD_URL}}` +- **Region**: `{{REGION}}` +- **CA certificate**: the platform's APIs use TLS certificates issued by our + private certificate authority. You received the CA bundle file + (`{{CA_BUNDLE_FILE}}`) with your credentials; point your tools at it + (`cacert` in `clouds.yaml`, or the `OS_CACERT` environment variable). + Do not disable certificate verification instead. + +All other service endpoints (compute, networking, storage, load balancers, +Kubernetes, secrets) are discovered from the platform itself -- never +hardcode them. After authenticating, run: + + openstack catalog list + +and take endpoint URLs from there. If an endpoint ever changes, the catalog +is updated and your tooling follows automatically. + +## 4. Credential handling rules + +1. **Delivery**: credentials were delivered out-of-band via + {{CREDENTIAL_DELIVERY_METHOD}} to your two named custodians + ({{CUSTODIAN_1}} and {{CUSTODIAN_2}}) -- never in email or ticket text. + That rule also binds you: never paste a password or credential secret into + email, chat, tickets, source code, or CI job logs. +2. **Automation uses the application credential, never passwords.** CI + systems, scripts, and scheduled jobs authenticate with the + `{{TENANT_SHORT_NAME}}-svc-cred` application credential. Account passwords + never go into a pipeline, a repository, or a CI credential store. See the + CI/Automation Integration Guide. +3. **One person, one login.** Your `-domain-admin` creates individual users + for team members. Shared human logins are against the house rules. +4. **Rotation and revocation are self-service.** The `-svc` account can + create additional application credentials (with expiry dates, if you + wish) and delete compromised ones, without any ticket to us. Rotate by + creating the new credential first, cutting your automation over, then + deleting the old one. +5. **Password resets are not self-service.** If a password is lost, one of + your named custodians requests a reset through your account contact. + Keep both custodians current with us -- they are your recovery path. +6. **Secrets are verified, not displayed.** When you need to confirm a + credential file is intact, check its length or format in a script; do not + print it to a screen or log. + +## 5. Support and escalation + +| Situation | Path | +|---|---| +| Quota change, password reset, custodian change | an authorized requester (named in your intake form) contacts {{ACCOUNT_CONTACT}} | +| Platform incident or outage affecting you | {{ACCOUNT_CONTACT}}, marked urgent | +| Suspected security issue (unexpected rights, unfamiliar activity in your domain) | {{ACCOUNT_CONTACT}}, marked urgent -- report first, investigate second | +| Shared substrate requests (new base image, new machine size, external connectivity) | {{ACCOUNT_CONTACT}} | +| Everything inside your domain | self-service -- see the Self-Service Guide | + +## 6. Boundary items you are asked to acknowledge + +These are the platform rules that protect your isolation and your neighbors'. +Each one is enforced by the platform; acknowledging them saves you from +discovering them the hard way. + +1. **No `admin`, ever.** The platform refuses admin rights on client + accounts by design. Your `-domain-admin` account already has every right + you need inside your domain. Requests for admin will be declined; an + admin grant that somehow appears is treated as an incident. +2. **Kubernetes clusters are created and deleted only by the `-cluster` + account, signed in with its password.** The platform's cluster machinery + cannot operate through an application credential -- a cluster create + attempted with your automation credential fails every time. This is a + platform constraint, not a configuration you can change. +3. **The SSH keypair `{{TENANT_SHORT_NAME}}-key` belongs to the `-cluster` + account.** Do not delete it or recreate it under another account; + clusters refuse to build with a key owned by anyone else. +4. **Your `-domain-admin` account does identity work only.** Do not run + workloads or automation with it. +5. **There is no self-service recovery for the `-domain-admin` password.** + If both custodians are unavailable, so is your recovery path. Keep two + current custodians named with us at all times. +6. **Quotas are set by us.** Your accounts cannot raise them; an authorized + requester asks instead. Treat quota-exceeded errors in automation as a + signal to request a raise, not as a platform fault. +7. **The shared substrate is ours**: the external network `provider-ext`, + the public floating IP pool, public base images, and machine sizes. + You consume them; changes to them go through your account contact. +8. **Your network range is {{TENANT_CIDR}}.** You may create additional + private networks freely, but choose ranges that do not overlap your + on-premises or VPN networks if you ever plan to interconnect them. +9. **Credential hygiene binds your side too** (section 4): application + credentials in automation, no passwords in CI, no secrets in logs or + repositories, individual users for people. + +Acknowledged for {{TENANT_SHORT_NAME}} by: ______________________ (name, role) + +Date: ______________ diff --git a/clientdocs/sweep-receipt.txt b/clientdocs/sweep-receipt.txt new file mode 100644 index 0000000..359dd31 --- /dev/null +++ b/clientdocs/sweep-receipt.txt @@ -0,0 +1,27 @@ +# clientdocs/sweep-receipt.txt -- client-docs sweep-on-change receipt. +# INTERNAL FILE: never shipped to a client; not part of the client package. +# Each entry pins the reviewed state of one tenant-facing file: +# +# The path list IS the tenant-facing manifest (tenant scripts, clientdocs/ +# and the onboarding contract are auto-covered; runbooks/policy files are +# manual lines -- add a line with any hash and recording fixes it). +# Repo lint fails when any pinned file drifts, until clientdocs/ has been +# reviewed for staleness and this receipt is re-recorded with: +# bash scripts/repo-lint.sh --record-clientdocs-sweep +d333a3a261960cc7186c260f8e9350feabbefd2a9484088225afc07f9be6d742 clientdocs/README.md +d8595b7a1bb339af0a05ea547a814d63d906dc17e5a51609aa041bb56d637fd8 clientdocs/acceptance-checklist.md +a7dcdd137fcf8f38fb32716a70c1d988c5365d379d6a450722e29fff99effb1c clientdocs/ci-integration-guide.md +5d3cc5597b550fdcadb3f084062479ddd4f1b22576a154c855bcb64f5cecc727 clientdocs/handover-pack.md +0ff49cdb77d87ca60f971b29bea33714ebc9e79622df11a7f1e744459fe222dd clientdocs/intake-form.md +c084b45c2dbe3d3430f0428b1aa55e4563787a88f59598938b024fdcd3f12a47 clientdocs/self-service-guide.md +488bda3e51ca1ddd6adb51bb317fccd5ee04731687fa7975e41a4043efe87adb clientdocs/welcome.md +917db0e786047491ff75c2624811c5bea212eef98c64327058fd87e680a6207b docs/tenant-onboarding-contract.md +64f99b638a1855b02180e907c9535db8bddc5847a502759a9e0d63c513b63077 policies/domain-manager-policy.yaml +02fe1fd7fcf07bfc6088622d4aa8430da53e435d60006d0b81f6b9c2e720a011 policies/overrides.zip +30919b61b2b79b33a853a29bc814558d7e0e940a3040b2f7710ea9a14333b71a runbooks/appendix-C-identity-rbac.md +8510402e5375a6d42cc47f28eec9698a23d88e04b22ef70ef6e129e1179f3d32 runbooks/appendix-D-magnum-trust-model.md +11d264207bc9e3ef5ebbb2df0ecd4ed22151270fb8732aec4ce6e1d4b8dd0b29 runbooks/tenant-onboarding-v2-DRAFT.md +a6080e4d4207a0a47d0ce9e9f54f5b7b3235e0dbf96b00eb2fbc2238df9895ff scripts/tenant-acceptance.sh +ad00be489480069d52df1b978be0360d7fb04dbf954a8cf487162872f2da0593 scripts/tenant-assert.sh +0b09bbc3de1927e299eb8650ea901c9f1939abc097e717aa70a049079b409edd scripts/tenant-offboard.sh +389e5fe25391a3e16bab71608b55fb3ad2153e80300ef66f76bf5687544ccf69 scripts/tenant-onboard.sh diff --git a/docs/session-ledger.md b/docs/session-ledger.md index 4f66a22..c40432e 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 = 098, BUNDLEFIX = 012. - (Scan and the addendum-27 changelog next-free pointer agree.) +- **Next-free numbers:** D = 074, DOCFIX = 106, BUNDLEFIX = 012. + (Scan and the addendum-28 changelog next-free pointer agree.) --- @@ -271,6 +271,16 @@ - **Open (small):** Horizon manual checks on foil1 (operator, browser, non-blocking); capi-test-1 CREATE_FAILED leftover (orphan-sweep candidate); beta-cluster lbtest leftover; repo-lint worktree-exclusion DOCFIX candidate. +- **INTEGRATED (addendum 28):** all six worktree drafts -> main as + DOCFIX-098 (H4 drift detector), 099/100 (offboard orphan sweep + Phase E0 + guard), 101 (clientdocs handover pack), 102 (cloud-snapshot.sh -- unblocks + the devteam snapshot gate), 103 (tenant-acceptance harness + the last + stderr-inventory fix), 104 (lint L7 sweep-on-change ENFORCED; receipt live, + 17 files). Gauntlet 35/35 on the integrated tree. Main-chat backlog items + 6/7/8 are now DELIVERED (by this stream, operator-assigned) -- main chat to + reconcile its section. NEXT UP: operator-gated cloud-snapshot run, then + devteam onboarding (task HELD on the snapshot), then handover-pack + instantiation with dev-team intake answers. diff --git a/docs/v1-redeploy-changelog.md b/docs/v1-redeploy-changelog.md index 87f680e..c9a23ab 100644 --- a/docs/v1-redeploy-changelog.md +++ b/docs/v1-redeploy-changelog.md @@ -2327,3 +2327,109 @@ devteam PINNED next (onboarding HELD on operator snapshot ruling). Next-free after this push (per scan, keep this line unwrapped): D-074, DOCFIX-098, BUNDLEFIX-012. + +### 2026-07-06 (addendum 28, jumphost stream) -- operator-away batch INTEGRATED: DOCFIX-098..104 (six worktree drafts -> main) + +Six subagent-drafted deliverables, each harness-green in an isolated worktree, +integrated after the addendum-27 window close. Operator rulings applied at +integration: orphan-sweep audit rc 0 report-only (as drafted); clientdocs L7 +receipt keeps all 14 pinned files incl. policies/ + appendices (as drafted); +H4 design defaults accepted (staged-D-073 = PASS-with-distinct-text). Full +gauntlet on the integrated tree: ALL GREEN (35 harnesses, 3 new). + +- **DOCFIX-098 -- scripts/keystone-policy-drift.sh + keystone_policy_compare.py + + tests/keystone-policy-drift/ (H4).** Read-only drift detector: repo policy + artifacts vs live keystone (base D-064 trio + helpers verbatim; cloud_admin + structural with IDs wildcarded; D-065/D-073 trust-family premise; overlay by + PARSED rule sets; PO: workload marker; local zip==yaml). Leader discovered + dynamically; juju-ssh stdout pipes only; no OS_* scope needed. Exit 0 PASS / + 1 DRIFT / 2 HELD. Harness 10/10 (mock leader deliberately keystone/1; ssh + stderr noise). Already live-proven: first real run in the addendum-27 window + reported PASS (39 rules aligned). Closes main-chat backlog item 6 (operator + assigned it to the jumphost stream). REVERT: delete the three paths. +- **DOCFIX-099 -- tenant-offboard.sh --sweep-magnum-orphans [--audit|--apply].** + Classifies magnum-domain trustees ORPHAN / PROTECTED / UNPROVEN, fail-closed + (non-convention names, ambiguous lookups, and admin-scope cluster-show + failures are UNPROVEN, never touched; magnum_domain_admin always PROTECTED; + exact keystone not-found message = orphan proof). --apply deletes proven + orphans only (trusts first, post-delete verified) behind a typed + confirmation; audit always rc 0 (operator-ruled). Verify-live before first + --apply: exact not-found message shape; admin-scope coe cluster show + behavior on alive tenant clusters; app-cred list --user admin behavior. + First audit target logged: capi-test-1 CREATE_FAILED leftover. +- **DOCFIX-100 -- tenant-offboard.sh Phase E0 app-cred inventory + idempotency + guard (H2 counterpart).** Explicit per-user app-cred inventory before + identity teardown; H2-style SKIP rc 0 on re-runs against already-cleaned + tenants. Ledger said "stage-3" but offboard has phases (onboard vocabulary + drift) -- implemented as Phase E0; removal mechanism remains the user-delete + cascade (osc has no admin-side app-cred delete). Harness (099+100): + tenant-offboard 7->14 cases. FOUND-NOT-FIXED (DOCFIX candidate): Phase B + residual-trust sweep increments SWEEP_FAIL inside a pipeline subshell -- + failed trust deletes cannot raise exit 22. REVERT: fenced blocks in + tenant-offboard.sh + drop the 7 new harness cases/mock arms. +- **DOCFIX-101 -- clientdocs/ handover pack (3 reusable templates + README + packaging rules).** handover-pack.md (identifiers/roles/endpoints/credential + rules/boundary acknowledgements from the onboarding contract), + ci-integration-guide.md (app-cred-only CI auth, clouds.yaml placeholders, + least-privilege per-pipeline creds, Magnum/CI boundary, quota pacing, worked + API sequence), acceptance-checklist.md (matrix derived from bundle-proven + services; expected-refusal boundary rows; object storage confirm-first -- + radosgw tenant enablement UNVERIFIED). {{PLACEHOLDER}}-driven; first + instantiation = devteam tenant (HELD on snapshot). Intake questions for the + dev team logged in the ledger. REVERT: git rm the 3 files + checkout README. +- **DOCFIX-102 -- scripts/cloud-snapshot.sh + snapshot_scrub.py + + tests/cloud-snapshot/ (backlog item 7, D-070 baseline).** One read-only + command -> asbuilt//: bundle-exported.yaml, juju-status.json, + model-config.yaml, checksummed manifest; credential-shaped values scrubbed + (WARN rc 2 + count; benign-placeholder whitelist; scrub re-verified by + --check). Flag-gated --with-controller-backup -> juju create-backup -m + admin/controller to ~/openstack-baseline/ (jumphost-local, NEVER committed; + D-071 amendment). juju model-config --format=yaml confirmed live this + session. Harness 20/20. Open design questions logged (controllers.json w/ + CA cert; fingerprintless redaction; line-oriented scrub; shared asbuilt/; + exit-2 contract reconciliation) -- defaults stand until ruled. REVERT: + delete the three paths. +- **DOCFIX-103 -- tests/tenant-acceptance/ harness (backlog item 8) + + acceptance:43 stderr fix (completes the DOCFIX-094 10-site inventory).** + 16 checks / 11 cases (happy, health-lag WARN, preconditions, kube/LB fails, + isolation violations; truthful mock emits trust-list stderr noise). The one + deferred stderr-merge site fixed in the DOCFIX-094 idiom (stdout-only JSON + capture, stderr tempfile surfaced as NOTE); regression-proven by reverting + (exactly the 4 guard checks fail pre-fix -- the hazard silently downgraded + trustee checks to WARN). FOUND-NOT-FIXED (DOCFIX candidates): foil_env + hardcoded OS_AUTH_URL (house rule 3); two single-column 2>&1 captures + (lines ~38/52); P3 C5 foil-LB grep false-VIOLATION if the foil runs its own + cluster. REVERT: git checkout HEAD~ -- scripts/tenant-acceptance.sh; git rm + -r tests/tenant-acceptance. +- **DOCFIX-104 -- repo-lint rule L7: clientdocs sweep-on-change ENFORCED.** + clientdocs/sweep-receipt.txt hash-pins every tenant-facing file (manifest = + the path list; scripts/tenant-*.sh, clientdocs/*.md, onboarding contract + auto-covered; tenant runbooks, appendices C/D, policies overlay+zip manual + lines -- operator kept all). Any drift FAILs lint with the one-line remedy + until reviewed + re-recorded via `bash scripts/repo-lint.sh + --record-clientdocs-sweep` (the flag's ONLY mutation). Skips when + clientdocs/ absent (fixture repos unaffected); content-hash based (immune + to the L5 prose-token weakness). Receipt recorded this session (17 files -- + 14 + the 3 new templates) after a genuine full-directory review. Namespace + note: lint L7 is distinct from appendix-A lesson L7. tests/repo-lint 9->17 + cases. Horizon-facing behavior has no file surface -- stays conventional. + REVERT: checkout repo_lint.py, repo-lint.sh, tests/repo-lint/run-tests.sh, + clientdocs/README.md, script-authoring ref; git rm clientdocs/sweep-receipt.txt. + +Integration notes: clientdocs/README.md hand-merged (DOCFIX-101 + DOCFIX-104 +both touch it); receipt recorded AFTER all tenant-surface integrations so the +hashes pin final states. Session worktrees pruned post-commit (their L1 lint +FAILs disappear with them; standing exclusion = DOCFIX candidate). Main-chat +backlog items 6/7/8 delivered by the jumphost stream -- main chat to reconcile +its section against this addendum. + +- **DOCFIX-105 (appended in-batch) -- ledger-scan.sh numbering blind past 099.** + Assigning DOCFIX-100..104 above tripped a latent scanner defect: the + next-free patterns were `DOCFIX-0[0-9]{2}` / `BUNDLEFIX-0[0-9]{2}` -- blind + to 100+, so next-free would have frozen at 100 forever. Patterns widened to + `[0-9]{3}`; harness gains 2 boundary cases (35->37 checks, ALL PASS). The + numbering authority is correct again (verified: highest=104 -> next-free=105 + before this entry consumed it). REVERT: git checkout HEAD~ -- + scripts/ledger-scan.sh tests/ledger-scan/run-tests.sh. + +Next-free after this push (per scan, keep this line unwrapped): D-074, DOCFIX-106, BUNDLEFIX-012. diff --git a/scripts/cloud-snapshot.sh b/scripts/cloud-snapshot.sh new file mode 100644 index 0000000..ddfd001 --- /dev/null +++ b/scripts/cloud-snapshot.sh @@ -0,0 +1,181 @@ +#!/usr/bin/env bash +# scripts/cloud-snapshot.sh [--with-controller-backup] [MODEL] +# +# Pre-change baseline snapshot (DOCFIX-102; session-ledger script-backlog item 7). +# Captures the restorable/diffable point-in-time baseline that D-070 KEPT when it +# retired the KVM-snapshot restore path: the juju export + inventory captures. +# Run it BEFORE consequential changes (tenant onboarding, maintenance windows, +# charm refreshes) to pin the pre-change state. It complements, not replaces, +# `bash scripts/cloud-assert.sh --capture` (DOCFIX-075): cloud-assert's BOM is +# the behavioral-sweep-attached deploy baseline; this is the fast juju-layer +# capture that needs NO OpenStack admin scope and no unit ssh. +# +# Captures into asbuilt// (the D-070 committed-baseline +# location; commit the directory -- it is the diffable pre-change baseline): +# bundle-exported.yaml juju export-bundle -m (D-070 "juju export") +# juju-status.json juju status -m --format=json (asbuilt BOM prior art) +# model-config.yaml juju model-config -m (inventory) +# manifest.txt UTC stamp, model, juju client version, per-file +# sha256 + byte counts, redaction report (inventory) +# +# Secret hygiene: exported bundles and model config CAN embed credential-shaped +# option values. Every captured text file is scrubbed by scripts/snapshot_scrub.py +# (credential-shaped keys get their values replaced with a REDACTED marker) and +# any redaction downgrades the verdict to WARN -- review before committing. +# +# --with-controller-backup: additionally runs `juju create-backup -m +# admin/controller` (EXISTS on juju 3.6 -- D-071 AMENDMENT 2026-07-05 / +# DOCFIX-088; the controller model on this cloud is admin/controller). The +# archive is ~900MB-class and slow, hence flag-gated and never default. It is +# written to ~/openstack-baseline/ on the jumphost (snap cannot use /tmp; and +# per ops-update-procedure 2.1 the archive is secret-adjacent and NEVER +# committed) -- the manifest records its path/size/sha256 only. +# +# Mutates NOTHING in the cloud except the flag-gated create-backup call (which +# writes a backup archive controller-side, then downloads it). Repo-side it +# writes only under asbuilt/; jumphost-side only under ~/openstack-baseline/ +# (flag path). CLOUD_SNAPSHOT_ROOT overrides the repo root for the offline +# harness ONLY. +# +# Exit (cloud-assert prior art): 0 snapshot complete | 1 HELD/FAIL (juju +# unreachable, or any capture failed -- partial captures are counted and +# reported) | 2 WARN (snapshot complete but redactions applied -- review +# before commit). ASCII + LF. + +set -uo pipefail +shopt -s inherit_errexit 2>/dev/null || true +IFS=$'\n\t' + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO="$(cd "$SCRIPT_DIR/.." && pwd)" +ROOT="${CLOUD_SNAPSHOT_ROOT:-$REPO}" + +MODEL="openstack"; WITH_BACKUP=0 +for a in "$@"; do + case "$a" in + --with-controller-backup) WITH_BACKUP=1 ;; + -*) echo "FAIL: unknown flag $a"; exit 1 ;; + *) MODEL="$a" ;; + esac +done + +# Structured captures take stdout ONLY -- merged stderr lands at char 0 and +# kills the parse / corrupts the capture (DOCFIX-094). stderr goes to a +# tempfile and is surfaced on failure, never discarded. +JERR="$(mktemp)"; trap 'rm -f "$JERR"' EXIT +J() { juju "$@" "$JERR"; } +jerr() { head -1 "$JERR" 2>/dev/null || true; } + +WORST=0 +fail() { echo " [FAIL] $*"; WORST=1; } +warn() { echo " [WARN] $*"; [ "$WORST" -ne 1 ] && WORST=2; } +ok() { echo " [ok] $*"; } + +command -v juju >/dev/null 2>&1 || { echo "FAIL: juju not found"; exit 1; } +command -v jq >/dev/null 2>&1 || { echo "FAIL: jq required"; exit 1; } +command -v python3 >/dev/null 2>&1 || { echo "FAIL: python3 required"; exit 1; } +SCRUB="$SCRIPT_DIR/snapshot_scrub.py" +[ -f "$SCRUB" ] || { echo "FAIL: helper missing: scripts/snapshot_scrub.py"; exit 2; } + +echo "================ S0: juju reachability (nothing written until this passes) ================" +ST=$(J status -m "$MODEL" --format=json || true) +if ! jq -e .applications >/dev/null 2>&1 <<<"$ST"; then + fail "cannot read juju status for model '$MODEL': $(jerr)" + echo; echo "CLOUD-SNAPSHOT: HELD (juju unreachable -- nothing captured)"; exit 1 +fi +ok "model '$MODEL' reachable" + +TS="$(date -u +%Y%m%d-%H%M%S)" +DIR="$ROOT/asbuilt/$TS" +mkdir -p "$DIR" +JVER=$(J version || true) + +echo "================ S1: captures -> asbuilt/$TS/ ================" +CAP_TOTAL=0; CAP_OK=0 +capture() { # capture