Swazee mark Product (type slug) Tool (chevron-wrench) Experiment (4-point star) Active (filled diamond) Shipped (hollow diamond) Shelved (diamond + slash) External link (↗) Search (angular magnifier) Filter (funnel) Close / Esc (chunky X) Move down (j) Move up (k) Return / Enter
SWAZEENET
VOL. I · NO. 01 · EST. MCMLXXXVII
BROADSIDE 12 active · 2026·05·14
№ 01 · product · Go 1.22 · 2025—

secnull,
a CMS that signs its own work.

A Go single-binary CMS for publishing Ed25519-signed security audits. Strict CSP nonces, Argon2id+TOTP login, SHA-256-chained audit log.

go security cms

secnull (cmd/secnullcms) is a publication whose credibility depends on the publisher's posture. The CMS itself is single-binary, pure-Go SQLite (no CGO), and refuses to serve any audit whose hash does not match the signed manifest at content/integrity.json. Every editorial action ends up in an append-only SHA-256-chained audit log.

ITech scope

IIArchitecture

Subcommand dispatch happens before flag parsing. main.go switches on os.Args[1] for serve, keygen, sign, verify, verify-audit, admin-bootstrap, and export, then routes to a per-subcommand handler in internal/subcommands/. Adding a subcommand means editing two files in lockstep — the dispatch arm in main.go and the implementation in internal/subcommands/<name>.go. The order matters: an earlier design that mixed flag parsing with subcommand selection produced order-of-operations bugs around shared flags like -config, and the explicit pre-parse split closes that class of bug.

Content loads once at startup, then optionally hot-reloads via an mtime-polling watcher running on a 500 ms tick. fsnotify was rejected for cross-platform parity reasons: Windows ReadDirectoryChangesW and Linux inotify disagree about atomic-rename publishing semantics, so a single poll loop avoids the divergence. If signature enforcement is on, any manifest mismatch aborts startup with a non-zero exit code — make sign-content is mandatory after every audit edit, and CI refuses to merge a content change whose signature has not been refreshed.

Routing uses Go 1.22 method-prefixed mux patterns ("GET /audits/{slug}"), so HTTP method is part of the route table rather than branched at handler time. The middleware chain order is load-bearing and tested explicitly: requestID → recoverer → accessLog → rate-limit → securityHeaders → adminAllowlist → originCheck → csrf → mux. Public read routes intentionally bypass the rate limiter so a search-engine crawl can’t starve a live reader; admin routes never bypass it. Reordering rate-limit and securityHeaders — for example, to "let throttling happen sooner" — would let a 429 response leak headers that should never be served, which is why both unit tests and a fuzz harness pin the order.

IIIState surfaces

IVOperating posture

The day-2 surface is small on purpose. An operator runs secnullcms admin-bootstrap exactly once per environment to seed the Argon2id+TOTP credential, after which every subsequent admin action goes through the web admin behind the middleware chain. Audit edits are made in the content tree on a workstation, signed with secnullcms sign, committed, and deployed; the deploy itself is a static binary refresh plus a content-tree sync. There is no admin SPA shipped to the browser — the editorial UI is server-rendered HTML, and any JavaScript on it is gated by the per-request CSP nonce, which is also why CSP reports go through the same audit log as everything else.

Failure modes are loud rather than silent. A signature mismatch on boot is fatal, not a warning — the process exits with a non-zero code so a supervisor like systemd or the Windows Service Control Manager surfaces the fault. A CSP violation is captured to audit.log with the offending element, the route, and the request id. A failed Argon2id verification increments a per-account lockout counter whose next-attempt window backs off quadratically; the counter is stored in the same secnull.db as the session store, which keeps the recovery story coherent across restarts.

Fig. I.
01requestID 02recoverer 03accessLog 04rate-limit 05secHeaders 06adminAllow 07originCheck 08csrf
8 stages, public reads bypass rate-limit · as of 2026-04-26
Fig. II.
server 3399 53% content · 835 auth · 724 subcommands · 577 auditlog · 368 config · 365 cmd · 201
internal/ + cmd/ · 6,469 LOC across 24 files · as of 2026-04-26

VSurface

What callers see is small: a single 14.1 MB binary on the path. secnullcms serve brings the public site up; secnullcms admin-bootstrap enrols the first operator with Argon2id and a TOTP secret; secnullcms sign updates content/integrity.json after an audit edit; secnullcms verify and verify-audit validate the signed manifest and walk the SHA-256-chained log end to end. The HTTP surface is intentionally narrow — public read routes plus an admin allowlist gated by origin check and CSRF. There is no plugin API, no embedded admin SPA, and no client-side framework, which is why the binary is small enough to ship as a single artifact and the supply chain is auditable in one go.sum.

VIConstraints

The constraints are deliberate, not residual. Pure-Go SQLite avoids CGO so containers behave identically to bare-metal builds; the ‘0 CGO deps’ plate above is a contract enforced by the build, not a number observed after the fact. Subcommand dispatch happens before flag parsing because mixing the two created order-of-operations bugs around shared flags like -config in the previous design, and pre-parse separation closes that whole class of bug. The middleware order is explicit and tested — reordering rate-limit and securityHeaders, for example, would let a 429 response leak headers that should never be served, and both unit tests and a fuzz harness pin the order. Static-export mode renders the public site to ./dist for serving from any HTTP server, but the audit-log chain is only verifiable when the binary itself is on the path; static export is a read-only mirror, not the canonical surface.

:/ ESC