Skip to content

S30 PLAN

Execute the isolated synthetic rehearsal after the S08 Identity dark launch, its audited Project Management receiver prerequisite and S08A session-store substrate, before ordinary staging authentication changes.

Purpose: Prove the complete native Identity/BFF behaviour and rollback topology without changing Auth0 defaults, real users, live data or production. Output: Focused SyRF and cluster-gitops PRs, redacted full-matrix evidence, and a separately reviewed/synced teardown.

**S30 PASSED on 2026-10-03.** Chris approved recording the pass and tearing the rehearsal down. The rehearsal was redeployed on 2026-10-02, the full Task 2 verify command passed, and the two-stage teardown removed it. The summary is in [S30-SUMMARY](S30-SUMMARY.md). | Task | State | |---|---| | 1. Prepare | **Done.** camaradesuk/syrf#3697 merged 2026-09-25. Refreshed for the redeploy in camaradesuk/cluster-gitops#1508 (merge `f86176a2`, 2026-10-02): staging's then-current pins, and `mongodb.com/atlas-resource-policy: delete` on the AtlasDatabaseUsers. | | 2. Live matrix | **Passed 2026-10-03.** `live-smoke.sh … --isolated-rehearsal --require-two-replicas --require-forwarded-header-matrix` exited 0. Password, `/api/auth/me`, admin/account, protected API, refresh/logout, SignalR and reset all passed. Replicas 2, shared-session replicas 2, forwarded-header matrix 5/5. Google is **operator-attested** (see below). | | 3. Teardown | **Done, in two stages** (2026-10-03). See [Teardown evidence (2026-10-03)](#teardown-evidence-2026-10-03). | **Google row:** the run host is headless, so Chris did the Google journey by hand in a desktop browser at 2026-10-03T01:46Z. Sign-in with Google worked, `/api/auth/me` returned 200, and Google was listed under ExternalLogins. The evidence records `google: false` and `googleJourney: "operator-attested"`, so it can never be read as automated (camaradesuk/syrf#3940). **Not exercised live:** old-cookie rejection after a generation change (`old_cookie_unauthorized_checked: false`), passkeys, optional MFA, Google unlink, token claims, Swagger, confirmation resend and forced-reset admission. They stay open in M005-VALIDATION. **Fixes needed for the pass:** - camaradesuk/syrf#3940: navigation resume, plus the operator-attested Google mode; - camaradesuk/syrf#3951: the reset test's title tripped redaction rule 8; - running from the approved `agents/s30-live` worktree, not `main`; - Playwright inside the official container, to avoid this CI host's `ERR_NETWORK_CHANGED`. The operator lessons are in [Send non-production email to Mailpit](../../../../../../how-to/non-production-email-mailpit.md#operator-run-lessons-s30-2026-10-03). **Known issue found:** camaradesuk/syrf#3935. Opening the verification link before registration completes rolls the account back. ### History: the 2026-09-25 deployment and the 2026-09-29 pause **Task 2, deployed 2026-09-25:** - camaradesuk/cluster-gitops#1188 (provision, merge `06d8f115`) and camaradesuk/cluster-gitops#1265 (merge `9399f5df`) deployed the rehearsal. #1265 makes the rehearsal API's `ProxySettings` trust `X-Forwarded-*` from the pod CIDR. - Five Argo apps were Synced/Healthy: `identity-`, `api-`, `web-`, `project-management-rehearsal` and `extra-secrets-rehearsal`. They were pinned to staging's validated artifacts built from `6d7d7205b`. - SyRF camaradesuk/syrf#3751 fixed the live harness (the reset-link selector, and waiting out the one-minute recovery-email throttle). **What passed:** - the forwarded-header matrix, **5 of 5** rows: discovery and the BFF callback, trusted and with untrusted `X-Forwarded-Host`/`-Proto`, plus the emailed reset link requested under untrusted headers; - password sign-in through the BFF, `/api/auth/me`, a protected API, and the account and admin pages; - the password reset journey, with the link read from Mailpit `tag:rehearsal`. **What blocked the full run:** - **The admin grant.** `/api/investigators` returned 403 until the synthetic account is in `administrator`. SignalR, refresh, logout and persisted-session checks run after that assertion, so they were not reached. - **The Google link.** Social sign-in needs the designated Google test account pre-linked. - **The two-replica check.** It uses `kubectl port-forward`, which is operator-run only. - The optional matrix flows were not reached: passkeys, MFA, link/unlink step-up, token claims and Swagger. **Task 3, pause teardown (2026-09-29):** camaradesuk/cluster-gitops#1189 (merge `b027c886`) removes the rehearsal. It is #1188 + #1265 reverted on current `main` (proved with `git revert -m 1` of both merges), except for two entries it deliberately retains: - the `atlas-operator-api-key` namespace entry, because the AtlasDatabaseUser finalizers need that credential; - the `plugins` AppProject's `syrf-rehearsal` destination. Argo CD cascade-deletes only live objects its project still permits. SyRF camaradesuk/syrf#3833 extends the `isolated-rehearsal-teardown` preflight to accept that single bounded destination. The preflight passes on #1189's head: 14 assertions, 0 residual references. Two unstick fixes and a cleanup PR followed (see the teardown chain below). The read-only absence evidence is in [Teardown evidence (2026-09-29)](#teardown-evidence-2026-09-29). **Kept for the redeploy:** - the GCP secret `camarades-google-oauth-rehearsal` and its non-production Google OAuth client; - the S08A `valkey-nonprod` `rehearsal` ACL user; - the operator's local synthetic-identity and run files. The accounts they describe no longer exist, because `syrf_identity_rehearsal` was dropped. **To redeploy:** re-merge #1188 + #1265, or an equivalent refreshed against `main`. Re-pin to staging's then-current validated artifacts and re-create the synthetic identities. Complete the admin grant and the Google link, then run the full Task 2 verify command, including `--require-two-replicas`. Then repeat Task 3 with a fresh inverse. **S09 prerequisite (unchanged by the pause):** the rehearsal proved that the API must trust the ingress hop before BFF callbacks use `https`. Ordinary staging's API, and production's before S10, need the same `ProxySettings`/forwarded-header trust before `bffAuth` is enabled. Two further findings for S09/S10: - the `ListUsers` permission compares group names case-sensitively against `administrator`, while the migration tooling documents `Administrator`; - this host's network churn can abort Chromium navigations (`ERR_NETWORK_CHANGED`). Re-run once on that error. Topology decisions from Task 1 still stand: - a full Web/API(BFF)/PM/Identity stack; - the route `rehearsal.syrf.org.uk`, with the API path-routed on `/api`, `/notifications` and `/swagger`, plus `identity.` and `project-management.rehearsal.syrf.org.uk`; - no source-IP allowlist; - inert placeholders for non-auth integrations.

