Repository navigation
Description
This was generated by AI during triage.
Context
First real users are ready to start using the app. Hosting target is undecided, and this repo stays host-portable — so the audit separates portable app-hardening (lands in this repo) from host selection + provider config (lands in a separate deployment/ops repo — see the boundary below). Host selection is one of its outputs.
Repo boundary (host-agnostic)
Per AGENTS.md → Host-agnostic repo (deployment boundary): this repo carries no provider-specific config. The checklist below spans two homes:
- This repo (portable): the app-side of Deploy: production proxy-trust chain unsolved — silently disables #144 HSTS and the #143 per-IP login limiter #260 (fail-loud on an empty trusted-proxy list in prod), Deploy: support URI-form (postgresql://) connection strings — Npgsql's key-value-only parser rejects them #261 (URI-form connection-string support), Deploy: enforce TLS (sslmode) on every production Postgres connection #262 (require
sslmode), Deploy: separate the migration/owner DB role from the runtime app role (no DDL at request-time) #263 (role-split SQL + test), Deploy: gate production on a CI-built/tested image + readiness health-check + post-deploy smoke test #266 (CI image build/test +HEALTHCHECK), plus the whole RECOMMENDED tier (Deploy: container/image hardening — non-root USER, pinned base digests, image vulnerability scan #267–PWA never calls registration.update() — long-lived sessions run a stale shell #274) and Squash the 30 EF migrations into a single InitialCreate before first production deploy #245. - A separate deployment/ops repo (provider-specific): host selection, the concrete env values (proxy CIDRs, connection URLs, CA bundles), the deploy manifest (readiness path, wait-for-CI), CDN / DNS / edge config, secret delivery, backups / PITR, the observability backend, capacity sizing, and the deploy / rollback runbook.
Already hardened — verify still true under prod config, don't redo
Forwarded headers / HSTS / CSP / pinned AllowedHosts (#144), auth rate limiting (#143), HttpOnly refresh cookie + in-memory access token (#145), ReadOnly PII gating (#127), account lockout (#128), CI vulnerability + SAST gates (#146), telemetry pipeline + metrics (#214, #215), static-asset caching / CDN posture (#141).
Audit checklist — each failing item becomes a linked gap issue
- Secrets & config: production secret delivery for PEM JWT keys,
Seed:*credentials, DB connection string — no user-secrets in prod; per-host env-var story (PemKey.Normalizehandles escaped\n). - Database: managed Postgres vs self-hosted; automated backups + a restore drill. Epic EPIC: Phase 1.5 — Egg product hardening #15's "Backup/recovery workflow" is still open — decide whether it blocks go-live.
- Migrations:
Database:MigrateOnStartupposture for prod (fine single-instance; multi-instance needs a migration gate). Pre-deploy squash to a singleInitialCreatetracked in Squash the 30 EF migrations into a single InitialCreate before first production deploy #245. - TLS + domain: cert story, a CDN / edge in front, health-check endpoint for the host's probes.
- Observability in prod: where OTLP traces/metrics/logs land (hosted Grafana / OTel backend); alerting on error rate and container resources.
- Provisioning: first-account setup flow for a real farm (seeded admin credentials from
Seed:*, not demo data); password rotation after first login. - Capacity: instance sizing taken from the day-in-the-life simulation (Release rehearsal: day-in-the-life simulation of 10 users (all roles) with container-resource + HTTP-traffic monitoring #243).
- Runbook: deploy, rollback, restore-from-backup, JWT key rotation, refresh-token invalidation.
- Host selection: compare 2–3 candidates (a managed PaaS vs a VPS, …) against the items above; recommend one. → deployment repo.
Additional checks — review-fleet pass (2026-07-28, codex + 2 grounded agents; details in the comment below)
Each item below is a filed, linked gap issue. BLOCKERs must clear before first real users.
BLOCKER
- Production proxy-trust chain — leaving
TrustedProxiesempty silently kills Security: forwarded headers, HSTS, CSP + security headers, pinned AllowedHosts #144 HSTS and collapses the Security: rate limiting on anonymous auth endpoints (login/refresh) #143 login limiter to one global bucket (Deploy: production proxy-trust chain unsolved — silently disables #144 HSTS and the #143 per-IP login limiter #260) — app-side landed (PR feat(api): fail Production boot when RateLimiting:TrustedProxies is empty (#260) #293: Production boot fails loud on an empty trusted-proxy list +AllowNoTrustedProxiesopt-out — the silent failure is now impossible). The concrete edge CIDR is deploy config (tracked separately in the deployment/ops repo), enforced by the guard. Deploy: production proxy-trust chain unsolved — silently disables #144 HSTS and the #143 per-IP login limiter #260 closed here as a dup. - URI-form (
postgresql://) connection strings are unparseable by Npgsql → first deploy crash-loops (Deploy: support URI-form (postgresql://) connection strings — Npgsql's key-value-only parser rejects them #261) — done (PR feat(infra): URI connection strings (#261) + Production TLS floor (#262) #295:postgres:///postgresql://URIs translated to Npgsql key-value, incl. mapped libpq params likechannel_binding; unknown params skip-with-warning). - Enforce TLS (
sslmode=Require/VerifyFull) on every production Postgres connection (Deploy: enforce TLS (sslmode) on every production Postgres connection #262) — app-side done (PR feat(infra): URI connection strings (#261) + Production TLS floor (#262) #295: Production boot fails closed on anysslmodeweaker thanRequire— allow-list, so undefined modes throw too — warns if notVerifyFull;Database:AllowInsecureConnectionopt-out for the co-located compose stack). TheVerifyFullCA bundle + per-hostsslmodevalue are deploy config (in the separate deployment/ops repo). - Separate the migration/owner DB role from the runtime app role — no DDL at request-time (Deploy: separate the migration/owner DB role from the runtime app role (no DDL at request-time) #263) — app-side landed (PR feat(api): migrate command + prod migration split (#263) + extract CLI commands into a testable layer #288:
migrateCLI verb +appsettings.Production.jsonMigrateOnStartup=false+ compose migrate-before-serve ordering +/health/ready503 backstop). The host-specific role split (least-priv runtime role:GRANT USAGE+DML, no DDL;ALTER DEFAULT PRIVILEGESfor the migrator role; migrate job on a separate owner credential) is tracked in the separate deployment/ops repo; Deploy: separate the migration/owner DB role from the runtime app role (no DDL at request-time) #263 closed here as a dup. - Farm timezone provisioning — seeded
"UTC"+ undocumented tzdata/ICU image dependency (Deploy: farm timezone provisioning — seeded UTC + undocumented tzdata/ICU image dependency #264) — PR feat(api): farm timezone provisioning + tzdata/ICU boot guard (#264) #287 - Sole-Owner account recovery — no supported path; add a break-glass procedure (Deploy: sole-Owner account recovery has no supported path (break-glass procedure) #265) — PR feat(api): break-glass account recovery — recover-admin CLI (#265) #286
- Gate production on a CI-built/tested image + readiness health-check + post-deploy smoke test (Deploy: gate production on a CI-built/tested image + readiness health-check + post-deploy smoke test #266) — app-side done: PR chore(docker): image hardening — non-root user, pinned digests, Trivy CI, Dependabot (references #267) #294 (CI builds + Trivy-scans the image) and PR feat(api): container HEALTHCHECK via in-process
healthcheckverb (#266) #304 (theHEALTHCHECKverb + probe, CI boot smoke, and a/health/{**rest}404 guard). Deploy: gate production on a CI-built/tested image + readiness health-check + post-deploy smoke test #266 closed here as a dup; the deploy manifest + wait-for-CI gate + post-deploy smoke test are tracked in the separate deployment/ops repo. - (strengthen the Database item) backups need encryption / retention / PITR / RPO+RTO, not just "daily backups + restore drill" — folds into epic EPIC: Phase 1.5 — Egg product hardening #15's Backup/recovery workflow
RECOMMENDED
- Container/image hardening — non-root
USER, pinned base digests, image vulnerability scan (Deploy: container/image hardening — non-root USER, pinned base digests, image vulnerability scan #267) — done (PR chore(docker): image hardening — non-root user, pinned digests, Trivy CI, Dependabot (references #267) #294: non-root uid 1654, digest-pinned bases, Trivy HIGH/CRITICAL CI gate, Dependabot docker ecosystem, + trivy-action SHA-pinned after the 2026-03 supply-chain incident). Also delivered Deploy: gate production on a CI-built/tested image + readiness health-check + post-deploy smoke test #266's app-side CI image build. - JWT signing-key rotation needs a multi-key validation ring +
kid(JWT signing-key rotation needs a multi-key validation ring + kid #268) - Transient-DB resilience (
EnableRetryOnFailure) on the request path (Add transient-DB resilience (EnableRetryOnFailure) on the request path #269) - Purge expired
refresh_tokens(retention sweep; idempotency side already tracked in idempotency_records grows unbounded: full response body stored per successful write, no purge job #259) (Purge expired refresh_tokens (retention sweep; sibling of #259) #270) - Background worker single-runner guarantee — double-runs if scaled >1 instance (Background worker has no single-runner guarantee — double-runs if scaled >1 instance #271)
- Personal-data export / erasure / retention policy — GDPR + PH DPA (Personal-data export / erasure / retention policy (GDPR + PH DPA) #272)
- Production log redaction + retention + cost + a log sink (Console-only today) (Production log redaction + retention + cost + log sink #273)
- PWA
registration.update()— long-lived sessions run a stale shell (PWA never calls registration.update() — long-lived sessions run a stale shell #274)
NICE / footguns (not filed separately — see comment): Npgsql pool vs Postgres max_connections (fold into #243) · compose CLUCKWORK_HOST vs CLUCKWORK_ALLOWED_HOSTS name drift · seeded admin's Admin role re-asserted every boot · anonymous /health/ready DB round-trip per hit · no committed prod-config source of truth (a host deploy manifest).
Deliverable
- Readiness report committed under
docs/, with a go/no-go recommendation. - One gap issue per failing checklist item, linked back to this issue and added to epic EPIC: Phase 1.5 — Egg product hardening #15.
Follow-up security scan — 2026-07-31
A second Graphify-guided code, dependency, secret, container, and deployment review validated the controls already listed above and opened the residual gaps below. No source change is implied by checking an item here; each checkbox closes only when its linked issue acceptance criteria pass.
New app-side gaps
- Multi-replica HTTP idempotency is not atomic (Make API write idempotency atomic across replicas #307).
- Privileged user administration lacks recent step-up authentication (Require recent step-up authentication for privileged user administration #308).
- Authentication payloads and credential fields need pre-hash bounds (Bound authentication request bodies and credential inputs before password hashing #309).
- Late refresh completion can resurrect or overwrite browser sessions (Prevent late refresh responses from resurrecting or overwriting SPA sessions #310).
- Production report work and concurrency are not bounded (Bound production report workload, query size, and concurrency #311).
- Authenticated API and export responses lack origin private/no-store headers (Set private no-store cache policy on authenticated API and export responses #312).
- Raw pessimistic lock reads acquire by Id before tenant ownership is in the SQL predicate (Tenant-scope pessimistic lock reads before acquiring database row locks #313).
- Create User retains plaintext password state across dialog cancellation (Clear Create User plaintext password on every dialog close path #314).
- Docker restore does not consume committed NuGet locks in locked mode (Restore NuGet packages in Docker with committed lock files and locked mode #315).
- Production OTLP permits plaintext and logs unsanitized endpoint URIs (Require secure and redacted OTLP endpoint configuration in Production #316).
- A static test private key and narrow ignore rules weaken public-repository secret hygiene (Generate test JWT keys at runtime and exclude sensitive artifacts from Git and Docker contexts #317).
- The EF design-time factory falls back to public local credentials and skips remote TLS validation (Fail closed when the design-time migration connection is unset or insecure #318).
- Production can silently inherit wildcard AllowedHosts when deployment config drifts (Fail Production boot when AllowedHosts is wildcard or unset #319).
Private deploy/infra follow-up
Provider-specific findings, concrete configuration, topology, private issue identifiers, and remediation status are intentionally tracked in a private deploy/infra tracker and are not enumerated in this public issue.
Existing app issues strengthened by this scan
- Background worker has no single-runner guarantee — double-runs if scaled >1 instance #271 now states that background-job safety does not cover Make API write idempotency atomic across replicas #307 and the one-replica invariant remains until both land.
- Production log redaction + retention + cost + log sink #273 now requires safe structured login-failure, lockout, refresh-replay, and rate-limit events. Any host-specific alert configuration belongs in a private deploy/infra tracker.
- SPA UI verification track: Playwright E2E smoke + canary-under-load over the #243 sim fixture #277 now carries browser regression coverage for Prevent late refresh responses from resurrecting or overwriting SPA sessions #310 and maximum-range canary coverage for Bound production report workload, query size, and concurrency #311.
- Base provisioning: static ref-data into schema + secure first-run admin (generated secret + MustChangePassword) — retire Seed:* config #283 now explicitly separates first-run provisioning from post-login step-up in Require recent step-up authentication for privileged user administration #308.
PostgreSQL baseline follow-up — 2026-07-31
- PostgreSQL 18 pre-launch baseline (Upgrade pre-launch PostgreSQL baseline from 16 to 18 #321): update disposable dev/CI/simulation/reference databases, use the PG18 /var/lib/postgresql volume target, require the current patched 18.x minor and reviewed digest, and verify migrations, checksums, SCRAM, persistence, health, and pg_stat_statements. This is explicitly not a live production database migration.
Activity
mforce commentedon Jul 28, 2026
Additional go-live checks — review-fleet pass (codex + 2 grounded Claude agents)
A three-reviewer sweep of the codebase (codex + 2 repo-grounded Claude agents; the usual pi reviewer's endpoint was down) against this checklist surfaced concerns not yet listed. Deduped and ranked below; the parenthetical
(3/3)etc. is how many independent reviewers flagged it — a confidence signal, not a priority. Every item is grounded in a verifiedfile:line. Items already on the checklist or in the "already hardened" list are excluded.Each BLOCKER + RECOMMENDED item is filed as a linked gap issue (#260–#274), all under milestone Phase 1.5 and added to epic #15:
DATABASE_URLunparseable → crash-loopsslmode)kidEnableRetryOnFailureon request pathrefresh_tokens(idempotency = #259)registration.update()(stale shell)The NICE-to-have / footgun items below were not filed separately — they stay here for whoever works the audit. The backup strengthening (encryption/retention/PITR/RPO/RTO) folds into this milestone's existing Backup/recovery workflow item rather than a new issue.
BLOCKER — resolve before first real users
Production proxy trust is unsolved — silently kills Security: forwarded headers, HSTS, CSP + security headers, pinned AllowedHosts #144 HSTS and the Security: rate limiting on anonymous auth endpoints (login/refresh) #143 per-IP login limiter (3/3).
ForwardedHeadersclears the framework defaults and trusts onlyRateLimiting:TrustedProxiesnetworks (Program.cs:243-247), but the Railway draft tells operators to leave it empty (deploy-railway-cloudflare.md:159). Result behind Railway/Cloudflare:X-Forwarded-Protois ignored →Request.IsHttps==false→UseHsts()never emits the header andUseHttpsRedirection()is a no-op; and the login/refresh limiter keys on the proxy's single IP (Program.cs:297) → every user shares one 10-per-15-min bucket (one attacker locks out the farm). Fix per host: pin the exact immediate-proxy CIDR (or platform forwarded-headers), then verify HSTS is emitted, spoofed XFF is rejected, and two clients get two buckets. Own gap issue.Railway
DATABASE_URLis apostgresql://URI Npgsql cannot parse → first deploy crash-loops (1/3, concrete). The draft setsConnectionStrings__Default=$(DATABASE_URL)and claims Npgsql parses the URL natively (deploy-railway-cloudflare.md:69-75); it does not — Npgsql takes key-value form only, andMigrateAsync()runs eagerly at boot (Program.cs:523-524) so the process dies before listening. No URL→key-value shim exists (repo-wide grep). Fix: map Railway's discretePG*vars, or add apostgresql://→NpgsqlConnectionStringBuildershim. Blocks adopting the draft as-is. Own gap issue.App↔DB connection has no enforced TLS (
sslmode) (2/3). Provider passes the string straight toUseNpgsql(PostgresDbContextConfigurator.cs:12); compose uses bareHost=db;…(docker-compose.yml:12), draft forwards$(DATABASE_URL)unchecked. Npgsql defaults toSSL Mode=Prefer→ silent plaintext fallback, no cert validation → DB password + farm/customer PII exposed over any non-loopback hop. Fix: requireSSL Mode=Require(ideallyVerifyFull+ root cert); fail deploy if the effective mode permits plaintext. Own gap issue.Migration/owner DB role not separated from the runtime role (2/3, codex=BLOCKER). One connection string serves every request and runs boot-time DDL with
MigrateOnStartupdefault-true (Program.cs:523,docker-compose.yml:12usesPOSTGRES_USERfor both). An app-tier RCE/injection gets DDL (DROP/ALTER), not just DML. Fix: migrate from a pre-deploy job with an owner credential, setDatabase:MigrateOnStartup=falsein prod, grant the runtime role DML-only. Pairs with the Squash the 30 EF migrations into a single InitialCreate before first production deploy #245 squash. Own gap issue.Seeded farm hardcodes timezone
"UTC";IFarmClockfails closed (2/3).DatabaseSeeder.cs:156seeds the account at UTC, andFarmClockthrows rather than substituting a zone (FarmClock.cs:29-41) — so until an admin sets the real IANA zone, every "today" boundary, the 7-day auto-lock sweep, and FIFO lot availability run on UTC. Also a latent image constraint: the Debian base ships tzdata/ICU, but nothing pins it — an Alpine/chiseled image orInvariantGlobalization=truebreaks every farm's date logic at once. Fix: make timezone confirmation a cutover gate (or seed fromSeed:TimeZoneId); document "image must contain tzdata"; add a boot smoke assertion that a representative zone resolves. Own gap issue.Sole-Owner account recovery has no supported path (2/3). No email/SMTP anywhere; self-service change needs the current password; an Owner cannot reset their own via the users route (
UserEndpoints.cs:32,139); re-seeding never resets an existing admin's password (DatabaseSeeder.cs:193-211). A single-Owner farm that loses the password → DB surgery only. Fix: a tested break-glass CLI (reset user, revoke refresh tokens, rotate security stamp, write an audit row), or require two Owner accounts before cutover. Own gap issue.CI is not a deploy gate; the Docker image is never built or tested in CI (2/3).
ci.ymlbuilds/tests source but never builds the multi-stage image (npm ci, dist→wwwroot copy, sourcemap move) — a Dockerfile break first appears at deploy; Railway auto-deploys on push independent of Actions unless "Wait for CI" is enabled. Fix: add adocker build+ boot-against-throwaway-Postgres +/health/readyjob; require CI before promotion; add a post-deploy smoke test (live/ready, SPA loads, login, refresh cookie, one authed read) through the real Cloudflare domain. Own gap issue.RECOMMENDED — before or immediately after launch
USERin the Dockerfile (Dockerfile:29-37; base ships a non-rootappuser unused);node:22-alpine/sdk:10.0/aspnet:10.0by floating tag; Dependabot covers Actions/npm/NuGet but not Docker. Fix:USER $APP_UID, pin bases by digest, add Trivy/Grype on the built image in CI, schedule base-image rebuilds.kidsupport (2/3). OneIssuerSigningKey(Program.cs:330), tokens carry nokid(JwtTokenService.cs). The runbook's "JWT key rotation" isn't mechanically possible without 401-ing every live access token. Fix: validation key ring (IssuerSigningKeys) +kid+ keep the old public key one access-token lifetime.EnableRetryOnFailureabsent (PostgresDbContextConfigurator.cs). The Hardening: DurableJobWorker crash takes down the whole API on a transient DB outage #65 worker recovers from DB blips; HTTP requests throw straight to 500 on a managed-PG failover. Fix: bounded Npgsql retry strategy; wrap the export's explicitRepeatableReadtransactions inCreateExecutionStrategy().ExecuteAsync./health/live+/health/ready(DB+migrations), but there's norailway.json(healthcheckPath) and the composeappservice has nohealthcheck/ the Dockerfile noHEALTHCHECK— so an instance with/health/ready==503 still gets traffic and deploys aren't readiness-gated. Fix: commitrailway.json healthcheckPath=/health/ready(+ timeout covering boot migration); add a compose healthcheck.refresh_tokensandidempotency_recordsgrow unbounded — no purge (2/3). Only sweep isDailyEntryLockSweep; nothing deletes expired refresh tokens or old idempotency rows (which store the full response body forever,IdempotencyMiddleware.cs:109). Months → index bloat + indefinite payload retention. Fix: a periodic worker sweep with retention windows (idempotency past the client-retry window; refresh lineage past reuse-detection need).DurableJobWorker(Program.cs:500); job poll +DailyEntryLockSweepclaim no rows (noFOR UPDATE SKIP LOCKED/lease). Two instances double-run. Fix: document "single API instance" as a deploy invariant now; add advisory-lock/SKIP LOCKEDclaiming before any horizontal scale. (Distinct from the checklist's multi-instance migration gate — this is runtime jobs.)Customer.cs) and user emails; only an admin farm-wide CSV export (F24: CSV export / manual backup — per-dataset CSVs, full account export (zip), self-host dump instructions #95), no per-subject DSAR or erasure;AuditEventpinsActorEmailimmutably. No retention rule. Relevant to EU/UK GDPR and the PH Data Privacy Act (the es/tl locales point at PH). Fix: classify fields, set retention, define export + erasure-vs-pseudonymization with a documented audit-trail exception, propagate deletes into backups/telemetry.appsettings.json) — no OTLP log sink, so structured logs (Observability: structured logs at domain state transitions #216) land only in the host's console with host-default retention. The anonymous/client-errorsendpoint writes caller-controlled messages + full stacks (ClientErrorEndpoints.cs:96) and boot logs real admin emails (DatabaseSeeder.cs:193). Fix: define forbidden fields + redaction, retention, access control, and a volume/cost budget for the chosen sink.skipWaiting) but nothing callsregistration.update()— a barn phone left open all day instandalonemode generates no navigation, soupdatefoundnever fires and theUpdatePromptnever appears; the client runs the old shell against the new/api/v1. Fix: periodicregistration.update()+ one onvisibilitychange→visible; keep prompt-not-auto behavior.NICE-TO-HAVE / footguns
max_connectionsundocumented (2/3) — default 100/instance; fold into the capacity note (Release rehearsal: day-in-the-life simulation of 10 users (all roles) with container-resource + HTTP-traffic monitoring #243) so a future 2nd instance can't exhaust connections.docker-compose.yml/.env.examplereadCLUCKWORK_HOST, butdeploy/.envsetsCLUCKWORK_ALLOWED_HOSTS→--profile proddeploy gets emptyAllowedHosts→ 400s on the real domain. Reconcile the name.DatabaseSeederre-adds the role to the existing admin each start; a deliberate demotion is undone by a restart. Stop re-asserting for existing users./health/readydoes a DB round-trip per hit (1/3) — minor load amplifier; cache the readiness result a few seconds or accept it behind Cloudflare.railway.json+ env-parity table would close the drift (already seen inTrustedProxies). Folds into the deploy-gate item.Verified-clean — do NOT re-open (reviewer consensus)
CORS (N/A — same-origin SPA from wwwroot) · prod error leakage (generic
Results.Problem, no dev page) ·/client-errors(CRLF-sanitized, byte-capped) · session invalidation on password change (all refresh tokens revoked in-transaction; documented residual: an access token stays valid ≤15 min after revocation, by design) · refresh-token theft (hash-only, rotation, reuse-detection + family revoke, xmin CAS) · #65 worker/DB-outage crash (fixed) · SWskipWaitinghazard (not present — only the update-detection gap above) · auth cookieSecure(env-based, safe behind the plain-HTTP proxy hop) · demo data leaking to prod (gated onSeed:Demo=false + empty catalog).feat(api): farm timezone provisioning + tzdata/ICU boot guard (#264) (#…
feat(api): migrate command + prod migration split (#263) + extract CL…
feat(api): fail Production boot when RateLimiting:TrustedProxies is e…
docs(agents): drop the private deploy repo's name from the host-agnos…
mforce commentedon Aug 16, 2026
Checklist item Provisioning: first-account setup flow for a real farm is superseded by #533 (slice 3 of epic #530, Phase 1.6 — Multi-farm tenancy).
#533 adds
provision-account— a run-then-exit CLI verb that creates an account, its per-account reference data (egg grades, packed-unit conversions) and its Owner with a freshly generated password printed to stdout only, all in one transaction and attributed to a system actor per #500. That is the real answer to "first-account setup flow for a real farm", and it generalises: farm #2 onward gets the same path rather than a hand-written INSERT.Note the checklist item as written still references
Seed:*credentials, which #283 retired along with the runtime seeder. The current answer isbootstrap-adminfor the default account andprovision-accountfor every farm after it.Tick this item when #533 merges.
Two other items on this audit are worth re-reading in light of #530:
docs/runbooks/break-glass-account-recovery.mdgains an ambiguous-email exit path in Auth: login by farm code, per-account email identity, immediate suspension #532, since email stops being globally unique.mforce commentedon Sep 13, 2026
Closing — the gate this issue describes has already been passed
Closed on 2026-09-13 during the issue cleanup, with the status recorded rather than assumed.
This was written on 2026-07-28 as a pre-go-live scan: "production go-live scan before first
real users." The app is now in production. A gate that has been passed cannot be a gate; leaving
it open as tier1 made the four-issue tier1 tier read as "blocking launch" when launch happened.
The 15 gap issues it spawned — verified 2026-09-13, not recalled
13 closed:
migrateverbrecover-adminregistration.update()Each carries a decision record in
docs/decisions/.2 open, and they do not need this tracker:
kid. Keepspriority:tier1.priority:tier1.Both are self-contained and reviewed on their own terms.
What genuinely remains is not in this repo
The rest of this issue's cross-references point at
cluckwork-deploy: trusted-proxy CIDRs (#5),deploy manifest with migrate-before-serve and the readiness gate (#6), post-deploy smoke test
through the production URL (#7), CDN/DNS/TLS edge (#8), backups — encryption, retention, PITR,
declared RPO/RTO, restore drill (#9), and the operational runbook (#12).
Per
AGENTS.md's host-agnostic boundary, provider manifests, IaC, edge config and concreteenvironment values belong in that repo, not this one. Tracking them from a Cluckwork issue was
always going to double-count them.
The one worth flagging out loud:
cluckwork-deploy#9, backups and a restore drill. An app inproduction without a verified restore is the highest-severity item anywhere in this cleanup, and it
is not a Cluckwork issue.
Not claimed
This close-out does not claim the deployment is fully hardened. It claims the audit is
finished and its findings are dispositioned. A fresh post-go-live review — backups verified,
restore drill executed, runbook current — is worth doing and would be a new issue with today's
evidence, not a two-month-old pre-launch scan.