Skip to content

Commit 18b236f

Browse files
committed
ops: harden Registry and website production HTTP
1 parent c79f5e4 commit 18b236f

9 files changed

Lines changed: 77 additions & 5 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,16 @@
5151
one-time seeded smoke identity and live objects were removed afterward, queue
5252
counts returned to zero, and a checksum-verified backup captured the migrated
5353
clean state. Production Compose now accepts explicit prebuilt API/verifier
54-
image references so shared hosts can deploy with `--no-build`.
54+
image references so shared hosts can deploy with `--no-build`. Harden the API
55+
and static Registry response boundary with HSTS, anti-framing, no-sniff,
56+
permissions policy, cross-domain-policy denial, and a deny-all CSP for JSON
57+
surfaces. Add a reproducible website production Compose/nginx contract with
58+
a read-only root filesystem, bounded temporary filesystems, health checks,
59+
log rotation, `no-new-privileges`, and matching browser security headers. A
60+
production recovery drill restores the post-`0002` dump into an isolated
61+
Postgres 17 container, extracts the object archive into an isolated volume,
62+
verifies both migrations and all seven core Registry tables, and removes the
63+
temporary restore resources afterward.
5564
- Close the 0.23 syntax-audit consistency gaps: canonical type declarations
5665
now use comma-terminated fields, syntax-combination gates cover canonical and
5766
comma-free compatibility input, checked example mirrors use named `U64_MAX`

