#CAS Middleware Operations
Runbooks, SLOs, and alerting for the independently deployed CAS middleware
(the @unicas packages in packages/). Production topology:
| Component | Worker / resource | Notes |
|---|---|---|
| UniCAS service (public) | unicas |
Single @unicas/service-cloudflare Worker for /v1/cas, /admin, MCP/OAuth, and admin UI |
| Spaces file App | unicas-spaces |
Separate @unicas/spaces Worker, Google user login, App-owned issuer, file catalog, and release smoke |
| OAuth KV | dedicated OAUTH_KV namespace |
OAuth clients, grants, token hashes, and encrypted authorization transactions |
| Control D1 | unicas-control (3a64d58d-…) |
Apps, issuers, members, and audit; physical tables still use Stack names |
| Data D1 | unicas-tenant (c3924c96-…) |
App/Space nodes, edges, and Root Refs; resource name is a physical compatibility identifier |
| R2 | unicas-content, unicas-content-preview |
node content |
EMAIL send binding |
Microsoft invitation challenges from the configured UniCAS sender | |
| Spaces D1 | unicas-spaces |
App Principals, identity bindings, sessions, file roots, and bounded smoke cleanup state |
Secrets live only as Worker secrets (Google, Microsoft, and GitHub client secrets,
SESSION_ENCRYPTION_KEYS, OAUTH_STATE_ENCRYPTION_KEY,
CAS_AUDIT_READER_KEY) — never
in vars or source. Deployment credentials are supplied through
CLOUDFLARE_ACCOUNT_ID / CLOUDFLARE_API_TOKEN; see
Deployment and local configuration.
#SLOs and error budgets
| SLO | Target | Measurement | Error budget (30d) |
|---|---|---|---|
| Service availability | 99.9% | /health + Space/admin request success |
43.8 min |
| Space + admin availability | 99.9% | /v1/cas + /admin success |
43.8 min |
| Service p95 latency (live) | < 500 ms | Worker request duration | — |
| Space node read p95 (cached/DB) | < 200 ms | node metadata/content reads | — |
| Spaces APAC directory TTFB p95 | ≤ 1 s | At least 30 authenticated synthetic /api/entries calls from the selected APAC probes |
— |
| Spaces directory Worker wall p95 | ≤ 500 ms | unicas-spaces response-construction wall time for the same bounded run |
— |
| Spaces cold directory request | ≤ 2 s | First authenticated synthetic request after a controlled cold interval | — |
| Key rotation effectiveness | new key ≤ 60 s, revoked key ≤ 60 s | JWKS cache bounds (30 s TTL / 60 s hard stale) | — |
| Administrator revocation | denied on the next browser, CLI, or MCP request; never later than 60 s | focused authorization probes and platform audit | — |
| Backup freshness | RPO ≤ 24 h | last successful D1 export timestamp | — |
| Restore | RTO ≤ 30 min | restore drill from exported SQL | — |
Error-budget burn: alert at 5% of monthly budget consumed per rolling 24 h,
page at 15%. Availability is measured from the unified service (/health plus
a routed probe like the live smoke's lease+read).
#Metrics and events
Cloudflare's aggregate Worker metrics are authoritative for request volume, invocation outcomes, and CPU/wall-time quantiles. Persisted custom logs use a 5% head sample and are diagnostic evidence only. Generated invocation logs, native Cloudflare tracing, and production manual trace export are disabled by the observability contract. The manual OTLP code path is checked in with a zero production sample and no destination or exporter secrets.
Existing structured events (JSON to Worker stdout and sampled Workers Logs):
cas_app_authorization— App/Space v1 authorization decisions. Events usekind∈authorized,rejected,fail_closed,registry_stale.fail_closedis an incident signal (registry unreachable past the hard bound, or a cold outage). Rejected requests carry stable capability error codes such asunknown_issuerandregistry_unavailablein their HTTP responses.admin_oidc_callback_failed— administrator login callback failures with a bounded reason and no token material.admin_email_challenge_delivery_failed— generic Email binding failure; the event contains no address, code, invitation token, or provider payload.unicas_*_failed— bounded incident locations without raw errors, URLs, identifiers, storage keys, or content.
Use Worker/zone metrics or the Cloudflare GraphQL API for five-minute request and status rollups. Query sampled event names and bounded reason/error codes in Workers Logs to diagnose a metric or probe signal; do not scale the 5% sample into an authoritative count. Manual D1/R2/Durable Object waterfalls are available for synthetic validation, but production export remains dormant until destination access and retention, canary evidence, and the exact nonzero sample receive review.
#Alerting rules
| Alert | Condition | Severity | Response |
|---|---|---|---|
| Service 5xx rate | > 1% of requests over 5 min | P1 | Check wrangler deployments list for unicas; rollback if a recent deploy regressed |
fail_closed burst |
either authorization event kind=fail_closed ≥ 3 in 5 min |
P1 | D1 reachability from the service Worker; registry row integrity |
registry_unavailable 401/403 rate |
> 0.5% of Space requests over 5 min | P1 | Same as above |
| Unknown-issuer spike | unknown_issuer > threshold after a rotation |
P2 | Issuer active? discovery still resolves to the expected jwks_uri? |
| Key age | any active issuer key older than 90 days | P2 | Run the rotation drill |
| Backup failure | scheduled D1 export fails | P2 | Re-run export; verify file non-empty (--remote!) |
| Admin OIDC failures | login error rate > threshold | P2 | Google client config, redirect URI, session keys |
#Runbooks
#Lease-driven upload cleanup
App/Space v1 creates at most 1024 active node-upload generations per Space. Presigned PUT URLs default to five minutes. Uploaded or abandoned temporary objects remain eligible for recovery until their 24-hour cleanup deadline; the bounded Space GC path removes expired records, reservations, and temporary objects without requiring another lease for that hash.
reservedBytes includes reservations whose byte length is known after
validation (and retained v1 server-mediated uploads). It does not include an
unvalidated direct R2 object because the v2 request declares no length. The
active-generation limit, publication-time 32 MiB check, short URL lifetime,
and cleanup deadline bound that pre-validation exposure instead.
Alert on repeated STORAGE_ERROR, CAS_UPLOAD_LIMIT, or ready-record/object
inconsistency. Never respond to a missing canonical object for a ready row by
issuing a fresh upload target. R2 PutObject does not currently support a
SHA-256 FULL_OBJECT checksum, so the 32 MiB limit and SHA-256 identity are
enforced when the next lease validates the temporary object, not by R2 while
receiving the PUT.
#Deploy
The UniCAS service remains one middleware deployment unit, while a complete
production release publishes four independently owned Workers: service,
Spaces App, product site, then documentation site. A promotion pull request from main
into release is the normal release boundary. Its merged release revision
runs the protected deploy-production job after CI validation. That job checks
out and builds the exact validated commit, runs canonical service smoke,
deploys Spaces and runs its file smoke, deploys the two static Workers, and verifies all five public
origins. A separate job then records a successful push deployment as an
immutable production-YYYYMMDD-<workflow-run-number> annotated tag pointing
to that exact revision; manual recovery runs do not create production tags.
For read-only manual release validation, dispatch CI; it runs
pnpm validate:release and cannot enter Production. Spaces bootstrap or
principal recovery uses the separate Recover Spaces production workflow.
Supply a full commit SHA reachable from main, select exactly one recovery
operation, and pass Production review. Recovery shares the
unicas-production concurrency group with promotion and never creates a
production tag.
Documentation-only publication uses the separate Deploy documentation
workflow. Supply the full lowercase SHA of the current main tip and pass
Production review. The workflow requires the checkout and refreshed
origin/main to equal that SHA before it runs repository code, shares the
unicas-production concurrency group with complete releases, and runs docs
tests, a browser test, a Wrangler dry-run, the docs deployment, public SDK route
checks, and an artifact-manifest.json source-revision check. It cannot deploy
the service, Spaces, product site, npm packages, tags, or GitHub Releases.
Local emergency deployment remains explicit. Always rebuild first because
Wrangler uploads dist/ and stale output silently deploys old code:
pnpm release:service:plan
pnpm release:service:production
pnpm release:spaces:plan
pnpm release:spaces:production
pnpm release:site:production
pnpm release:docs:production
pnpm release:service:smoke # App/Space v1; run twice 70s apart
pnpm release:spaces:smoke -- --base-url https://spaces.unicas.work
pnpm release:service:production applies pending CAS_DB D1 migrations after the
service build and before Worker publication. Tenant schema initialization is
not a request or cron responsibility. A migration error stops publication;
do not deploy the new Worker separately. Worker rollback does not roll back
the D1 migration ledger, so every future migration must state backward
compatibility or a forward-repair procedure.
The Spaces Worker uses the explicit aws:ap-southeast-1 placement hint rather
than fixed IAD or Smart Placement. SPACES_DB remains in ENAM, while the
UniCAS tenant D1 and Durable Object data plane are in APAC. An authenticated
directory read therefore pays one bounded session query across regions before
executing the dominant manifest path near the APAC data plane. Do not switch
back to Smart Placement without a new canary: optimizing the first query can
increase the downstream Durable Object dispatch time.
Treat the placement hint as a candidate topology, not proof of latency. Before
accepting a deployment, run at least 30 authenticated synthetic directory reads
from the agreed APAC probes, report p50/p95/p99/max for TTFB and Worker wall
time, record the observed colo/placement, and verify a controlled cold request.
Compare spaces_session, spaces_manifest, spaces_unicas, spaces_root
when present, cas_auth, cas_do, applicable cas_d1_*/cas_r2_*, and
cas_edge. Do not report response-construction timing as full stream
completion.
If explicit APAC placement misses any directory threshold, restore the last
accepted Spaces version and placement configuration before evaluating an APAC
SPACES_DB migration. A D1 move requires a separate copy, consistency,
cutover, rollback, and data-validation plan; do not infer it from a broad
region label.
The App/Space smoke script uses one dedicated /deploy-smoke Space, per-run node hashes,
and per-run request IDs. After acquiring the parent Root Ref, it releases that
ref in a finally path even when a later assertion fails, so subsequent runs
do not accumulate positive Root Refs. It also checks an empty isolation Space
and rejects both token/route version mismatches.
HTTPS targets skip the paused-body concurrency probe because Cloudflare ingress
buffers the request body; set UNICAS_SMOKE_ENABLE_CONCURRENCY=1 only when the
target preserves streaming ingress. Running smoke twice with a 70s gap also
proves the authority-cache refresh path.
The Spaces smoke uses a dedicated Principal and Space. Each run creates a temporary file Root, records paths before mutation, uploads a multi-node file, requires immutable-node reuse on a second upload, and checks exact authority and Space denials. Cleanup uses a stable Root Ref request ID, removes the catalog row only after release, and is safe to repeat. The scheduled handler retries at most ten expired or failed runs per invocation. Detailed bootstrap, rotation, and recovery steps are in Spaces file App operations.
#Failure diagnosis
Use the named failed step and its non-secret Wrangler output to identify the deployment unit. Validation failures publish nothing. A failure in the service publish or canonical smoke leaves the product and documentation Workers untouched, although the service version may already be live. A Spaces deploy or smoke failure occurs after the service smoke and blocks both static sites. A product-site failure occurs after both service smokes. A documentation failure occurs after all earlier units succeed. A final HTTPS-check failure means the publishes completed but one public route did not return a successful response.
A tag-production failure happens only after all deployment and verification
steps passed, so production is live even though its audit marker is missing.
Use Re-run failed jobs on that original workflow run; do not dispatch a new
manual run or create a tag by hand. The retry is idempotent when the existing
tag resolves to the deployed commit and fails without moving it on a conflict.
Follow Deployment and local configuration
for tag inspection, ruleset policy, and conflict escalation.
For any failure after a Wrangler command starts, compare the affected Worker's
wrangler deployments list output with the workflow commit and timestamps.
Do not print environment values or private-key files while diagnosing. The
workflow's always() cleanup removes its ephemeral smoke key even when the
service or smoke step fails. Fix a transient or configuration problem and use
a manual release dispatch; use version rollback for a bad artifact.
#Credential rotation
To rotate the Cloudflare deployment token, create a replacement from the
Edit Cloudflare Workers template with the same single-account and
unicas.work zone scope. Replace the CLOUDFLARE_API_TOKEN secret in the
GitHub Production environment, complete a green manual run from release,
then revoke the old token.
To rotate the smoke signer, first publish the replacement public key through
the tracked https://unicas.work/deploy-smoke JWKS while retaining the old
key. Deploy the product-site Worker and wait for the verifier-cache overlap
window. During a deployment-free window, replace
UNICAS_SMOKE_PRIVATE_KEY_PKCS8 and UNICAS_SMOKE_KID in the same GitHub
environment, then complete a green manual run before removing the old public
key in a later product-site deployment. The issuer URL and audience do not
change during signer rotation, so no new control-plane activation is needed.
Rotate Worker runtime secrets separately with wrangler secret put; never
copy them into GitHub deployment configuration.
#App invitation management
App members can inspect invitation history with
GET /admin/apps/{appId}/member-invitations, filtering by
status=pending|accepted|expired|revoked and traversing { items, nextCursor }
pages with limit (1..1000). Cursors bind the App, status filter, and control
snapshot. Restart traversal after INVALID_CURSOR; current membership is
checked for every page. Reads never return bearer tokens, token hashes, or
accept URLs.
DELETE /admin/apps/{appId}/member-invitations/{invitationId} requires that
invitation's exact If-Match, not the App revision. A pending, unexpired
invitation becomes revoked and returns 204 with its new ETag. A revoked
record with a current ETag is a no-op; a stale version returns 412, and an
accepted or expired invitation returns 409 INVITATION_NOT_PENDING.
List, accept, and revoke operations reconcile elapsed pending invitations to durable expired state. Accepted, revoked, and expired transitions advance the invitation revision and append immutable-actor audit evidence. Acceptance and revocation race under one atomic storage precondition, so they cannot both succeed. Records remain available for history rather than being deleted.
V2 creation returns only { invitationId, acceptUrl, expiresAt } and the
invitation ETag. Acceptance returns only { appId } for navigation. The
Console's Members section includes Administrators and Invitations views with
filtering, refresh, paging, and revoke confirmation. A freshly created URL is
copyable only from that creation result and is cleared when switching Apps;
the invitation list cannot recover it.
#Platform Account bootstrap and reset
Administrator authorization is deny-by-default and belongs to the stable
Account. platform.admin and apps.create are independent child authorities;
App membership grants access only to that App. Exact provider issuer/subject is
stored only on External Identity and retained with audit evidence.
Fresh control-schema initialization creates only the current Account/App model.
It does not inspect, synthesize, backfill, or upgrade legacy administrator data.
For the authorized internal-development production cutover, use
release/service/reset-smoke.mjs from the exact release candidate. Its
preview inventories the fixed production bindings and refuses unknown tables,
Apps, Spaces, issuer configuration, R2 prefixes, or KV key formats. Execution
requires the exact Smoke App ID and either a reviewed backup directory or the
explicit DELETE-ALL-TEST-DATA-NO-BACKUP confirmation.
Execution first deploys and verifies a maintenance Worker on both service
origins, directly enumerates the dedicated R2 and KV bindings, then repeats the
D1 inventory and rejects any change. Pending direct-upload sessions or physical
objects outside the Smoke node and temporary-upload prefixes stop the reset. It
captures the authorized Google issuer/subject entirely inside D1, clears the
bounded test state, creates exactly the current Account/App and App/Space schemas, and
bootstraps the Account, two platform authorities, Smoke App membership,
external issuer, and audit evidence. The subject is never returned through
Wrangler or written to a local file. Any failure leaves maintenance active;
only the held release workflow restores the normal Worker. If schema creation
completed but final verification was interrupted, rerun with --verify-current
and the exact App ID instead of attempting the destructive path again.
Bootstrap the first Account through the local bootstrap or a restricted, environment-specific initialization procedure that atomically creates:
- one
cas_accountsrow and itscas_account_profilesrow; - one exact
cas_external_identitiesbinding; platform.adminand, when required,apps.createrows incas_account_platform_authorities.
Do not derive an Account from email, write Principal/operator compatibility rows, or copy provider identity into membership rows. Verify bootstrap with count-only queries over active Accounts and Account authorities, then establish a second Platform Admin through the protected API.
Platform invitation idempotency replay stores its bearer token only as an
encrypted value under SESSION_ENCRYPTION_KEYS; retain old keys until sessions
and pending invitation receipts sealed with them have expired. If D1 is
unavailable, authorization fails closed; there is no bypass flag or fallback
email allowlist.
#Suspend and restore an App
An App member can pause all App Space data-plane operations without deleting data or changing memberships, issuer configuration, or Root Ref retention:
unicas apps update <appId> --status suspended --etag '"4"'
unicas apps update <appId> --status active --etag '"5"'
Read the current App with unicas apps get <appId> before selecting an ETag.
The HTTP operation is PATCH /admin/apps/{appId} with If-Match; success is
204 No Content with the resulting App ETag. CLI and MCP return only
{ etag }. A current same-value update is a no-op; stale preconditions fail
with 412 REVISION_MISMATCH. Actual transitions append app.suspended or
app.restored control audit events with the immutable actor identity.
Suspension rejects reads, metadata reads, leases, Root Ref reads/updates,
usage, and GC with 403 APP_SUSPENDED once the verifier observes the change.
Capabilities issued before suspension are covered.
Issuer authority refreshes after 30 seconds and fails closed at 60 seconds
since its last successful read if registry refresh fails. Failure must never
preserve old access beyond that hard bound; requests then fail with registry
unavailability until current state can be read. An in-flight operation already
authorized before the boundary is not rolled back.
Protected-resource metadata is served only for active Apps; suspended Apps return an unavailable-issuer response. Prior positive discovery responses may remain cached for at most their 60-second max-age, but do not override capability verification.
App administrator reads and recovery mutations remain available, including metadata repair, membership and invitation administration, issuer repair, audit, and Restore. Suspension neither releases Root Refs nor initiates GC. Restore leaves data and configuration intact; verifiers observe it on refresh. The existing authorization event stream records suspension denials without recording bearer capabilities. The App/Space verifier refuses a suspended owning App so it cannot bypass this operational stop.
#Inspect App usage
The Console App Overview reads current aggregate CAS usage through:
GET /admin/apps/{appId}/usage
Cookie: cas_admin_session=<HttpOnly OIDC-backed session>
Current App membership is required; no Space capability is used. Suspended Apps remain inspectable. The response reports node count, logical and physical ready bytes, upload reservations, missing-content nodes, and leased nodes summed across independently accounted Spaces.
The read queries cas_space_usage, one mutable projection row per usage-
bearing Space, and never scans App node rows or R2. Node and reservation writes
maintain that row transactionally. Scheduled maintenance emits
cas_usage_reconciliation with only examined/observed/missing/failure counts
while it backfills and rotates through canonical-object observations. A 503 SERVICE_UNAVAILABLE means at least one node has not completed observation or
the accounting store is unavailable; retry after reconciliation advances. Do
not treat it as zero usage.
The Console Usage card also exposes an App-member recovery action:
POST /admin/apps/{appId}/gc
Cookie: cas_admin_session=<HttpOnly OIDC-backed session>
X-CSRF-Token: <session token>
Content-Type: application/json
{}
The action records app.gc.requested in the control audit log, enumerates a
bounded keyset page of usage-bearing Spaces, and dispatches each Space through
its normal keyed mutation actor. It never bypasses the Space GC eligibility
checks: only nodes with zero Root Refs, zero child references, and an expired
lease may be deleted. One request has fixed Space and per-Space node budgets
and may return a nextCursor; repeat with that cursor to finish the current
App sweep. Run another sweep after deletions because removing parents can make
their children eligible in a later pass.
#Rollback
Wrangler retains prior versions. Inspect each unit that may have changed and select the known-good version ID. If more than one unit changed, roll them back in reverse deployment order: documentation, product site, Spaces, then service. Skip units that the failed workflow never reached.
A Worker version rollback changes the code receiving traffic; it does not undo D1, R2, KV, Durable Object storage, or Worker secret changes. Inspect the target and failed versions for binding or Durable Object migration compatibility before rolling back. Routine releases in this workflow do not rewrite resources or runtime secrets, but a future migration can make an older service version unsafe to restore.
pnpm --filter @unicas/docs-site exec wrangler deployments list --config wrangler.jsonc
pnpm --filter @unicas/docs-site exec wrangler rollback <docs-version-id> --config wrangler.jsonc --yes --message "rollback failed production release"
pnpm --filter @unicas/site exec wrangler deployments list --config wrangler.jsonc
pnpm --filter @unicas/site exec wrangler rollback <site-version-id> --config wrangler.jsonc --yes --message "rollback failed production release"
pnpm --filter @unicas/spaces exec wrangler deployments list --config ../../.wrangler/spaces/wrangler.production.json
pnpm --filter @unicas/spaces exec wrangler rollback <spaces-version-id> --config ../../.wrangler/spaces/wrangler.production.json --yes --message "rollback failed production release"
pnpm --filter @unicas/service-cloudflare exec wrangler deployments list
pnpm --filter @unicas/service-cloudflare exec wrangler rollback <service-version-id> --yes --message "rollback failed production release"
After rollback, rerun the canonical smoke with provisioned operator
credentials and check https://api.unicas.work/health,
https://console.unicas.work/, https://spaces.unicas.work/, https://unicas.work/, and
https://docs.unicas.work/. Do not use a source rebuild as a substitute for an
explicit version rollback: rebuilding a moving branch does not identify the
artifact receiving traffic. The service rollback procedure was verified in
the admin drill on 2026-08-26; include the two static Workers in the next full
rollback drill.
#Backup and restore
Backup (manual or scheduled; daily target):
wrangler d1 export unicas-control --remote --no-schema --output <cutover>/unicas-control.sql
wrangler d1 export unicas-tenant --remote --no-schema --output <cutover>/unicas-tenant.sql
pnpm --filter @unicas/spaces exec wrangler d1 export SPACES_DB --remote --config ../../.wrangler/spaces/wrangler.production.json --output ../../.wrangler/spaces/unicas-spaces.sql
--remote is mandatory (without it wrangler exports an empty local DB).
Store the SQL off-box (the gitignored .wrangler/ copy is a working backup,
not a durable one). R2 content is referenced by node hashes in the physical
unicas-tenant D1
backup; a restore re-verifies blobs through the canonical read path.
For the split-origin smoke-only cutover, review the live reset plan without executing it:
node release/service/reset-smoke.mjs `
--expected-stack-id <current-smoke-stack-id>
This is a physical pre-cutover tool: its Stack/Tenant flags and output match the
current D1/R2 schema and are not v2 aliases. The command refuses inventories
containing another physical partition in any scoped control or data table or
an unexpected object-key shape. The rendered D1 commands drop the legacy physical tables;
they do not merely delete rows, because retained stack_id/tenant_id columns
would block the new Worker from creating the clean App/Space schema.
With --backup-dir, pass a fresh empty directory. After maintenance is active,
the script creates both D1 exports itself, downloads every physically
enumerated R2 object, verifies each canonical object's SHA-256, and writes
backup-manifest.json with D1 and R2 sizes and hashes. Existing export or
manifest files abort the operation so stale backups cannot satisfy the guard.
Run the destructive command only inside the approved maintenance window, then
deploy the new Worker immediately so it can create the app_id/space_id
tables. Do not reuse this one-time pre-cutover tool after the physical cutover.
After reviewing the printed R2, OAuth KV, and D1 commands, execute with the
same explicit physical Stack ID and either a fresh backup directory:
node release/service/reset-smoke.mjs --execute `
--expected-stack-id <current-smoke-stack-id> `
--backup-dir <off-machine-cutover-directory>
or, only when test-data disposal is explicitly approved, omit the directory
and add --confirm DELETE-ALL-TEST-DATA-NO-BACKUP.
The operation also refuses to start while either direct-upload signing secret is configured. Revoke the underlying R2 API token first; deleting only the Worker binding does not invalidate already signed URLs.
This tool targets only the isolated unicas-* resources committed in this
repository. It has no legacy Worker, route, database, bucket, or namespace
parameter.
Restore (disaster drill; destructive — clears target tables first):
wrangler d1 execute unicas-control --remote --command "<clear tables>"
wrangler d1 execute unicas-control --remote --file=unicas-control.sql
wrangler d1 execute unicas-tenant --remote --command "<clear tables>"
wrangler d1 execute unicas-tenant --remote --file=unicas-tenant.sql
The 2026-09-15 App/Space cutover verified both data-only exports by rebuilding the pre-cutover schemas in temporary SQLite databases, importing the exports, and checking the expected control/data counts. Every R2 backup was also rehashed against its canonical key. Production restore remains a destructive incident action and was not executed as part of the drill.
#App OAuth issuer key rotation
App signing keys come exclusively from the active issuer's discovered
jwks_uri; UniCAS has no manual or copied active-key table. Rotation happens
at the authorization server: it publishes the new key alongside the old one
(overlap), and UniCAS refreshes the remote JWKS after the authority cache TTL
and when an unknown kid is encountered outside the fetch cooldown.
Verification behavior:
- A key removed from a successfully refreshed JWKS stops validating immediately.
- Within the cache TTL the cached JWKS keeps verifying; past the hard stale
bound a failed registry refresh fails closed (
registry_unavailable) — it never silently falls back to a manual key set.
Operational checks:
- Confirm the App's OAuth issuer is
active(unicas app-oauth-issuer get <appId>). - After the provider rotates keys with overlap, confirm traffic still verifies with the new key after the refresh interval.
- On suspected compromise, rotate the provider signing key, then verify the
provider JWKS no longer lists the compromised
kidand old tokens fail after the cache TTL.
#Key compromise
- Rotate the authorization server's signing key so the compromised key stops being advertised (publish the replacement with overlap first).
- Wait for UniCAS to refresh the remote JWKS and confirm tokens signed by the
compromised
kidare rejected. - Rotate any secrets that may share the compromise (audit reader key,
session encryption keys) and review
cas_control_audit_eventsfor the affected window.
#Incident checklist
- Confirm service
/health; confirm/v1/cas+/admin/appsprobes. wrangler deployments listforunicas— recent deploy? Rollback first, diagnose later.- Query
cas_app_authorizationforfail_closed/unknown_issuer— registry reachability vs key/issuer config. - Check the service Worker's D1 bindings (
CAS_CONTROL_DB,unicas-control) andwrangler d1 execute ... SELECTreachability. - After resolution, run the smoke twice (70 s apart) to confirm both the happy path and the cache-refresh path.
#Pending ops items
- Scheduled backup job (Workers Cron or external) with alerting on failure.
- Throwaway-D1 restore drill (destructive restore not yet executed).
- Alert delivery integration (Cloudflare alerting webhooks or an external monitor like Better Stack / Grafana) wired to the rules above.
- Cloudflare analytics/logpush consumption for the 5-min roll-ups.