# Publishing Without a Platform
**A zero-database static index on Cloudflare Workers and R2**
Point Arena Cyber Security
Version 1.0
---
## Summary
This paper documents the architecture running at
`open.pointarenacybersecurity.dev`, the index you are probably reading this
from. It is roughly 400 lines of JavaScript and an object storage bucket. There
is no database, no authentication, no write path reachable from the internet,
and no user input that reaches a parser.
We are publishing it because the architecture is the argument. A small
organization that needs to distribute files publicly does not need a platform,
a CMS, or a managed service. It needs about an afternoon.
The paper covers the threat model, what the design removes, what risk remains
after the removals, and how to reproduce it. It includes a vulnerability we
found in our own implementation while writing the threat model, because the
process of finding it is more instructive than the fix.
---
## 1. Why this exists
The tooling this organization builds on has always been free. Nmap, Suricata,
Wazuh, nuclei, MISP. What is not free is the knowledge of how to run them well.
That sits behind certification costs, training paywalls, consulting retainers,
and vendor-managed platforms.
A small nonprofit could technically run a SIEM for nothing and never will,
because nobody has written down the path from zero to working in language they
can follow. That gap is the paywall.
Publishing is therefore the primary activity, not a side effect of it. Which
makes the publishing mechanism itself worth getting right, and worth explaining.
---
## 2. Threat model
Most security architecture writing starts from confidentiality and works
outward. That is the wrong starting point here, and getting the starting point
right is most of the design.
**Everything this system serves is intended to be public.** There is no secret
to protect. Confidentiality is not a property we are defending, it is a
property we have deliberately given up.
What remains:
| Property | Defending? | Why |
|---|---|---|
| Confidentiality | No | All content is published deliberately |
| Integrity | **Yes, primary** | Readers run these scripts as root |
| Availability | Yes, secondary | AGPL section 13 obligates source availability |
| Attribution | Yes | License terms depend on provenance being clear |
Integrity is the one that matters. We publish hardening scripts and detection
content. Somebody is going to `curl | sudo bash` one of these against their
advice and ours. If an attacker can alter what we serve, they get root on a
nonprofit's infrastructure with our name on the payload.
Availability matters for a less obvious reason. This work is AGPL-3.0-or-later.
Section 13 requires that anyone running a modified version over a network offer
the corresponding source to that service's users. We run a modified version over
a network. The source has to stay reachable for as long as the service does, so
availability is a license obligation, not just an operational preference.
### Adversaries considered
1. **Opportunistic scanners.** Automated, high volume, looking for known CMS
and framework vulnerabilities. The overwhelming majority of traffic.
2. **A targeted attacker seeking content substitution.** Wants to alter a
published script so downstream readers execute attacker-controlled code.
3. **A resource-exhaustion attacker.** Wants to drive up cost or take the site
down, with no interest in the content.
4. **The platform itself.** Cloudflare can read and alter anything here. This is
accepted, and section 7 explains why and what it costs.
---
## 3. Architecture
```
reader
|
| HTTPS
v
Cloudflare edge (TLS, DDoS, cache)
|
v
Worker (~400 lines, no state)
|
| binding, not a network call
v
R2 bucket (private, no public URL)
```
The Worker does four things:
- **Listing.** Calls `list()` on the bucket, groups keys by prefix, renders a
directory table. Cached 60 seconds in the isolate.
- **View.** Fetches one object, computes its SHA-256, renders it as escaped
text with its metadata.
- **Raw.** Streams one object through with a corrected content type.
- **Digests.** Generates `SHA256SUMS` across the whole bucket on demand.
There is no build step and no deployment pipeline for content. Publishing a
file is uploading it to the bucket. The listing is derived from the bucket at
request time, so the index cannot drift from what is actually being served.
This is a deliberate choice over a generated manifest, which can and does go
stale.
License assignment is by path prefix. Anything under `papers/` is CC BY-SA 4.0,
everything else is AGPL-3.0-or-later. Encoding policy in structure rather than
metadata means it cannot be forgotten on a per-file basis.
---
## 4. What is absent
The security properties here come almost entirely from what is not present.
**No database.** There is nothing to inject into. The entire class of SQL and
NoSQL injection is not mitigated, it is inapplicable.
**No authentication.** No login form, no session handling, no password storage,
no reset flow, no token issuance. Credential stuffing, session fixation, and
privilege escalation have nothing to attach to. This is a direct consequence of
the threat model: because we gave up confidentiality, we do not need access
control, and access control is where a large share of web vulnerabilities live.
**No write path on the internet.** Uploads happen through the storage provider's
own console, authenticated separately. No endpoint in the Worker accepts a
`POST`. Non-`GET` methods return 405 before any other logic runs.
**No parsing of untrusted input.** The request path is used for two things: a
literal `Map` lookup against keys already read from the bucket, and a string
prefix comparison for directory grouping. No template engine, no deserializer,
no markdown renderer, no YAML loader. File contents are HTML-escaped and placed
inside `<pre>`. A published file containing a `<script>` tag renders as text.
**No dependencies.** No npm packages. The dependency tree is the JavaScript
runtime. There is no transitive supply chain to audit and nothing to patch on
someone else's schedule.
**No server.** No OS to keep patched, no SSH daemon, no reverse proxy config, no
TLS certificate renewal, no log rotation, no fail2ban. These are not solved
problems in this design, they are absent problems.
---
## 5. What remains
An honest threat model has a residual risk section. Here is ours.
### 5.1 A cache-key amplification bug we shipped
While writing section 2 we found a real vulnerability in our own code.
The `SHA256SUMS` endpoint reads every object in the bucket and hashes it. That
is expensive and bounded by bucket size, so the response is cached at the edge.
The original implementation cached it like this:
```js
const hit = await caches.default.match(request);
```
The Cache API keys on the **full URL, including the query string.** So
`/SHA256SUMS?x=1` and `/SHA256SUMS?x=2` are different cache entries. An
unauthenticated caller looping over query strings misses the cache on every
request, and each miss re-reads and re-hashes the entire bucket.
That is unbounded CPU consumption and unbounded Class B storage operations,
triggered by anyone, with no authentication, from a single line of shell. On a
bucket with four small files it is nothing. On a bucket with two hundred papers
it is a bill and an outage.
The fix is to cache against a fixed key rather than the incoming request:
```js
const ck = new Request(url.origin + '/SHA256SUMS');
const hit = await caches.default.match(ck);
```
Two observations worth more than the bug itself.
First, this survived review precisely because it looked idiomatic. Passing
`request` to `caches.match()` is the pattern in most documentation and most
example code. Correct-looking code that is wrong in one specific context is
harder to catch than obviously bad code.
Second, it was found by writing the threat model, not by scanning. No static
analyser flags this. It requires asking "what does an attacker control, and
what does that cost me," which is a question a human asks and a tool does not.
### 5.2 Platform compromise
Whoever controls the Cloudflare account controls everything served here. That
is the single highest-impact risk in the design and it is not mitigated by
architecture at all.
Mitigations are account-level, not code-level: hardware-key multi-factor
authentication on the account, API tokens scoped to the minimum required,
periodic audit-log review, and no shared credentials.
### 5.3 No transparency over time
A reader can verify that the file they downloaded matches the digest published
alongside it. They cannot verify that the digest published today matches the
digest published last month. If an attacker with account access alters both the
file and the digest, verification passes and the substitution is silent.
Publishing the digests here proves internal consistency, not history. Closing
this gap requires signing the digest file with a key held outside the platform
and publishing the key fingerprint somewhere the platform does not control. We
have not done this. It is the most significant known weakness in the current
design.
### 5.4 Single point of failure
Cloudflare outage means both the content and the section 13 source availability
go down together. Mirroring elsewhere would address it and adds a second system
to keep synchronized. We have accepted the risk for now.
### 5.5 Availability of the listing
The listing depends on a successful `list()` call. A storage-side failure is
caught and returns a service-unavailable page rather than an unhandled
exception, which was itself a fix after an early deploy returned an opaque
platform error when the storage binding was missing.
Failing usefully is a security property. An error page that says which binding
is missing costs an attacker nothing and saves an operator an hour.
---
## 6. Integrity in practice
Digests are generated by reading the objects, not from a file maintained by
hand or by a build step. A stale checksum file is worse than none, because it
converts a verification step into a false assurance.
For a reader:
```
curl -sO https://open.pointarenacybersecurity.dev/SHA256SUMS
sha256sum -c SHA256SUMS
```
Each raw response also carries the file's license as a header, so an automated
consumer can check licensing without a second request or an HTML parse.
The limitation in section 5.3 applies. This proves the bytes you received match
the bytes we are serving. It does not prove the bytes we are serving are the
bytes we served last month.
---
## 7. On depending on a platform
There is an obvious tension in an anti-gatekeeping organization running on
commercial infrastructure owned by a large company.
We think the tension is real and the tradeoff is still correct, for a specific
reason: **the lock-in is negligible.** The content is files in a bucket,
accessible over a standard S3-compatible API. The Worker is a few hundred lines
of dependency-free JavaScript against a small, well-documented interface.
Moving this entire system to another provider, or to a plain static file server
on a VPS, is an afternoon. Nothing about the content, its structure, or its
licensing is encoded in a proprietary format. There is no export process,
because there is nothing to export.
That is the property to optimize for when accepting a dependency. Not whether
you depend on someone, but how expensive it is to stop.
---
## 8. Cost
At the scale of a small nonprofit publishing text files, this runs within the
free tier of both the compute and storage products. Storage cost scales with
bytes stored, and read operations are billed per request with a substantial
free allowance.
The relevant point is not the exact figure, which changes. It is that a
publishing platform for a security nonprofit has an ongoing cost somewhere
between zero and the price of a coffee, and the alternative most organizations
reach for is a managed CMS with a monthly seat price.
---
## 9. Reproducing this
The complete source is published at
`open.pointarenacybersecurity.dev/view/pacs-open-worker.js` under
AGPL-3.0-or-later. Setup instructions are in the comment header.
The short version:
1. Create a private object storage bucket. Do not enable a public URL.
2. Create a Worker from the default template and replace its code.
3. Bind the bucket to the Worker as `FILES`.
4. Attach a custom domain.
5. Upload files. Prefixes become directories.
Total configuration is one binding and one hostname. Total maintenance is zero,
because there is nothing running that can fall behind on patches.
If you modify it and run it for others, section 13 requires you to offer your
users the corresponding source. That is the point. Improvements should come
back.
---
## 10. What this is not
Stating limitations plainly is part of publishing honestly.
**Not a git host.** No version history, no branches, no blame. If provenance
over time matters for your use case, you need something else.
**No contribution flow.** No pull requests, no issue tracker. Patches arrive by
email. That is a deliberate tradeoff for simplicity, and it will not scale past
a handful of contributors.
**No discovery.** Nobody stumbles onto this. A self-hosted index has no search
ranking, no recommendation surface, no social graph. Reach must be built
separately, and for a publishing-first organization that is a genuine cost, not
a footnote.
**Not suitable for anything private.** There is no access control by design. If
you need some files restricted, this architecture does not extend to that. Add
authentication and you reintroduce most of what section 4 removed.
---
## 11. Conclusion
The security of this system is not the result of controls added. It is the
result of a threat model that permitted removing almost everything, followed by
actually removing it.
Most systems cannot do this. Most systems have real confidentiality
requirements, real write paths, real users with real permissions. But more
systems could do it than currently do, and the reason they do not is usually
that nobody questioned whether the platform was necessary before adopting one.
The one vulnerability we found in our own implementation was found by writing
the threat model down. That is a cheap technique and an underused one.
---
## Licensing
Copyright (C) 2026 Point Arena Cyber Security.
This work is licensed under the Creative Commons Attribution-ShareAlike 4.0
International License. To view a copy of this license, visit
https://creativecommons.org/licenses/by-sa/4.0/
SPDX-License-Identifier: CC-BY-SA-4.0
The software described is licensed separately under AGPL-3.0-or-later.
Corrections and patches: hello@pointarenacybersecurity.dev
License. Code and detection content is AGPL-3.0-or-later. If you modify it and run it for others over a network, including as a hosted or managed service, section 13 requires you to offer those users your source. Papers and written guidance are CC BY-SA 4.0.
Verify what you got. curl -sO https://open.pointarenacybersecurity.dev/SHA256SUMS && sha256sum -c SHA256SUMS
Source of this site. This index is itself AGPL. Read it here.
Patches and questions. hello@pointarenacybersecurity.dev