The threat model
There are no accounts here, no database and no payments, so the risks are narrower than an app’s. They still exist:
- Script injection. If anything ever slips untrusted markup or a script into a page, it runs with this origin’s authority.
- Supply chain. Every third-party script, font or CDN is code or content someone else can change.
- Clickjacking and framing. Another site loads this one in an invisible frame and borrows its clicks.
- Downgrade and interception. A network attacker strips HTTPS on a first visit.
- Form abuse. The contact form gets used for spam, header injection, or to relay mail to people who never asked for it.
- Leakage. Visitors’ browsing data leaks to third parties through referrers, analytics or embedded content.
Each section below maps to one or more of these.
Deny by default: the CSP
The Content-Security-Policy starts from default-src 'none', which blocks every kind of resource, then allows back exactly what the site uses, all from its own origin:
default-src 'none';
script-src 'self';
style-src 'self';
img-src 'self';
font-src 'self';
connect-src 'self';
manifest-src 'self';
form-action 'self';
base-uri 'none';
frame-ancestors 'none';
upgrade-insecure-requests
There is no 'unsafe-inline' and no 'unsafe-eval' anywhere. That has consequences for how the site is written:
- Every script is a file. The one-line script that marks the page as scripted before it paints lives in
/boot.jsinstead of an inline tag. - No
style=""attributes in the markup. Animation delays that would usually sit in inline styles are set with:nth-child()selectors instead, and runtime values (like the card highlight that follows the pointer) are set through the CSSOM, which CSP allows. form-action 'self'means the contact form can only ever post back to this origin, even if the markup were tampered with.base-uri 'none'stops an injected<base>tag from quietly re-pointing every relative URL.
The rest of the header set
| Header | Value | Stops |
|---|---|---|
| Strict-Transport-Security | max-age=63072000; includeSubDomains | Plain-HTTP downgrades for two years after the first visit, on every subdomain. |
| X-Content-Type-Options | nosniff | Browsers guessing a file is a script when it was served as something else. |
| X-Frame-Options | DENY | Framing in older browsers; frame-ancestors 'none' covers current ones. |
| Referrer-Policy | no-referrer | Leaking which page a visitor came from when they follow a link out. |
| Permissions-Policy | camera=(), microphone=(), geolocation=(), … | The page, or anything injected into it, asking for powerful device features. Every powerful feature the site doesn’t use is switched off. |
| Cross-Origin-Opener-Policy | same-origin | Other windows keeping a handle on this one (tabnabbing, cross-window leaks). |
| Cross-Origin-Resource-Policy | same-origin | Other sites embedding this site’s files. |
| Cross-Origin-Embedder-Policy | require-corp | Loading any cross-origin resource that hasn’t opted in. Together with COOP it makes the page cross-origin isolated. |
The headers live in a single _headers file that Cloudflare applies to every static response. The Worker that handles the form sets the same set in code, so its JSON replies and redirects are covered too.
Zero third parties
The page makes no requests to any other origin. Concretely:
- Fonts are self-hosted (Geist, Geist Mono and Barlow Condensed, all under the SIL Open Font License), subset to Latin and served as WOFF2 with a one-year immutable cache.
- No analytics, tag managers or embeds. Nothing here measures you.
- No CDN-hosted libraries. The background glow is a small WebGL shader shipped with the site, the vault door is inline SVG, and icons are a local SVG sprite.
- No cookies are set by the site. (Cloudflare, which hosts it, can set a strictly necessary bot-management cookie if that protection is switched on.)
The contact channel
Forms are where small sites get abused. The form posts to a Cloudflare Worker at /api/contact, which:
- Rejects cross-site posts by checking
OriginandSec-Fetch-Site, and accepts only form-encoded bodies. - Caps the body at 16 KB while reading it, so a huge upload is cut off instead of buffered.
- Rate-limits per IP address with the Workers Rate Limiting binding.
- Drops bots quietly: a hidden honeypot field, plus a timestamp that flags forms submitted faster than a person can type. Bots get a normal-looking success, so there is nothing to tune against.
- Validates every field against the same limits the form shows (name 100, email 254, message 4,000 characters) and says plainly which one failed.
- Forces header-bound fields onto one line, stripping CR and LF so nobody can inject extra email headers, and encodes non-ASCII names per RFC 2047.
- Never emails the address you typed. There is no auto-reply, so the form can’t be used to send mail to a victim. Your address only appears as Reply-To on the copy sent to the lab.
- Stores nothing and logs only a random message ID and the topic: no names, addresses or IPs.
- Fails as JSON, never as a stack trace. Without JavaScript the form still works and lands on a plain thank-you or try-again page.
What isn’t done yet
Honest gaps, in the order they are likely to be closed:
- HSTS preload. The header is ready for it, but joining the browser preload list is a hard commitment for the whole domain, so it waits until every subdomain is settled.
- DNS hardening. DNSSEC, CAA records limiting which authorities may issue certificates, and SPF, DKIM and DMARC for the domain’s mail are set at the registrar and DNS level, not in this code.
- Trusted Types. The site’s own scripts write very little HTML, but enforcing Trusted Types would make that a browser-checked guarantee.
- A challenge on the form. If spam gets through the honeypot and rate limit, the next step is a privacy-preserving challenge, which would add the first third-party request.
Check it yourself
Don’t take this page’s word for it. From a terminal:
curl -sI https://armory.network/
Or open your browser’s developer tools, reload, and look at the response headers and the network tab. If something here is wrong, or you find a way around any of it, open a channel and choose Disclosure.
Written 4 November 2026. When the configuration changes, this note changes with it.