Trust · Policies

DOC-24

Secure development and operations

The rules under which code is written, checked, released and run — including the two places where this service is weaker than the rules would suggest.

Record for
  • 27001 A.5.8
  • 27001 A.5.37
  • 27001 A.8.7
  • 27001 A.8.24
  • 27001 A.8.25
  • 27001 A.8.28
  • 27001 A.8.29
  • 27001 A.8.32

Review. Reviewed whenever the gate changes, whenever a new class of user input is accepted, and whenever the cryptography in use changes. Reviewed in any case at least once every 90 days, and immediately if a dependency scanning capability is introduced, since that is the weakness this document is currently recording.

Scope

This procedure covers everything from a change being proposed to it running in production: how code is written, what must pass before a release, how cryptography is used, how the deployment refuses to proceed, and what is monitored afterwards. It applies to the Worker, both durable classes and the game engine submodule alike, because they ship as one artefact.

The change processes themselves — classification, the change record, authorisation, rollback — are set out on the conformance register and are not repeated here. This document is the security half of the same life cycle.

Secure coding rules

The whole codebase is TypeScript under strict checking, and the type check must pass with no errors before a release. This is a security rule rather than a tidiness one: most of the input-handling faults this service could have are shapes the checker rejects.

Output escaping goes through one helper per module and never inline. Having a single escape function means a review question — is this interpolation escaped — has one place to look rather than several hundred.

User input is validated against a fixed pattern, not sanitised into shape and trusted. The visitor identifier must match its expected form or a fresh one is minted rather than the supplied value being repaired. A reason code must be exactly one letter followed by three digits, checked in the browser and checked again on the server, and the fallback used when a code is unknown is itself validated against the same pattern. Display names go through the single name policy described in the risk treatment plan.

Exports are treated as an injection surface in their own right. A field in the text log that begins with an equals, plus, minus or at sign is prefixed with an apostrophe before it is written, because a spreadsheet opening that file would otherwise treat a player-supplied name as a formula; fields containing a quote or a comma are quoted and their quotes doubled.

Comparisons of secrets are constant-time. Authorisation fails closed in every branch — no configured token denies, a non-loopback request that is not over TLS denies, an unrecognised authorisation scheme denies — and there is no branch that grants by falling through.

Content security policy

Server-rendered pages are served with a policy whose script-src is a per-response nonce and nothing else: no unsafe-inline, no host allowance, no scheme allowance. Under CSP level 3 the presence of a nonce makes every unmarked inline script inert, which is the property being bought — injected markup cannot guess a nonce it has never seen.

The nonce is 128 bits from the platform's cryptographic random source, minted fresh for each response. Every inline script this service emits is written with a literal placeholder in place of the nonce value, and exactly one function swaps that placeholder for the real value while setting the header. A script that was not written through that path does not get a nonce, and a placeholder must never survive into a response — both are checked before release.

The policy for the client bundle is deliberately different and the difference is worth recording, because it looks like a weakening and is not. It carries both self and a nonce. Self covers the application's own modules; the nonce exists so the platform's edge rewriter, which runs downstream of this Worker and injects an analytics tag, has a nonce to copy. Without one it injects an unnonced inline script and the console fills with policy violations. This is also why no external host is named in that policy: the nonce authorises the edge's own injection without widening the policy for anybody else.

Alongside it: strict transport security for a year including subdomains, no content-type sniffing, framing denied both by header and by frame-ancestors, base-uri and object-src set to none, and form-action confined to self.

Response headers, and where they are applied

One header table is applied to every response this Worker emits — transport security, no content-type sniffing, a referrer policy, frame denial and a permissions policy that switches off camera, microphone and geolocation. It is applied in one place, on the way out, rather than per route.

That is a correction. The permissions policy used to be set on the static asset branch only, so the game carried it and the eight server-rendered pages — the ones that exist to demonstrate these controls — did not. A header set on one branch of a router is not a control, and it was found by reading the served response rather than the source.

Limits on the unauthenticated writes

