VayuPress Threat Model
Version: 2.0.0
Date: 2026-10-16 (rewritten against the code; v1.0 was 2026-06-12)
Author: Security Lead, @johalputt
Review cycle: Annual or after any architectural change
Every mitigation below names the file, setting or check that performs it. A
mitigation that cannot be named is listed as a residual risk instead.
Trust Boundaries
┌──────────────────────────────────────────────────────────────────────┐
│ INTERNET (untrusted) │
│ Readers · Members · Authors · Mail servers · MCP clients · Attackers│
│ ─────┬───────────────────────────────┬──────────────────── TB1 ───── │
│ ▼ HTTPS 443 ▼ SMTP/IMAP/POP3 (25 587 143 993 110 995)
│ ┌──────────────────────────┐ │ │
│ │ Nginx: TLS, compression │ │ │
│ └────────────┬─────────────┘ │ │
│ ──────────────┼──────────────────────┼───────────────── TB2 ───────── │
│ ▼ 127.0.0.1:8080 ▼ │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ VayuPress, one Go process, user www-data │ │
│ │ VayuShield → routes → console / API / MCP / site / mail / talk │ │
│ │ plugins: out of process, seccomp + namespaces │ │
│ └───────┬──────────────────────────────────────┬────────────────┘ │
│ ─────────┼──────────────────────────── TB3 ──────┼─────────────────── │
│ ▼ ▼ outbound (TB4) │
│ /var/lib/vayupress (SQLite WAL, media, mail delivery, ACME, │
│ mailboxes, PGP store) update mirror, feeds, │
│ Tor, operator-configured │
│ providers │
└──────────────────────────────────────────────────────────────────────┘
- TB1 — Internet ↔ host.
ufw allows 22, 80, 443 and the mail ports only.
HTTPS terminates at nginx; mail is served by VayuPress directly over
STARTTLS / implicit TLS.
- TB2 — nginx ↔ application. The application listens on
127.0.0.1:8080.
It resolves the client address itself and trusts forwarding headers only
from a configured proxy (realIPMiddleware, auth.ClientIP).
- TB3 — application ↔ storage. One data directory owned by
www-data;
systemd allows writes only to the data, log, cache, static and backup
directories (ReadWritePaths, ProtectSystem=full).
- TB4 — application ↔ the outside. Outbound connections are listed under
Network below.
Entry Points
| ID |
Entry point |
Authentication |
Notes |
| EP-01 |
Public site (/, posts, topics, search, feeds, llms.txt) |
None |
Behind VayuShield; cached pages |
| EP-02 |
Members (/members, sign-up, comments) |
Passwordless member session |
Comments are held for moderation |
| EP-03 |
Console /os |
Session cookie + TOTP (optional) + CSRF token on writes |
Role checked per path (osPathMinLevel); /os/api/* defaults to admin |
| EP-04 |
API /api/v1 |
API key (X-API-Key or Bearer) |
Scoped section:action keys, stored hashed, per-key rate budget |
| EP-05 |
MCP /mcp |
OAuth 2.1 or an API key |
A tool the key does not grant is invisible and refused |
| EP-06 |
/health, /health/live, /health/ready |
None |
Liveness only |
| EP-07 |
Detail health (/health/db, /queue, /storage …) and /metrics |
Loopback or API key; 404 otherwise |
Recon surface closed (audit H5) |
| EP-08 |
/debug/pprof/* |
Administrative API + its own rate limit |
ADR-0037 |
| EP-09 |
Payment webhooks (/api/v1/stripe/webhook, /api/v1/payments/webhook/{gateway}) |
Provider signature / HMAC |
Idempotent fulfilment |
| EP-10 |
Mail: SMTP 25, submission 587, IMAP 143/993, POP3 110/995 |
SMTP AUTH / app passwords for clients; SPF, DKIM, DMARC for inbound |
internal/vayuos/mail |
| EP-11 |
VayuTalk relay (talk. subdomain) |
Talk token per mailbox |
Messages held only in memory |
| EP-12 |
/.well-known/* (WKD key discovery, OAuth metadata) |
None |
Read-only |
| EP-13 |
Tor .onion (VayuTor) |
As the clearnet routes |
Onion responses carry no HSTS, no-referrer |
| EP-14 |
SSH 22 |
Operator's keys |
Outside VayuPress |
Assets
| ID |
Asset |
Sensitivity |
Location |
| A-01 |
Published posts |
Public |
/var/lib/vayupress/vayupress.db |
| A-02 |
Drafts, members, orders |
Confidential |
The same database |
| A-03 |
Install API key |
Critical |
API_KEY in /etc/vayupress/env |
| A-04 |
Scoped API keys, OAuth grants |
High |
The database, as hashes |
| A-05 |
TLS private keys |
Critical |
certbot's store (/etc/letsencrypt) |
| A-06 |
PGP private keys |
Critical |
/var/lib/vayupress/vayudata/pgp, AES-256-GCM at rest |
| A-07 |
Mailboxes |
Confidential |
/var/lib/vayupress/vayudata/mail |
| A-08 |
Third-party secrets (payments, AI, DNS) |
Critical |
The database, AES-256-GCM at rest (internal/secrets) |
| A-09 |
Backups |
High |
VAYUKEEP_TARGET, AES-256-GCM under an Argon2id-wrapped key |
| A-10 |
Audit log |
High |
audit_log, append-only by trigger (migration 005) |
| A-11 |
Media |
Internal |
/var/lib/vayupress/media |
| A-12 |
Onion service keys |
Critical |
The database (pinned so addresses stay stable) |
| A-13 |
Configuration |
Critical |
/etc/vayupress/env |
Threat Actors
| ID |
Actor |
Motivation |
Capability |
Likelihood |
| TA-01 |
Script kiddie |
Defacement, crypto mining |
Low |
High |
| TA-02 |
Spam bot, scraper, AI crawler |
SEO spam, comment spam, content harvesting |
Low–Medium |
High |
| TA-03 |
Credential stuffer |
Account takeover |
Medium |
Medium |
| TA-04 |
Targeted attacker |
Data exfiltration, content manipulation |
High |
Low |
| TA-05 |
Supply chain attacker |
Backdoor via dependency or release |
High |
Low |
| TA-06 |
Malicious author or staff member |
Privilege escalation, data theft |
Medium |
Low |
| TA-07 |
Rogue maintainer |
Insider threat |
High |
Very Low |
| TA-08 |
Nation-state |
Censorship, surveillance, deanonymisation of a Tor Space |
Very High |
Very Low |
Mitigations
Authentication & Authorization
| Threat |
Mitigation |
Implementation |
| Credential stuffing (TA-03) |
Lockout per address |
5 failures in 15 min → locked 1 h (internal/auth, ADR-0021) |
| Weak password storage |
Argon2id |
internal/auth |
| Session theft |
Second factor |
TOTP (internal/totp) |
| CSRF |
Double-submit token |
auth.CSRFTokenMiddleware on every writing method |
| Privilege escalation (TA-06) |
Roles per path, admin by default for the console API |
osPathMinLevel (cmd/vayupress/handlers_auth.go) |
| API key theft |
Scoped, hashed, expiring, revocable keys |
internal/apikeys (ADR-0134) |
| An AI client doing too much |
The connector's tools are limited to its key's scopes |
internal/mcp, ADR-0139 |
Injection
| Threat |
Mitigation |
Implementation |
| SQL injection |
Values bound with ? placeholders |
database/sql |
| XSS through content |
Post HTML sanitised to an allowlist; SVG to its own closed allowlist; a strict CSP |
bluemonday policies in internal/render; scripts only by per-request nonce or hash (ADR-0036) |
| Template injection |
html/template auto-escaping |
Go standard library |
| Path traversal |
Cleaned and confined paths |
render.CacheWrite, unsafePathComponent, the media name pattern |
| Mail header injection |
A decoded line break in a header becomes a space |
decodeMIMEWord (internal/vayuos/mail) |
Network
| Threat |
Mitigation |
Implementation |
| SSRF (TA-04) |
Fetches of URLs a user or plugin supplies refuse private, loopback and link-local addresses |
internal/safefetch — embeds, media import, AI providers, VayuFlow, feeds, mail recovery |
| Clickjacking |
X-Frame-Options: DENY, frame-ancestors 'none' |
Application headers |
| Protocol downgrade |
HSTS, two years with subdomains (clearnet only) |
Application headers; nginx ssl_protocols TLSv1.2 TLSv1.3 |
| Application-layer floods, scrapers (TA-02) |
VayuShield: lane, fair-shed, reputation, proof-of-work, optional XDP |
internal/vayushield (ADR-0111) |
| Volumetric floods |
Not mitigated on the host |
See RR-04 |
| Data leaving a Tor Space (TA-08) |
Clearnet callbacks gated off; egress through Tor |
internal/vayuos/torspace, tor_egress.go (ADR-0141, ADR-0143) |
Outbound connections the install makes: mail delivery to recipients' MX
hosts; ACME (certbot); the update mirror (releases verified against a pinned
Ed25519 key); the datacenter range feeds (AWS, Google Cloud, DigitalOcean); the
Tor network when VayuTor is on; and, only when the operator configures them,
payment providers, AI providers, Cloudflare's API, IndexNow and webmentions.
Supply Chain
| Threat |
Mitigation |
Implementation |
| Vulnerable dependency (TA-05) |
govulncheck on reachable code; weekly freshness check |
ci.yml, dep-freshness.yml |
| Tampered module |
go mod verify; go.sum |
CI |
| Build tampering |
Two cold builds must be byte-identical; pinned Go toolchain |
scripts/reproducible-build.sh |
| Artifact substitution |
cosign-signed releases with an RFC 3161 timestamp; the updater verifies an Ed25519 signature against the operator's pinned key |
tag-release.yml, internal/update/releasekey.go |
| Committed secrets |
trufflehog |
CI |
| Licence |
Apache-2.0 and an SPDX identifier on every Go file |
check-licenses |
Data Protection
| Threat |
Mitigation |
Implementation |
| Backup theft |
Encrypted, chained, authenticated archives |
internal/backup, VayuKeep (ADR-0145) |
| Key theft from the database |
PGP private keys and third-party secrets encrypted at rest |
AES-256-GCM (internal/vayuos/pgp, internal/secrets) |
| Log tampering |
Append-only audit table |
Triggers in migration 005 |
| Database corruption |
WAL mode; integrity checked on every staged update and every restore drill |
internal/update/snapshot.go, internal/vayukeep |
| Migration tampering |
Checksum verification at start; the service will not start on drift |
internal/db (ADR-0034) |
| Reader tracking |
No cookies, no IP or user agent stored; a daily-rotating salted hash |
internal/vayuanalytics/session (ADR-0097) |
Infrastructure
| Threat |
Mitigation |
Implementation |
| Root privilege abuse |
Runs as www-data; NoNewPrivileges; only CAP_NET_BIND_SERVICE (for the mail ports) |
vayupress.service |
| Writes outside the data directories |
ProtectSystem=full, ReadWritePaths limited to data, log, cache, static and backup |
vayupress.service |
| Port scanning |
ufw: 22, 80, 443 and mail ports |
scripts/deploy-vayupress.sh |
| Brute force on nginx |
fail2ban jails nginx-http-auth, nginx-limit-req |
/etc/fail2ban/jail.d/vayupress.conf |
| Plugin escape |
seccomp (seven architectures), namespaces, capabilities dropped, FDs closed, minimal env |
internal/sandbox (ADR-0057) |
| Certificate expiry |
certbot's renewal timer |
certbot |
Residual Risks
| ID |
Risk |
Severity |
Accepted By |
Rationale |
| RR-01 |
One SQLite writer |
Low |
Architecture Lead |
A design choice; 234,727 posts on one host serve without it binding (docs/BENCHMARKS.md) |
| RR-02 |
ReadOnly mode did not refuse post writes |
Medium |
— (closed, 3.18.8) |
Closed: post and page writes are refused at enqueue, in the worker, by the scheduler and on the console routes (mode.RefusesContentWrites, docs/architecture/system-modes.md) |
| RR-03 |
SSH is outside VayuPress |
Low |
Operator |
fail2ban's jails cover nginx, not SSH; key-only SSH is the operator's to configure |
| RR-04 |
Volumetric floods |
High |
Security Lead |
Every host layer runs after a completed handshake on the uplink; only anycast capacity in front absorbs one. VayuShield's posture report keeps it as a permanent fail |
| RR-05 |
Mutual TLS for plugin IPC |
Low |
Security Lead |
Plugins are local subprocesses; revisit when the plugin SDK is external |
| RR-06 |
IndexNow trusts the configured key and host |
Low |
Operator |
Submissions are suppressed in read-only, quarantined and maintenance modes and journalled; the key is operator-supplied and not rotated automatically |
| RR-07 |
Both Spaces share one machine |
Medium |
Operator |
Spaces separate identity and content, not the host; the console states this (ADR-0143) |
Review Log
| Date |
Reviewer |
Changes |
| 2026-06-12 |
@johalputt |
Initial threat model (v1.0) |
| 2026-06-14 |
@johalputt |
Added the federation HTTP-signature gap and RR-06 (IndexNow) after a code audit |
| 2026-10-16 |
@johalputt |
v2.0, rewritten against the code: the isso comment server, the vayupress user, /data paths, the cron integrity check and federation are gone; entry points, assets, outbound connections and mitigations now name their mechanisms; RR-02 (read-only does not refuse post writes), RR-04 and RR-07 added |