‎crates/cellscript-tools/src/tooling_release.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ pub fn run(root: &Path) -> Result<()> {
387387
"website/package.json",
388388
&[
389389
r#""prepare:registry": "node scripts/generate-registry-data.mjs""#,
390-
r#""build": "npm run prepare:registry && astro check && astro build && npm run check:docs && npm run check:dist""#,
390+
r#""build": "npm run prepare:registry && astro check && astro build && npm run check:docs && npm run check:dist && npm run check:deploy""#,
391391
r#""check:docs": "node scripts/check-doc-links.mjs""#,
392392
r#""check:dist": "node scripts/check-dist-regressions.mjs""#,
393393
],

‎docs/releases/CELLSCRIPT_0_23_RELEASE_NOTES.md‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ work.
3030
| Registry operations | `api.registry.cellscript.dev` and `registry.cellscript.dev` run as an isolated self-hosted Postgres/Node/object-volume/read-only-nginx stack behind trusted TLS. |
3131
| Registry retry safety | Pre-admission failures release only the failed request's nonce and retry reservation; accepted metadata commits transactionally, and readiness covers the actual managed object prefixes. |
3232
| Registry verification | Publish transactionally queues a leased, bounded real-compiler verification job; verified evidence/status commit atomically before crash-safe static-index convergence, and default search stays hidden until the baseline passes. |
33+
| Production HTTP boundary | API/static JSON responses use HSTS, deny-all content policy, anti-framing, no-sniff, and restrictive browser permissions; the website ships a reproducible read-only nginx deployment with health checks and bounded logs/temp storage. |
3334
| Registry install policy | Explicit unverified/quarantined install acknowledgements persist per dependency, so lock refresh and subsequent builds retain the same auditable risk choice. |
3435
| Tooling | CLI, LSP, WASM, website bindings, examples, and package tooling use the same edition contract. |
3536
| Syntax audit | Canonical type fields use trailing commas, checked examples use named `u64` boundaries, and compatibility plus CKB-VM regressions cover both source and witness placement. |
@@ -316,6 +317,21 @@ four production containers remained healthy, and a checksum-verified backup
316317
captured the migrated, cleaned state. This proves the live worker boundary but
317318
still does not substitute for publisher-owned JoyID authorisation.
318319

320+
The final production hardening pass makes the website deployment itself a
321+
tracked artifact instead of server-local configuration. Its nginx container
322+
runs read-only with bounded writable tmpfs mounts, health checks, log rotation,
323+
and `no-new-privileges`; the website, API, and static Registry preserve HSTS,
324+
anti-framing, no-sniff, cross-domain-policy, referrer, and permissions headers
325+
through the shared TLS proxy. JSON-only Registry responses additionally carry a
326+
deny-all content security policy.
327+
328+
The post-migration backup is also restore-tested, not only checksum-tested. An
329+
isolated Postgres 17 container restored both numbered migrations and all seven
330+
core Registry tables, while an isolated object volume accepted the complete
331+
archive. Neither restore target shared the production database, object volume,
332+
network endpoint, or lifecycle; both temporary targets were removed after the
333+
drill.
334+
319335
These endpoints prove the deployed service boundary, not a publisher-owned
320336
JoyID signature or first-package install. That interactive positive flow
321337
remains the explicit adoption checkpoint.

‎roadmap/CELLSCRIPT_0_23_ROADMAP.md‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,8 @@ Production-readiness evidence currently proves:
251251
served rows/objects were removed afterward;
252252
- live health/readiness checks cover Postgres, the object volume, runtime, and
253253
admin configuration;
254+
- the website has a tracked read-only nginx/Compose deployment, and all three
255+
public surfaces preserve their intended security headers through TLS;
254256
- the proxy admits a 2 MiB body to application validation and the Node adapter
255257
rejects 7 MiB + 1 byte with a structured 413;
256258
- unauthorised admin writes, invalid public queries, static POSTs, and traversal
@@ -260,8 +262,9 @@ Production-readiness evidence currently proves:
260262
object-hash drift, or source-tree drift;
261263
- API restart recovery preserves the database, audit log, and object volumes;
262264
- the daily systemd backup produces checksum-verified Postgres and object-store
263-
archives, both archive formats pass non-destructive restore inspection, and
264-
a post-`0002` backup captured the migrated, cleaned production state;
265+
archives, and a post-`0002` backup captured the migrated, cleaned production
266+
state; an isolated Postgres 17/object-volume drill restored both migrations,
267+
all seven core tables, and the complete object archive;
265268
- the website serves the live Registry and contains no Coming Soon surface.
266269
- a cryptographically valid WebAuthn-shaped P-256 fixture completes capability
267270
registration, explicit namespace claim, signed publish, idempotent replay,

‎services/registry-api/README.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ https://registry.cellscript.dev/health
106106
- Token-gated audit-event read path for review, incident response, and
107107
production debugging.
108108
- Token-gated verification queue metrics and audited dead-letter requeue.
109+
- Consistent API and static-object response hardening: HSTS, anti-framing,
110+
no-sniff, no-referrer, restrictive browser permissions, and a deny-all CSP
111+
for JSON-only surfaces.
109112
- Audit/event log records for capability, namespace, auth failure, rate-limit,
110113
and publish transitions, including admin review/quarantine/yank overrides.
111114
- Successful capability use updates `last_used_at` and writes a
@@ -173,6 +176,11 @@ filesystems, health checks, log rotation, and `no-new-privileges`. Production
173176
API readiness requires a fresh verifier heartbeat, so a missing or wedged
174177
consumer cannot present the write path as ready.
175178

179+
The API and read-only static service emit the same conservative transport and
180+
browser security boundary on success and error responses. The JSON-only CSP
181+
allows no executable or embedded content; CORS remains explicit for public
182+
CLI/browser reads and signed write requests.
183+
176184
Production validation performed at deployment includes trusted TLS for both
177185
domains, dependency-aware readiness, a 2 MiB request reaching application JSON
178186
validation, a structured application 413 at 7 MiB + 1 byte, rejection of
@@ -233,6 +241,13 @@ systemctl enable --now cellscript-registry-backup.timer
233241
systemctl start cellscript-registry-backup.service
234242
```
235243

244+
The 2026-08-01 production recovery drill restored the post-`0002` custom dump
245+
into an isolated Postgres 17 container and extracted the object archive into an
246+
isolated Docker volume. Both migrations and all seven core Registry tables were
247+
present; the disposable container and volume were removed after verification.
248+
This exercise did not attach to or mutate the production database/object
249+
volume.
250+
236251
Verify a backup before treating it as recoverable:
237252

238253
```bash

‎services/registry-api/deploy/registry-static.nginx.conf‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ server {
66
server_tokens off;
77
charset utf-8;
88

9+
add_header Content-Security-Policy "default-src 'none'; base-uri 'none'; frame-ancestors 'none'" always;
10+
add_header Permissions-Policy "camera=(), geolocation=(), microphone=()" always;
11+
add_header Referrer-Policy "no-referrer" always;
12+
add_header Strict-Transport-Security "max-age=31536000" always;
13+
add_header X-Content-Type-Options "nosniff" always;
14+
add_header X-Frame-Options "DENY" always;
15+
add_header X-Permitted-Cross-Domain-Policies "none" always;
16+
917
location = /health {
1018
default_type application/json;
1119
return 200 '{"status":"ok"}\n';
@@ -17,8 +25,13 @@ server {
1725
default_type application/json;
1826
add_header Access-Control-Allow-Origin "*" always;
1927
add_header Cache-Control "public, max-age=60, stale-while-revalidate=300" always;
28+
add_header Content-Security-Policy "default-src 'none'; base-uri 'none'; frame-ancestors 'none'" always;
29+
add_header Permissions-Policy "camera=(), geolocation=(), microphone=()" always;
2030
add_header Referrer-Policy "no-referrer" always;
31+
add_header Strict-Transport-Security "max-age=31536000" always;
2132
add_header X-Content-Type-Options "nosniff" always;
33+
add_header X-Frame-Options "DENY" always;
34+
add_header X-Permitted-Cross-Domain-Policies "none" always;
2235
}
2336

2437
location /source-snapshots/ {
@@ -32,8 +45,13 @@ server {
3245
}
3346
add_header Access-Control-Allow-Origin "*" always;
3447
add_header Cache-Control "public, max-age=31536000, immutable" always;
48+
add_header Content-Security-Policy "default-src 'none'; base-uri 'none'; frame-ancestors 'none'" always;
49+
add_header Permissions-Policy "camera=(), geolocation=(), microphone=()" always;
3550
add_header Referrer-Policy "no-referrer" always;
51+
add_header Strict-Transport-Security "max-age=31536000" always;
3652
add_header X-Content-Type-Options "nosniff" always;
53+
add_header X-Frame-Options "DENY" always;
54+
add_header X-Permitted-Cross-Domain-Policies "none" always;
3755
}
3856

3957
location / {

‎services/registry-api/src/index.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1798,8 +1798,13 @@ function corsHeaders(requestId: string): Headers {
17981798
"access-control-allow-headers": "content-type,authorization,idempotency-key,x-registry-admin-token,x-registry-admin-actor",
17991799
"access-control-expose-headers": "x-request-id,x-idempotency-status",
18001800
"cache-control": "no-store",
1801+
"content-security-policy": "default-src 'none'; base-uri 'none'; frame-ancestors 'none'",
1802+
"permissions-policy": "camera=(), geolocation=(), microphone=()",
18011803
"referrer-policy": "no-referrer",
1804+
"strict-transport-security": "max-age=31536000",
18021805
"x-content-type-options": "nosniff",
1806+
"x-frame-options": "DENY",
1807+
"x-permitted-cross-domain-policies": "none",
18031808
"x-request-id": requestId,
18041809
});
18051810
}

‎services/registry-api/test/registry-api.test.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,12 @@ describe("registry api", () => {
211211
});
212212
const ready = await get(readyApp, "/ready", { REGISTRY_ADMIN_TOKEN: "secret" });
213213
expect(ready.status).toBe(200);
214+
expect(ready.headers.get("content-security-policy"))
215+
.toBe("default-src 'none'; base-uri 'none'; frame-ancestors 'none'");
216+
expect(ready.headers.get("permissions-policy")).toBe("camera=(), geolocation=(), microphone=()");
217+
expect(ready.headers.get("strict-transport-security")).toBe("max-age=31536000");
218+
expect(ready.headers.get("x-frame-options")).toBe("DENY");
219+
expect(ready.headers.get("x-permitted-cross-domain-policies")).toBe("none");
214220
expect(await ready.json()).toMatchObject({
215221
status: "ready",
216222
checks: {

0 commit comments

Comments
 (0)