Teardown evidence (2026-10-03)

This was a two-stage teardown, following lessons 1–3 below. Lesson 4 was applied in #1508, which set mongodb.com/atlas-resource-policy: delete. Lesson 5 is why the DNS records remain:

  • Stage 1: camaradesuk/cluster-gitops#1525 (merge 840fef7a, 02:29:21Z). It is the inverse of #1508, keeping the three ClusterExternalSecret selectors, the plugins destination and the PostDelete hook (without the PostSync create hook).
  • Stage 2: camaradesuk/cluster-gitops#1526 (merge b9e3ec88, 02:42:22Z) removed those once absence was proven. All three of its paths are byte-identical to before #1508.
  • Preflight: preflight.sh --mode isolated-rehearsal-teardown on stage 2's head gives rc 0: 14 assertions, 0 residual references. The redaction check passes.

Read-only discovery after stage 1 synced:

Item State
Rehearsal Applications The four service apps were gone by 02:30:14Z, and extra-secrets-rehearsal by 02:31:22Z
AppProject syrf-rehearsal Its own resources-finalizer held it until no Application referenced it. Gone at 02:32:16Z; root Synced/Healthy
Namespace syrf-rehearsal Gone by 02:31:22Z, with no stuck ESO finalizer, because the CES still selected it
RabbitMQ vhost syrf-rehearsal The PostDelete Job completed. The broker log shows exactly one vhost deletion, syrf-rehearsal, at 02:31:13Z
Atlas database users Deleted. All three took the operator's delete path. There is no "Not removing Atlas database user" line, which on 2026-09-29 appeared for all three. Each logged Removed 1 connection secrets and released its finalizer; a failed Atlas delete keeps the finalizer. The Atlas API itself still answers 401 to read-only access, so this rests on operator logs and object status (camaradesuk/syrf#3834)
Databases syrf_rehearsal, syrf_identity_rehearsal Absent from the Preview cluster's database list
Mailpit Rolled at 02:29:40Z; Ready, 0 restarts. The rehearsal SMTP user, its ExternalSecret and its store are gone, and MP_SMTP_AUTH holds only the shared users
Ingresses None with a rehearsal host
DNS rehearsal., identity.rehearsal., project-management.rehearsal.syrf.org.uk Still resolve to the shared ingress. They are left for the GitOps-owned fix in camaradesuk/cluster-gitops#1380; nothing was deleted by hand
Ordinary staging and production Staging API SYRF__IdentityProvider=auth0, Web SYRF__AuthProvider=auth0. Staging web, Identity discovery and API /health/ready all return 200, as does production web. API/PM/Web/Identity staging and API/PM/Web production are Synced/Healthy

