Skip to content
C.W.K.
Lesson 03 of 04 · published

The Door the CDN Must Ask

~14 min · fail-closed, export-policy, cloudflare-images, content-hash

Level 0Sketch
0 XP0/43 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete

The last door before the public internet

Everything so far guards the studio from the inside. The public site is a different boundary. Its images live on Cloudflare Images, and an uploader script on a Mac pushes them there when a site commit carries new pictures. A protected face must never make that trip, whatever the file is called and wherever in the repository it sits.

The architecture names the rule and its trap. Super Soul originals and anything derived from them cannot be uploaded to Cloudflare Images. The exporter revalidates current policy and recorded lineage immediately before upload. Hiding something in a UI is not enough, and an exporter must consume Avatars' decision rather than infer safety from a directory. The site's policy module opens with the reason, in two lines: Avatars is the lineage authority, and the upload boundary checks bytes, so a renamed protected avatar or a derived cover cannot slip past a folder deny list.

Two answers from Avatars

Studio.export_policy(id) answers for one asset. It resolves the asset's lineage, asks Pippa about every subject with no actor attached, and lists the Super ones as blocked_subjects. The answer is allowed only when that list is empty. Its comment adds the rule from Track 2: an archived copy of a protected image is still a protected image, so the archive flag is never read here.

The hash door answers for bytes. It finds every asset whose SHA-256 matches, runs export_policy on each, and folds the results: known is whether anything matched, allowed is whether every match is allowed, and the blocked subjects are the union. The same handler answers on two paths: /internal/export-policy for a local engine, and /api/export-policy behind the admin gates of the next lesson.

What "known" means

Follow the code when nothing matches. matches is an empty list, policies is empty, and Python's all() over an empty list is True. Bytes Avatars has never stored come back as known: false, allowed: true. The architecture's phrase that missing policy blocks public export can be read as "unknown bytes are blocked". The code does not do that, and it could not honestly do anything else: Avatars can refuse bytes it knows descend from a protected soul, and it cannot vouch for bytes it never saw. Most of the site's images are screenshots and diagrams that never passed through the studio.

What fails closed is the answer, not the file. The site's gate throws, and nothing is uploaded, in every case except a well-formed allow.

SituationDoor saysSite gate
Bytes with a protected lineageknown: true, allowed: falsethrows
Bytes Avatars stored, all publicknown: true, allowed: truepasses
Bytes Avatars never storedknown: false, allowed: truepasses
Door address cannot be resolvednothingthrows
No answer within 15 secondsnothingthrows
Any non-2xx statusa refusalthrows
allowed not exactly true, or known not a booleana malformed bodythrows

That last row is quiet but important. A body of {"allowed": true} with no known is refused, so a proxy or a stub that does not speak the contract cannot pass for the real door. The site's test feeds the gate a protected answer, that malformed answer, a 503 and a thrown network error, and requires all four to reject. The gate returns known to its caller, and today neither caller acts on it.

Ask before the bytes change

The check is keyed by the original bytes, so it has to run before anything alters them. putToCfImages calls the gate first, before it strips EXIF and GPS for publication and before it deletes or creates any public image. The asset sync does the same for an oversized file: it checks the original before the resize changes its content hash. Lesson 2 explains why the order matters. A stripped or resized copy no longer matches the original's hash, so unless those exact bytes were stored too, it is a stranger to Avatars, and a stranger answers allowed.

Fail closed, twice, for real

The gate shipped on 2026-09-29, and it has failed closed in production twice. Until a fix the next day, 2026-09-30, every new image upload came back 403. Avatars answers /internal/ only to a loopback caller with no x-forwarded-for, origin or sec-fetch-mode header, and Node's built-in fetch adds sec-fetch-mode: cors to every request. The fix was a plain node:http GET, with a test that pins the headers it does not send.

The second time came from the other direction. Avatars had moved to the Pippa machine in late October, and the gate still defaulted to its own Mac's loopback port, where nothing answered. Every site commit with a new image stopped. The fix, on 2026-10-25, asks the family registry where Avatars runs: its loopback when it runs here, otherwise an ssh tunnel to the serving Mac's loopback. The door stays loopback-only, and a new test fails if the door resolves to this Mac's own port while Avatars runs elsewhere.

Both outages were loud, and neither let a picture through. That is the whole trade of failing closed: a broken check looks like a stopped commit, never like a quiet pass.

Principle: An export gate passes on one thing only, a well-formed allow from the authority. Silence, refusal, a timeout or a body that does not match the contract all stop the upload.

Code

cwkAvatars, backend/studio.py: export_policy, one asset's verdict from its lineage (verbatim, lines 636-642)·python
    async def export_policy(self, id):
        # Policy reads never depend on the archive flag: an archived copy of a
        # protected image is still a protected image.
        asset = await self.io("asset_lineage", id)
        subjects = await self.subjects(asset["subjects"])
        blocked = [s["soul_pk"] for s in subjects if s["is_super"]]
        return {"asset_id": id, "allowed": not blocked, "blocked_subjects": blocked, "sha256": asset["sha256"], "checked_at": now()}
cwkAvatars, backend/app.py: the hash door, where all() over no matches is true (verbatim, lines 526-531)·python
    @app.get("/internal/export-policy")
    @app.get("/api/export-policy")
    async def hash_policy(sha256: str):
        matches = [a for a in await studio.io("list", "asset") if a["sha256"] == sha256]
        policies = [await studio.export_policy(a["id"]) for a in matches]
        return {"known": bool(matches), "allowed": all(p["allowed"] for p in policies), "blocked_subjects": sorted({s for p in policies for s in p["blocked_subjects"]})}
cwk-site, scripts/avatar-export-policy.mjs: the gate, which passes only a well-formed allow (verbatim, lines 59-75)·javascript
export async function assertAvatarExportAllowed(bytes, { fetcher = plainGet, policyUrl } = {}) {
  const sha256 = createHash('sha256').update(bytes).digest('hex');
  const url = new URL(policyUrl ?? await resolvePolicyUrl());
  url.searchParams.set('sha256', sha256);
  let response;
  try {
    response = await fetcher(url, { signal: AbortSignal.timeout(15000) });
  } catch (error) {
    throw new Error(`Avatars export policy at ${url.origin} did not answer; no image was uploaded: ${error.message}`);
  }
  if (!response.ok) throw new Error(`Avatars export policy at ${url.origin} refused the check (${response.status}); no image was uploaded.`);
  const policy = await response.json();
  if (policy.allowed !== true || typeof policy.known !== 'boolean') {
    throw new Error('Avatars blocked public export of this image or its reference lineage.');
  }
  return { sha256, known: policy.known };
}

External links

Exercise

Add an export gate to a pipeline that publishes files somewhere you cannot easily take them back from (a CDN, a public bucket, a package registry). Hash the original bytes, ask an authority about that hash, and allow the upload only on a well-formed allow. Then write one test per failure: an explicit deny, a missing field, a non-2xx status, a timeout and an unreachable address. Every one must stop the upload.
Hint
Write the success check as a positive match on the whole contract (the allow is exactly true and every required field has the right type), never as "not denied". Run the gate before any transform that changes the bytes, such as stripping metadata, resizing or recompressing.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.