Two routes accept a write from anyone: the public action log and the player profile. Both are throttled on the connection the edge stamps, never on anything the caller supplies — the profile identity is a cookie the first read hands out, so throttling on it would mean discarding the cookie bought a fresh allowance. Each also sits under a ceiling shared by every public caller at once, so a distributed flood is bounded even when no single connection is.

The profile write was the gap. A row ceiling governed how many profiles could be created and nothing at all governed how often an existing one could be overwritten, although an overwrite costs the same durable write as a creation. Both branches are metered now.

Body size is capped at 16 KiB and the cap is counted on the bytes that actually arrive. It was previously read from Content-Length, which a chunked request does not send, so the check passed on a missing header rather than failing closed. The stream is now cut the moment it passes the ceiling.

Rules for the use of cryptography

There are four uses and no others. Transport: everything runs over TLS terminated by the platform, with strict transport security asserted for a year including subdomains, and the operations routes refusing plaintext outright anywhere but loopback because a basic credential is reversible base64.

Integrity: each control receipt is hashed with SHA-256 over a canonical serialisation with a fixed field order and an explicit version marker, chained to its predecessor's hash, with the head anchored under a separate key written in the same operation as the row it describes. One function computes that hash and both the writer and the verifier call it — two implementations that drifted by a single field would make verification fail on honest data and look exactly like a real tamper alarm.

Randomness: the content-policy nonce and the visitor identifier both come from the platform's cryptographic random source. Nothing security-relevant uses the simulation's seeded generator, and nothing in the simulation uses the cryptographic one; the two are kept apart deliberately, since the simulation's determinism depends on its generator being reproducible and that is the opposite of what a nonce needs.

Comparison: credential comparison is constant-time over the raw bytes.

Key management, in full. There is one long-lived secret, the operations token, held as a platform secret and never written into tracked configuration. Rotation is performed by putting a new value and redeploying; it is on demand rather than scheduled, and the risk treatment plan records that as accepted residual risk. There is no key hierarchy, no key this service generates and stores, no certificate this service manages — the platform terminates TLS — and no encryption at rest configured by this service beyond what the platform applies to its own storage. The shortness of this list is the control: there is nothing here to mismanage.

The gate before production

Four checks must pass, in order, and a failure of any of them stops the release. The client build must succeed. The type check must pass with no errors across the Worker, the engine and the client. The working tree must be free of whitespace damage, and the submodule's tree checked separately, because a check run in the parent repository says nothing about the engine.

Then the service is run against the same runtime production uses — the local runtime is the same engine, not an emulation of it — and the release is exercised rather than assumed. Every route that must answer does answer, repeatedly rather than once, since a route that works on the first request and fails on the fifteenth is the failure mode that matters. Requests carry a cache-buster: a cached response has produced a false result here before and cost real time.

Four contract checks run at the same time, each of which has been broken by a plausible change in the past. Every inline script carries its response nonce and no placeholder survives. The operator console answers unauthorised rather than serving. The receipt chain's published verdict reads verified. Every evidence link on the register resolves — public routes 200, operator routes 401 — because a dead evidence link is a finding in its own right rather than a broken link.

One thing this gate is honest about: it is a checklist a person performs, not a pipeline that enforces it. There is no automated build, and the checks above run because they are followed rather than because a machine refuses without them.

Deployment

Production deploys through one script and there is no second path. It refuses to run at all if the account identifier is absent, and it says where the value belongs — an untracked environment file, never the tracked configuration, because an account identifier does not belong in a file under version control. It then builds, and refuses again if either operations secret is missing from the deployed environment, so a release cannot produce a running service whose control panel has no valid credential.

The deployment is the whole bundle at a version. There is no in-place editing of a running service, no partial upload and no package installation at runtime. Rollback is a redeploy of an earlier version through the same script.

After release the running version identifier is readable from the operator status route, which is what ties a running service back to the change entry that produced it.

Technical vulnerabilities, stated as they are