Kept on purpose:

  • the GCP secret camarades-google-oauth-rehearsal and its Google client;
  • the S08A Valkey rehearsal user;
  • the operator's run wrapper and Playwright wrapper;
  • the agents/s30-live worktree.

Teardown evidence (2026-09-29)

Read-only discovery (kubectl get/describe/logs, DNS lookups, the Preview cluster's database list) after #1189 synced:

Item State
Argo Applications (identity-, api-, web-, project-management-rehearsal) and the syrf-rehearsal AppProject/ApplicationSet Gone by 13:13Z
Workloads, Ingresses, Certificates, ExternalSecrets, AtlasDatabaseUser and DatabaseLifecycle objects in syrf-rehearsal Gone; no namespaced object remains
Databases syrf_rehearsal and syrf_identity_rehearsal Dropped by DatabaseLifecycle cleanupOnDelete
Mailpit Rolled once at 13:11:43Z, ready, 0 restarts. The rehearsal SMTP user, its store and its Secret are gone; the shared staging/preview users are unchanged
Valkey rehearsal ACL user Retained on purpose (S08A substrate, kept for the redeploy); valkey-nonprod Synced/Healthy
syrf-rehearsal namespace Gone at 13:31:56Z, after camaradesuk/cluster-gitops#1379 (see the lessons below). It was stuck Terminating from 13:12Z on two ESO ClusterExternalSecret finalizers
RabbitMQ vhost syrf-rehearsal Removed. After camaradesuk/cluster-gitops#1381 restored the hook, the PostDelete Job syrf-rehearsal-rabbitmq-vhost-delete completed at 13:47Z; it exits 0 only on HTTP 204/404. extra-secrets-rehearsal finalized at 13:47:52Z
Atlas database users Not removed from Atlas. The operator (2.13.2, --object-deletion-protection=true) logged "Not removing Atlas database user from Atlas as per configuration" for all three. It honours only mongodb.com/atlas-resource-policy, not the atlas.mongodb.com/deletion-protection: "false" annotation our charts use, and previews share the gap. Tracked in camaradesuk/syrf#3834. Read-only Atlas API access returned 401, so this is inferred from the operator logs
DNS rehearsal., identity.rehearsal., project-management.rehearsal.syrf.org.uk Still resolve to the shared ingress, which returns 404. External-DNS runs upsert-only with registry: noop, so it never deletes records. Nobody deleted them by hand; tracked for a GitOps-owned fix in camaradesuk/cluster-gitops#1380
Ordinary staging and production API/PM/Web staging and API/PM production Synced/Healthy. Staging API SYRF__IdentityProvider=auth0, Web SYRF__AuthProvider=auth0. Staging Identity discovery 200 and API /health/ready 200

Leftovers kept for the redeploy:

  • the GCP secret camarades-google-oauth-rehearsal and its Google OAuth client;
  • the Valkey rehearsal user and its valkey-nonprod-rehearsal store;
  • the operator's local run files (the synthetic accounts themselves were dropped with the database).

Atlas users left in Atlas (names only): syrf_rehearsal_app, syrf_identity_rehearsal and syrf_identity_rehearsal_pm_ro. They are left in place for now; see camaradesuk/syrf#3834. The preview-orphan-sweep CronJob never collects them, because it matches only ^syrf_pr_[0-9]+_app$. A redeploy's AtlasDatabaseUsers use the same usernames.

Teardown chain:

  1. camaradesuk/cluster-gitops#1189 (b027c886): the inverse, keeping two entries.
  2. camaradesuk/cluster-gitops#1379 (019154cf): re-selected the namespace in two ClusterExternalSecrets so ESO releases it.
  3. camaradesuk/cluster-gitops#1381 (cda83b18): restored the PostDelete vhost hook.
  4. camaradesuk/cluster-gitops#1382: removes every retained or restored entry once absence is proven.

Teardown lessons (apply to the redeploy's teardown)

  1. Keep a namespace in ClusterExternalSecret selectors until the namespace is gone. ESO v1.0.0 puts a per-CES finalizer on each selected namespace. It removes that finalizer only while the CES still selects the namespace (gatherProvisionedNamespaces); de-selecting only deletes the ExternalSecret.
  2. Keep AppProject destinations until the Applications have finalized. Argo CD deletes only live objects the project still permits (getPermittedAppLiveObjects).
  3. Keep PostDelete hooks at their git path until the Application has finalized. Argo CD renders them from the Application's current target state, so deleting the path strands the post-delete finalizer. A two-PR teardown avoids all three: first delete the ApplicationSet, apps and config; then, after read-only proof of absence, delete the selectors, destinations and hooks.
  4. atlas.mongodb.com/deletion-protection: "false" has no effect. The operator honours only mongodb.com/atlas-resource-policy (camaradesuk/syrf#3834).
  5. External-DNS never deletes records (camaradesuk/cluster-gitops#1380).

@docs/planning/auth0-to-openiddict/actions-openiddict-mapping.md @docs/planning/auth0-to-openiddict/phases/05-cutover-decommission/M005-VALIDATION.md @docs/planning/auth0-to-openiddict/phases/05-cutover-decommission/slices/S08/S08-SUMMARY.md @docs/planning/auth0-to-openiddict/phases/05-cutover-decommission/slices/S08A/S08A-SUMMARY.md @scripts/auth-migration/live-smoke.sh

Task 1: Prepare and validate the isolated rehearsal topology SyRF chart/package definitions, rehearsal preflight/tests and live harness; cluster-gitops isolated Application, namespace values, ExternalSecret/operator resources and inverse teardown Use separate isolated SyRF and cluster-gitops PR worktrees and extend only established chart/package plus Argo/Helm/Kustomize/ExternalSecret/operator patterns. Add closed, fixture-tested preflight modes for isolated provision and teardown, and extend the checked-in live harness with closed flags for the isolated route and forwarded-header matrix. Pin immutable validated API and Identity artifacts. Consume only S08A's rehearsal ACL user (~rehearsal:*) on the shared non-production Valkey, with bffAuth.redis.keyPrefix: "rehearsal:" and its namespace-local Secret contract; never use the staging credential or prefix (topology approved 2026-09-22). Declare a new restricted rehearsal route and dedicated namespace, Identity database, encryption/DataProtection material, OAuth clients, allowlisted callbacks, synthetic mail adapter references and least-privilege credentials. Render every resource and prepare a separate inverse GitOps PR before deployment. Reject floating tags, shared staging/production databases or clients, real-user/production/Auth0 exports, default Web/API provider changes, Terraform and manual cloud resources. SYRF_WORKTREE="\({SYRF_WORKTREE:?}" && GITOPS_WORKTREE="\)" && "\(SYRF_WORKTREE/scripts/auth-migration/assert-worktree.sh" "\)SYRF_WORKTREE" && "\(SYRF_WORKTREE/scripts/auth-migration/assert-worktree.sh" "\)GITOPS_WORKTREE" && bats "\(SYRF_WORKTREE/scripts/auth-migration/tests/scripts.bats" --filter 'isolated rehearsal' && "\)SYRF_WORKTREE/scripts/auth-migration/preflight.sh" --mode isolated-rehearsal --syrf-worktree "\(SYRF_WORKTREE" --gitops-worktree "\)GITOPS_WORKTREE" --environment staging --evidence /tmp/m005-s30-preflight.json && "$SYRF_WORKTREE/scripts/auth-migration/check-redacted-evidence.sh" /tmp/m005-s30-preflight.json Approve only when the provision and inverse diffs identify exact immutable revisions, bounded synthetic resources and route, separate data/secret/client domains, retained Auth0 defaults, and no production or ordinary staging Web/API mutation. The isolated topology and its inverse are reviewed, render cleanly and cannot receive default staging traffic or real-user data.

Task 2: Sync and run the complete synthetic live matrix None (isolated live staging evidence only) After the sole operator confirms the restricted callback, mailbox and secret-reference prerequisites, merge/sync only the reviewed rehearsal Application. Create only designated synthetic identities. Run password sign-in, SyRF confirmation/resend, forced reset admission, passkeys, optional MFA and recovery, Google sign-in/link/unlink with step-up and notification, profile completion, token claims, API/BFF/Swagger authorization, sessions and SignalR. Exercise trusted and deliberately untrusted X-Forwarded-Proto/X-Forwarded-Host inputs through the actual ingress; generated callbacks and emailed links must use only the allowlisted rehearsal origin. Capture bounded pass/fail, revision and aggregate evidence; never record credentials, cookies, participant identifiers or secret/resource identifiers. SYRF_WORKTREE="\({SYRF_WORKTREE:?}" && "\)SYRF_WORKTREE/scripts/auth-migration/live-smoke.sh" --environment staging --expected-provider openiddict --isolated-rehearsal --require-two-replicas --require-forwarded-header-matrix --evidence /tmp/m005-s30-live.json && "$SYRF_WORKTREE/scripts/auth-migration/check-redacted-evidence.sh" /tmp/m005-s30-live.json Approve only with the exact Synced/Healthy rehearsal revision, all synthetic matrix rows green, no real-user email/data, and independent proof that ordinary staging Web/API still use Auth0. The isolated deployed topology passes every required synthetic parity and ingress scenario while Auth0 remains staging's default.

Task 3: Sync the isolated teardown and prove rollback cluster-gitops isolated rehearsal Application and operator resources only Merge the separately reviewed inverse revision and wait for Argo reconciliation. Remove only the rehearsal Application/revision and synthetic-only resources according to their approved retention class. Do not disable the retained S08 Identity service or alter ordinary staging Web/API, Auth0, shared services, production, or real-user data. Use read-only discovery to prove the rehearsal Application, workloads, route, DNS and disposable synthetic state are absent; verify the retained staging Identity route and Auth0 defaults remain healthy. SYRF_WORKTREE="\({SYRF_WORKTREE:?}" && GITOPS_WORKTREE="\)" && "\(SYRF_WORKTREE/scripts/auth-migration/preflight.sh" --mode isolated-rehearsal-teardown --syrf-worktree "\)SYRF_WORKTREE" --gitops-worktree "\(GITOPS_WORKTREE" --environment staging --evidence /tmp/m005-s30-teardown.json && "\)SYRF_WORKTREE/scripts/auth-migration/check-redacted-evidence.sh" /tmp/m005-s30-teardown.json Approve only when the teardown has its own PR/revision/sync, no rehearsal object remains, the S08 Identity service is still healthy, and ordinary staging Web/API are still on Auth0. The synthetic topology is removed through GitOps with no effect on Auth0, retained Identity, ordinary staging, real users or production.

Fixture-tested provision/teardown preflight, exact rendered GitOps resources, full live parity and forwarded-header matrix, separate Argo revisions, and read-only absence evidence all pass. The isolated-rehearsal half of M005-R10 is complete; S09 may separately prepare an ordinary staging switch and Auth0 rollback.

After completion, create `slices/S30/S30-SUMMARY.md`.