How it gets hacked, and why it mostly can't · 03_qals_architecture/QALNET_SECURITY.md
Qalnet Security — Threat Model & Hardening Plan
Date: 2026-09-06 · Question: "How easy would it be for this network to be exploited and hacked — and ways around it?" Scope: qalnet-dev-1 as running today + the Phase 1-2 design. Honest, attacker-brained, with a live audit finding already fixed-by-design.
0. The honest one-paragraph answer
A private chain run by one company is smaller-surface but higher-concentration than a public chain: there's no anonymous attacker horde (RPC isn't public), but qalarc is the trust root — if our keys/machines fall, the chain falls. The design answers this in layers: Tailscale-only consensus, owner-set ledger-enforced caps on any agent (optional, uncapped by default — the prompt-injection-drains-the-wallet class of attack dies at a set cap), full-reserve money that can't be printed past the cap (B-QALS issued only against confirmed AUD; supply pre-endowed in the issuer wallet qals:issuer — cap gate fail-closed in loopd, on-chain twin written + tested), and anchoring to public IOTA so our private history is publicly tamper-evident. Biggest real-world risks today are operational (keys on home machines, exposed devnet ports, fork supply-chain), not cryptographic.
1. Live findings (audit of the running devnet, 2026-09-06)
| # | Finding | Severity | Status/mitigation |
|---|---|---|---|
| F1 | RPC (:9000) and faucet (:9123) bind 0.0.0.0 — reachable from LAN | Med (devnet, no real value) | Devnet only; production binds RPC to localhost/Tailscale IP, public access via an authenticated gateway. Validator gRPC correctly localhost (127.0.0.1:19200). |
| F2 | Validator + fullnode keys live in /tmp/qalnet-dev1 plaintext |
Med (devnet) | Production: keys in Stronghold/OS keystore, 0700 dirs; genesis ceremony on isolated media. |
| F3 | Client YAML contains api_mainnet.iota.cafe envs with grpc set — accidental --env mainnet would target public IOTA |
Low | Keep active_env pinned; document; CI guard later. |
| F4 | Single validator (committee-size 1) — the operator IS consensus | By design (dev) | Phase 1: 4 validators (3f+1 tolerates 1 Byzantine), consensus traffic inside Tailscale. |
| F5 | Anchors trust producer-declared metadata (hash is real, but "what it claims to be" is producer-signed only) | Accepted (v0) | v1: signature field populated with Qal ID producer keys; issuer accreditation via Hierarchies. |
2. Threat model by surface
2.1 Network & validators
- DoS / residential internet flakiness: Starfish is designed for lagging participants (that's why we chose it); the cloud validator keeps quorum alive if a home node drops. Mitigation: 4-validator committee (tolerates 1 fault), grow to 7; alerts to Signal (hub) on epoch misses.
- Validator key theft (home machines): highest-impact realistic attack — a stolen validator key on a 4-node net can't forge history alone (needs >⅓ byzantine to halt, >⅔ to attack) but can sign bad blocks/censor. Mitigations: keys never leave Stronghold-backed storage; validator machines have no interactive logins beyond ops; Tailscale ACLs segment who can reach validator ports; disk encryption.
- Eclipse/Sybil: irrelevant while p2p is Tailscale-only (peers are explicitly our machines). Becomes relevant only if we open the validator set — gate behind accreditation + stake.
- Consensus attacks (long-range, nothing-at-stake variants): N/A to BFT with checkpointed finality; anchoring checkpoint roots to public IOTA adds external light-client verification.
2.2 Money layer (the juiciest target)
- "Print B-QALS": structurally impossible — the issuance entry-point requires oracle-confirmed AUD deposit; supply invariant (
reserve ≥ outstanding) is assert-enforced in the issuance path; monthly attestations + auto-pause on >0.5% discrepancy. (2026-09-15: the on-chain twin is being redesigned from mint-gated to genesis-preminted + transfer-gated at the cap-raise checklist — the full sellable supply is pre-endowed in the genesis issuer walletqals:issuer; a purchase transfers from it.) The attack becomes forge a bank statement — a fraud problem with a paper trail, not a code problem. - Reserve oracle capture (3-of-5 multisig): two colluding signers can trigger
investigate(annoyance); three can false-attest — mitigations: external signer independence, reconciliation against PSP data an attacker can't rewrite, and the chain's own liability ledger (any false attestation is provably false the moment redemption exceeds reality — the system is designed to fail loud, not silent). - Hold/settle games (provider overcharging): actual clamped to hold; telemetry cross-checked (wall clock, benchmarks); sampling verification; slashing. Buyer exposure is bounded by the hold they placed — the petrol-card property.
- Replay/double-spend: object model with versioned UIDs — replay is a non-issue at the VM level (each tx consumes unique objects).
2.3 Agent layer (the NEW attack class everyone's getting wrong in 2026)
- Prompt injection → wallet drain: the headline attack of the agent economy. Ours is bounded structurally: epoch caps in
qal_credit(a hijacked agent physically cannot exceed its allowance), per-job Holds (it can't commit future funds), revocation flips a status bit (~2s network-wide). A fully-pwned agent's worst day = its daily cap, with a complete signed audit trail. - Agent key theft: blast radius = cap + revocation; keys are delegated (governance DID is ops-controlled) — agents never custody master keys.
- Malicious tool/MCP server: tools sit behind the gateway (holds + caps apply per call); capability VCs name exactly which tools an agent may invoke.
2.4 Contract layer (Move)
- Move's design (linear types, no dynamic dispatch, resource safety) kills entire bug classes (reentrancy, phantom assets) that plague Solidity. Remaining risks: our logic bugs (bad cap math, hold mismatches) → mitigated by property tests (8/8 today, growing), the linter suite (already caught patterns during publish), and a paid external audit before any real value — budgeted in the business review.
- Genesis misconfig: fixed supply, no admin "mint" capability in
qal_*packages by construction; upgrade authority: packages initially immutable (nocapobjects exposed) until governance needs it.
2.5 Supply chain (the underrated one)
We run a ~200-crate fork of third-party code. Real risks: upstream compromise, malicious dep injection via a future cargo add, stale CVEs. Mitigations: Cargo.lock committed; monthly upstream rebase cadence (implementation plan); cargo audit/dependabot on the fork repo; builds reproducible-ish on one machine; no code runs from repos/ (reference only) — only qalnet/ is built.
2.6 Data layer (accountability pipeline)
- Hash collisions: sha256 — no practical threat; anchor stores exactly 32 bytes.
- Payload availability rot (URI dies): the real long-term risk — mitigations: MinIO replication across fleet + periodic re-verification (
qalpipe verify-dir), anchor stores content hash so any surviving copy re-validates. - Metadata lying: anchor says what producer claims — v0 accepted, v1 producer signatures + accredited issuers (F5).
- Registry loss: verification reads the CHAIN (registry is only an object-id index) — survives the registry being destroyed.
2.7 Physical/ops
Home machines: power/net loss (Starfish tolerates; DR drills monthly), disk death (snapshots + snapper on superlocal; chain DB rebuildable from genesis.blob), theft (disk encryption, keys in Stronghold, revocation drills). The hub's Signal alerts are the incident channel.
3. "How easy is it to hack?" — scored
| Attack | Difficulty today | After Phase 1 (4 validators, hardened) |
|---|---|---|
| Steal a user's B-QALS | needs their machine+keys | same (no chain path exists) |
| Drain an agent | capped by epoch allowance; revoked in 2s | same, smaller caps, on-chain |
| Forge a receipt/anchor | would need to rewrite chain history | + publicly falsifiable (IOTA anchoring) |
| Print money | no path exists (paper phase: admin of loopd) | on-chain: needs reserve oracle majority + bank fraud |
| Halt the chain | DDoS superlocal's link (devnet: trivial; cost: nothing at stake) | needs 2 of 4 validators offline |
| Corrupt history | 1-of-1 validator today (devnet!) | >⅓ committee Byzantine + defeats anchoring checks |
Devnet is soft on purpose — it holds no value. The table above is why Phase 1 (committee + hardening) precedes any real money.
4. Hardening backlog (ordered)
- Bind all devnet services to 127.0.0.1/Tailscale IP (config, not firewall surgery).
- Keys out of /tmp → Stronghold + 0700 (script it).
- 4-validator committee across Fleet + 1 cloud VM; DR drill (kill one, watch Starfish heal).
cargo audit+ lockfile commit on qalnet; monthly upstream rebase.- Property-test expansion for qal_credit edge cases (concurrent holds, epoch boundary).
- Anchor
authorized_keys/ACL hashes (the SSH/Tailscale integration) — dogfooding F5's fix. - External Move audit before Phase-2 value (US$30-80k, budgeted).
- Public-IOTA checkpoint anchoring cron (external tamper-evidence).
- Revocation drill scheduled monthly (hub → Signal).
- Incident runbook (who, what, Signal channel, kill switches).
Philosophy: we're not trying to be unattackable (nobody is); we're making every attack bounded, loud, and provable — caps bound it, events/alerts announce it, receipts and anchors prove it. That's a security posture you can sell.
Findings addendum (2026-09-10, from the multi-relay test campaign — 53/53 green)
- init aborts if any relay in
--relaysis dead (qalchat.py:975 unguarded http_json) — one down relay blocks identity creation pre-loopd-claim. Fix queued: per-relay warning + continue. - import-env "no relay contact" guarantee holds only for pinned senders — first-contact imports need a keyserver (qalchat.py:2039/497). Documented in ferry usage.
- Envelope-id dedupe is per-server (ids minted per-POST, qalchat.py:2334): cross-relay replicas surface as chain-gap noise (exit-3), not silent dupes — bounded, loud, acceptable; full fix = client-minted ids.
- Tailscale DERP flaps on the fleet path (20s+ latency spikes) — all cross-machine tests now warm up
tailscale ping; monitors should alert on DERP-sustained paths. - Relay #2 confirmed live through the full matrix: failover, isolation, 2MiB file transfer (sha256 ✓, anchored), offline ferry with replay rejection.
- Fleet availability flap observed live during verification: relay #2 answered, timed out, answered again within minutes (systemd Restart=always on our side — the machine/network itself wobbles). Verified states: agent run 53/53 → my run 27+1fail (DERP timeout) → retry 48/49 (repost window missed during flap). Action: heartbeat→Signal alerting (SECURE_OPS weekly item) promoted to NOW; consider a third relay for quorum-of-mailboxes.
Red-team campaign 2026-09-10 — findings & fixes
A dedicated attacker agent ran the full attack book (findings-only, all PoCs reversed). 4 CRITICAL · 5 HIGH · 8 MEDIUM · 4 LOW — full report: 11_system_audit/REDTEAM_FINDINGS_2026-09-10.md (PoCs preserved in redteam_20260910_poc/).
CRITICALs — all fixed same-day (verified live):
- C1 unauth topup → admin-key gate (QALLOOPD_ADMIN_KEY; paper-mode warns loudly)
- C2 untracked registration balances → opening balances now count against the 500M program cap
- C3 negative settle = infinite money (settle -500000 credited +500k) → hard clamp max(0, …); verified: exploit attempt settles 0
- C4 holds never escrowed (expiry-release was phantom credit) → holds now debit at placement (petrol-card, matching qal_credit on-chain); settle gained to_agent_id (escrow pays beneficiary directly — the on-chain semantics, in the paper rail)
Still open (queued, from the report): bind chain RPC to 127.0.0.1 at next regenesis; relay pubkey registration needs signed proof (fleet-relay TOFU hijack); marketplace offer signing (seller ed25519); qalpipe verify should bind the registry entry to the original object_id+timestamp (registry-rewrite hardening). HIGHs and below: see the report — each has a concrete patch sketch.
WAVE-25 addendum (2026-09-11)
- Relay pubkey proof-of-possession SHIPPED (red-team HIGH closed): registrations now carry
X-Pubkey-Proof(ed25519 overpubkey-register:<handle>); relay verifies before storing (proof_verified: trueon legit; forged sigs → 401, verified live). Transition-compatible (proofless = flagged unverified); peer pins remain the client-side backstop. Ops note: a stale nohup relay had been shadowing the systemd unit on :8830 — killed; runbooks updated (always checkss -tlnppid vs unit MainPID). - Wallet passkeys shipped with an honest Linux finding: WebKitGTK (this webview) ships without WebAuthn — passkey UI activates on macOS/Windows, full local verifier (CBOR/COSE, 10 tests) enforced Rust-side where available;
tauri-plugin-authenticatoris the Linux upgrade. Passkey = presence gate; vault+mnemonic remain the crypto root. - gmux-bridge (:8842) — gmux's real surface mapped (its :8768 phone dashboard, permission-respond API): gmux panes ↔ capped loopd wallets on one screen, permission alerts → owner's phone inbox via encrypted DMs, 30/30.