How vulnerability information arrives: through the public report intake, which is unauthenticated and open to anyone, and through the operator noticing. That is the complete list, and it is a passive one.

How exposure is evaluated and acted on: a report is recorded as a retained event and a receipt on arrival; the affected code path is read; where the report is about behaviour, the tick or the route is exercised rather than reasoned about; and the fix ships as a change entry naming the finding it closes with its closure time published. Every finding accepted to date has been closed this way, and the closure times are on the public record rather than described here.

What is missing, recorded rather than dressed: nothing scans the dependencies of this service for known vulnerabilities. There is no advisory feed subscribed to, no scheduled scan, and no automated build in which such a scan could run. The mitigations are real but partial — the dependency surface is small and pinned by a lockfile, the build is reproducible from a clean checkout, and the Worker imports only the engine, store and protocol entry points of the game module so browser libraries cannot enter the server bundle at all — and none of them tells anyone that a pinned dependency has had an advisory published against it. This is carried on the risk treatment plan as an open risk, and the conformance row for technical vulnerabilities stays partial because of it.

Malware, and why the surface is the control

There is no anti-malware product here, and installing one would be theatre. The control is that there is no vector for a file to arrive and no mechanism to execute one.

Every route this service serves is enumerated in the API reference. None of them accepts a file, a multipart body or a form upload; there is no code path anywhere in the Worker that reads a multipart body or form data at all. The only content a member of the public can store is a display name of at most sixteen code points, passed through the name policy, and a skin identifier checked against a fixed pattern.

The runtime executes only the deployed bundle. There is no interactive shell, no filesystem the service writes executables to, and no runtime package installation. Content served to a browser is constrained by a content security policy that permits no external script origin and makes unmarked inline script inert.

The awareness half of this control has no meaning at one person and no endpoint in scope, and it is not claimed. The endpoint the operator works from is outside the boundary of this management system by the scope statement.

Access to the source

The source is a private repository with one hosted remote and one account holding write access, plus the operator's working copy. The engine is a pinned submodule of it.

What this service can demonstrate: the source is not reachable from any route it serves, no secret is present in any served response, and the deploy path requires a credential the repository does not contain — the account identifier comes from an untracked environment file and the operations secrets are platform secrets, both deliberately absent from tracked configuration.

What it cannot demonstrate, and does not claim: who holds write access to the repository, and that anyone has reviewed that list. Access there is controlled by the hosting account, which is outside the boundary of this management system, and there is no route this service could serve that would prove anything about it. The conformance row stays partial for that reason rather than being marked met on an assertion.

Security in project management

There is no separate security workstream. Every change is classified before it is built, and the classification includes the security consequence: whether it touches authentication, public input, retention or spend. That classification then decides how much of the gate above applies.

The classification step also asks whether the change alters a treatment already recorded against a risk. Where it does, the risk treatment plan is reissued in the same deployment as the change, so the assessment cannot quietly fall behind the service it describes.

Where the operating procedures live

Route-level operation: the API reference documents every route this service serves, with its authorisation, its required headers and its effect, and the machine-readable version of the same. Every functionally distinct route appears there — the pages, the public writes and the operator routes alike. Writing this section is what turned up the one that did not: the unauthenticated route that records a public gameplay event was operating undocumented, and it is documented now.

What deliberately does not get its own entry, so that the claim above is exact rather than approximate: trailing-slash forms of a documented path; three earlier path names that reach a documented handler unchanged, noted on the entries they reach where the service's own copy rules allow the old name to be printed; a redirect used by the interface; and the earlier operator paths kept working under the register's old prefix, which are the same handlers behind the same authentication. There are also two proxy routes to a second backend that is not configured in this deployment and that answer unavailable in consequence; they are described here rather than documented as capabilities the service does not currently have.

Development and release: this document.

Change control: the change processes published on the conformance register, each with its trigger, its steps and the record it produces.

Incident handling: the incident record and the report intake, each of which is an operation with its own route and its own record.

They are published rather than filed, so availability to whoever needs them is a property of the service being up rather than a claim about a document store.