Audit date: 2026-02-25. Scope: what breaks when QNTX moves from 127.0.0.1 to the public internet.
QNTX assumes the network boundary IS the security boundary. Every endpoint is reachable by the machine owner alone. On the www, every endpoint is reachable by everyone.
server.bind_address defaults to 127.0.0.1. The server refuses to start if bind address is non-loopback and auth.enabled is false.
# am.toml — required for non-localhost deployment
[server]
bind_address = "0.0.0.0"
[auth]
enabled = true
Code: server/init.go (safety check), am/defaults.go (default + env binding QNTX_BIND_ADDRESS).
No TLS. server/lifecycle.go uses ListenAndServe(). All traffic (auth cookies, attestation data, sync payloads) is cleartext. Needs ListenAndServeTLS or a reverse proxy (Caddy, nginx).
Peer sync has zero authentication. server/sync_handler.go — /ws/sync accepts any WebSocket connection. An attacker can run Merkle tree reconciliation and exfiltrate the entire attestation store. Budget data (spend limits) is also exchanged. This is the single biggest exposure.
CORS origin validation uses prefix matching. server/util.go:51 — strings.HasPrefix(origin, allowedOrigin). With http://localhost in the allowlist, http://localhost.evil.com passes. Needs exact scheme+host+port matching.
No rate limiting. Zero rate limiting on any endpoint. Login ceremonies, API calls, file uploads, WebSocket connections — all unlimited.
Missing Origin header accepted on WebSocket. server/util.go:32 — if origin == "" { return true }. Raw WebSocket clients bypass origin checking entirely.
/health leaks reconnaissance data. server/handlers.go:411-428 — Public endpoint returns version, git commit, build time, client count, owner name.
In-memory sessions. server/auth/sessions.go:18 — sync.Map. Server restart logs out all users. Under DoS this amplifies impact. Sessions need SQLite persistence.
10MB WebSocket messages x 256 buffer depth. server/client.go:40,25 — Each client can buffer ~2.5GB. A few malicious clients = OOM.
Session cookie missing Secure flag. server/auth/handlers.go:221-229 — Cookie is HttpOnly + SameSite=Lax but not Secure. Over HTTPS the cookie can still leak via HTTP downgrade.
WebAuthn RPID hardcoded to "localhost". server/auth/auth.go:46 — WebAuthn won't work on a real domain. RPID must come from config.
DNS rebinding on sync connections. server/sync_handler.go:122 — Standard websocket.Dialer resolves DNS at connect time.
SQLite database unencrypted at rest. Anyone with filesystem access reads all attestations, credentials, embeddings.
Watcher engine doesn't use SaferClient. ats/watcher/engine.go:110 — Standard http.Client on user-configured URLs. See docs/security/ssrf-protection.md.
Plugin binaries have no integrity verification. plugin/grpc/discovery.go:288-294 — Binary found by name in search paths, executed without checksum or signature.
No request body size limit on most POST endpoints. File uploads (50MB) and prose (10MB) have limits. Config updates, attestation creation, type creation do not.
server/files.go)//go:embed, no filesystem traversal possibleplugin/grpc/auth.go)SaferClient blocks private IPs on AI provider requests (internal/httpclient/safer_client.go)am.toml excluded from git, env var overrides available| Pri | Item | Effort | Status |
|---|---|---|---|
| P0 | Auth required for non-loopback bind | Low | Done |
| P0 | TLS termination | Low | Open |
| P0 | Peer sync authentication | High | Open |
| P0 | CORS exact matching | Low | Open |
| P0 | Rate limiting middleware | Medium | Open |
| P1 | WebAuthn RPID from config | Low | Open |
| P1 | Require Origin header on WS | Low | Open |
| P1 | Strip /health or auth-gate it | Low | Open |
| P1 | Secure flag on session cookie | Low | Open |
| P1 | Persist sessions to SQLite | Medium | Open |
| P1 | WebSocket per-client memory cap | Medium | Open |
| P2 | Request body limits on remaining endpoints | Low | Open |
| P2 | Plugin binary signature verification | Medium | Open |
| P2 | SQLite encryption at rest | Medium | Open |