Security
This document covers the security posture of the features added alongside the admin redesign and self-update system. For the broader threat model see THREAT-MODEL.md.
Self-update
The self-update system is designed to eliminate the remote-code-execution class that a naïve "upgrade now" button would introduce. See ADR-0064.
Trust boundaries
| Step | Trust mechanism |
|---|---|
| Check for updates | Read-only; no trust decision, no mutation |
| Download binary | Over TLS to GitHub; bytes are untrusted until verified |
| Integrity | SHA-256 vs published checksum (constant-time compare) |
| Authenticity | Sigstore signature, pinned to this project's release workflow identity |
| Executable check | The verified bytes must be an executable image for this platform |
| Apply | Atomic rename, previous binary kept as .bak |
| Restart | Operator action only — never automatic |
Why signatures, not just checksums
A checksum proves the bytes match the published checksum file. If the release channel is compromised, the attacker controls both the binary and its checksum — they are published together and fetched over the same connection, so a checksum is a self-certificate.
Authenticity is therefore the release's Sigstore signature. It is verified against a trust root compiled into this binary, so verification needs no network and cannot silently degrade when a fetch fails.
A Sigstore signature on its own proves only that somebody signed — anyone can run a CI job and obtain a valid certificate. The security is in the identity policy: the signature must come from
- SAN
https://github.com/johalputt/VayuPress/.github/workflows/tag-release.yml@refs/heads/main - issuer
https://token.actions.githubusercontent.com
The branch is part of that identity deliberately. The same workflow filename on another branch, or in a fork, produces a perfectly valid signature carrying a different SAN, and a policy matching only the repository would accept it.
Signing certificates are keyless and live about ten minutes, so every release is signed by a certificate that has long expired by the time anyone installs it. Validity is therefore judged as of a signed record of when the signature was made, not the current time. Two independent observers supply that record: the transparency log entry, whose inclusion in the log is verified before its timestamp is trusted, and an RFC3161 countersignature from a timestamp authority anchored in the same embedded trust root.
Requiring such a timestamp is not the same as verifying one. v3.17.29 asked for the observer-timestamp threshold without asking for the transparency log entry to be verified — and verifying that entry is what produces the timestamp — so the threshold was compared against an empty set and every genuine release was rejected. It is recorded here because the shape of the mistake outlives it: a policy clause that consumes a value proves nothing unless some other clause produces it.
Gates (all must pass before any filesystem change)
- System mode ∉ {
read-only,quarantined,maintenance} - Checksum verifies
- A signature bundle exists for the binary — a release without one is refused, never accepted on its checksum
- That signature verifies, and its certificate identity matches the workflow above
- The verified bytes are an executable image for this platform
- Database backed up (optional; the operator's choice, and never silent)
VAYU_RELEASE_PUBKEY is an optional additional Ed25519 pin for operators who
maintain their own key. It is not required, and it is no longer what provides
authenticity.
The CLI path additionally requires VAYU_SELFUPDATE_ENABLED=true.
The web apply route
POST /os/api/update/apply downloads, verifies and replaces the binary;
/os/api/update/restart and /os/api/update/rollback sit beside it. All three
are admin-role-checked and CSRF-protected, and all three run the verification
above — there is no flag, environment variable or request field that turns it
off.
An earlier version of this document stated that no endpoint downloaded, replaced or restarted the binary. That described ADR-0064's original CLI-only design and had not been true since the VayuOS panel gained one-click updates. It is recorded here rather than quietly corrected, because a security document that under-describes the attack surface is itself a finding.
Admin UI (/admin/v2)
See ADR-0065.
- CSP preserved. No
unsafe-eval, nounsafe-inline, no third-party origins. Tailwind is precompiled; Alpine uses its CSP build; inline scripts carry a per-request nonce. - No new write surface. The editor reuses the existing
/api/v1/articlesendpoints and the established CSRF cookie/header handshake./admin/v2adds no privileged mutation route. - Non-breaking. The legacy
/adminis untouched, limiting the blast radius of any bug in the new UI to the new UI. noindex. All admin pages setX-Robots-Tag: noindex.
Plugin feature endpoints
The comment, webmention, and newsletter receivers accept untrusted public input:
| Endpoint | Hardening |
|---|---|
POST /api/v1/articles/{slug}/comments |
Stored as pending; rendered only after moderation; body sanitized on render |
POST /webmention |
Source/target validated; stored pending; W3C 202 semantics; no SSRF auto-fetch in the receive path |
POST /api/v1/newsletter/subscribe |
Email validated; double-opt-in confirmation token required before active |
Spam classification (internal/spam) and the existing rate limiter apply to
these public routes.
Dependency hygiene
All Go modules (core + every tool under tools/) are kept current via
go get -u ./... && go mod tidy. Note that this remote build environment's
network policy blocks vuln.go.dev, so govulncheck cannot reach the live
vulnerability database here; dependency currency is maintained by upgrade-and-tidy
plus CI running govulncheck where the database is reachable.
Reporting
Report vulnerabilities privately to the maintainers per MAINTAINERS.md. Do not open public issues for security reports.