Aller au contenu

Threat model

Ce contenu n’est pas encore disponible dans votre langue.

Status: living document, updated at every phase when the attack surface changes (required before F1 code). Scope: the open source client on a single machine. The managed/corporate tier adds endpoints and flows that will be modeled separately (F11).

  1. Machine availability of network, DNS and boot (the “never lock the user out” invariant).
  2. Integrity of system configuration (firewall, DNS, sysctl, units) and of IbexWall’s own state (state.db, audit log, snapshots, trust anchors).
  3. Confidentiality of local data: DNS query history, app inventory, network metadata, findings; never passwords, files, screen, keystrokes.
  4. Authorization boundary: who may apply changes (human approval, polkit) and what AI clients may see.
  5. Reputation of the project: signed releases, no supply-chain compromise.
  • B1: untrusted local users → core socket (anyone can connect; authz is per method via polkit).
  • B2: plugin processes (third-party code) → core (they only propose; sandbox units).
  • B3: network attacker → DNS resolver, web gateway, update downloads (blocklists, VM images, packages).
  • B4: untrusted data (domains, SSIDs, process names, log lines, AI justifications) → UI rendering and MCP/AI consumers.
  • B5: remote admin → web gateway (off by default) and future managed policy (bundles are signed; verified in core).
  • B6: kernel/root boundary: if an attacker already has root, IbexWall is not the defense (documented honestly, §8).
  • Spoofing: caller identity via SO_PEERCRED + SO_PEERPIDFD; never a bare PID (TOCTOU). polkit subject by pidfd where supported; fallback unix-process with start time. Risk: pidfd support varies by kernel/polkit version; verified in F1 spike before any authz ships.
  • Tampering: every mutation is a typed op validated against the manifest, snapshot-first, audit-logged with a hash chain (ibexctl audit verify). Audit log tampering detected by chain verification; root can truncate it (accepted, logged as limit).
  • Repudiation: audit entries bind actor (uid/proposal client), plan digest, result; AI proposals are attributed to the client and approved by a human.
  • Information disclosure: socket responses scoped by polkit action; config read excludes secrets by policy; logs carry no secrets; DNS query log off by default.
  • Denial of service: 0666 socket allows unauthenticated connects; per-peer concurrency limits and event backpressure; watchdog + Restart=on-failure; worst case the daemon dies → fail-open (network keeps working).
  • Elevation of privilege: ops are allowlisted per plugin manifest; exec ops use fixed argv from the manifest (no shell, no interpolation); file writes are path-allowlisted, symlink-free (openat2 RESOLVE_NO_SYMLINKS), size-capped; sysctl keys and nft prefixes are allowlisted. Executor may run in a transient unit with exact ReadWritePaths (F1 spike).
  • Spoofing: plugin binaries verified (minisign; dev key only in dev builds); local plugins require explicit admin trust and are labeled in the UI.
  • Tampering: a plugin can only propose ops; core rejects anything outside [privileges]. Manifest parsing is strict (deny_unknown_fields) and size-limited; units are generated by the core, never read from the plugin.
  • Repudiation: proposals carry plugin id and transaction id.
  • Information disclosure: plugin sandbox restricts read paths; sensitive plugins (accounts, disk) denied to MCP by default; plugins see their own config, not others’.
  • Denial of service: per-call timeouts; stuck plugins are restarted and marked degraded; monitor plugins holding eBPF links detach on death (fail-open by default).
  • Elevation of privilege: capability bounding sets are minimal and declared; root sandbox requests require justification and review in the plugin index.
  • Spoofing: upstream presets have fixed bootstrap IPs; DNSSEC validation configurable (off by default at MVP, ADR pending); TLS via rustls (no OpenSSL).
  • Tampering: blocklist downloads over HTTPS with size limits, parse validation and atomic swap; optional hash pinning.
  • Repudiation: blocklist updates are transactions in the audit log.
  • Information disclosure: queries leave only via the chosen encrypted upstream; plaintext 53/853 egress is dropped for everyone else (nft fragment) when enforcement is on; query logging off by default (opt-in, 24 h retention).
  • Denial of service: resolver death triggers on_failure (default restore: remove drop-ins, restart resolved, notify) so the machine keeps name resolution; portal-captive flows temporarily relax with a visible warning.
  • Elevation of privilege: only CAP_NET_BIND_SERVICE, own user, loopback bind; no write access anywhere.

3.4 Clients (Tauri app, web UI, ibexctl, TUI)

Section titled “3.4 Clients (Tauri app, web UI, ibexctl, TUI)”
  • Spoofing: they hold no privileges themselves; every privileged action round-trips through polkit (the desktop password prompt).
  • Tampering: CSP strict, no remote content, no {@html} with external data; Tauri capabilities minimal; types generated from schemas.
  • Information disclosure: the web gateway (off by default) binds loopback on a random port with a one-time token (ibexctl web open), Host/Origin validation (anti DNS-rebinding), CSRF protections; remote mode requires TLS + strong auth + explicit enable.
  • Denial of service: client crashes never affect protections (all state is server-side).
  • Prompt injection via untrusted data (B4): every external string (domain, SSID, process name, log line, justification) travels in a structured untrusted object, never concatenated into instructions; tool descriptions state this contract; a corpus of malicious inputs is part of the test suite (SEC-11).
  • Exfiltration: redaction before any response (stable pseudonyms via HMAC with a local salt; homes become ~); sensitive plugins denied by default; rate limits; every call audited. No apply/ approve/rollback tools exist at all.
  • Scope abuse: per-client scopes in mcp.toml; scope changes are admin actions.

3.6 Supply chain (CI, releases, VM harness)

Section titled “3.6 Supply chain (CI, releases, VM harness)”
  • Pinned toolchain, versioned Cargo.lock, cargo-deny/cargo-audit in CI, actions pinned by SHA with least permissions, no pull_request_target with PR code, SBOM + cosign signatures + provenance on releases, reproducible-build goal. Blocklist and VM image downloads verify checksums.
  • Snapshot before apply; rollback on failure, timeout, or crash (startup recovery reverts applying/pending_confirm).
  • Network-risk transactions require confirmation within a window (default 60 s) or they auto-revert.
  • Chaos tests are the acceptance bar: kill -9 mid-apply, resolver murder, VM reset mid-transaction, uninstall with active resources. After each: network and DNS still work, state consistent.
  • Irreversible ops (package.install, kexec_lock) carry flags, extra consent and explicit UI warnings; they are excluded from reversibility claims.
  • Precedence default < profile < user < org. Org locks are enforced in the core (not the UI) for every path: API, CLI, UI; violations return Locked {keys, source}.
  • Signed policy bundles: JCS canonicalization + ed25519, expiry checks, anti-rollback (version monotonicity), trust anchors in /etc/ibexwall/trust. The local provider requires the com.ibexwall.policy.provider polkit action (root/package rule only).
  • An attacker with root (or kernel compromise) can disable IbexWall. We are defense in depth for the user space boundary, not a hypervisor.
  • eBPF bypass scenarios exist (documented, like any host firewall); layered controls (eBPF + nft) and honest documentation instead of impossible guarantees.
  • Physical access, evil maid, firmware implants: out of scope beyond reporting Secure Boot/lockdown posture.
  • Multi-user machines with hostile local users: polkit allow_active semantics assume a single desktop user; server multi-user hardening comes post-v1.

The SEC-01..SEC-15 checklist in the engineering process maps 1:1 to the controls above; PRs reference the checklist item they touch (docs “definition of done”).

  • We never claim total protection; the README and website must match §6 and §8 of this document.
  • “Everything is reversible” applies to reversible-flagged operations only; irreversible ones are called out in UI and docs.

This document must be updated before merging: any new varlink method, polkit action, op type, plugin capability, network listener, download channel, or sandbox escape surface. F1 (socket, polkit, engine) is the first mandatory review point.

10. F1 review (socket, polkit subject, engine, supervisor)

Section titled “10. F1 review (socket, polkit subject, engine, supervisor)”
  • Socket: 0666 with per-method polkit; identity from SO_PEERCRED plus /proc start time. Polkit errors fail closed (call refused).
  • Validated in VM: unprivileged caller gets PermissionDenied (exit 3).
  • Engine: out-of-manifest ops refused at plan and re-checked at apply; kill -9 mid-apply is rolled back on restart; unconfirmed network changes auto-revert; audit tampering is detected.
  • Supervisor: manifest signature verified on every use (minisign format, dev anchor only; no embedded release key yet, so non-dev builds refuse all plugins).
  • Open items: pidfd subjects (ADR-0025), fuzz target for the varlink framing, rate limiting on the socket, stale plugin processes.
  • The daemon unit no longer writes /etc: every file-touching transaction (apply and rollback) runs in ibexwall-exec inside a transient unit whose ReadWritePaths name that transaction’s directories (or the nearest existing ancestor; the helper creates missing drop-in directories itself).
  • Validated in VM: an ops list that tries to write outside its declared directory fails with EROFS and the file is not created (s8).
  • This contains path-validation bugs. It does not contain a fully compromised daemon, which can still ask systemd to start arbitrary transient units; that residual risk is accepted and documented.
  • The ops and manifests the helper reads are core-written files under /run/ibexwall and /var/lib/ibexwall; the helper decides nothing (validation, snapshotting and auditing all happened in the core).
  • The resolver is a separate unprivileged process; it parses untrusted input (DNS packets, list files, list downloads). Lists are validated name by name, capped at 64 MiB and replaced by atomic rename; a download that does not parse or is empty never replaces a working list. Downloads are https only, with few redirects.
  • No query names are logged; only per-category counters exist. A test asserts the handler has no logging of names.
  • Upstreams are addressed by IP with a certificate name, trust anchors are bundled (webpki-roots), so reaching the upstream needs neither the host’s DNS nor its trust store.
  • FallbackDNS= is emptied in the resolved drop-in: a dead local resolver can never make the machine fall back to plain public servers.
  • Enforcement is an owned nftables chain (net_dns_guard) that refuses port 53 and 853 for every user except the resolver; loopback stays open. DoH on 443 cannot be blocked wholesale; it is addressed by the canary, the known-endpoints list refused by the stub, and browser policies that are written only with explicit consent and only for installed browsers. An application with its own DoH client and an unknown endpoint can still bypass DNS-level blocking; that is inherent and documented.
  • Risk: enforcement plus a dead resolver means no DNS. Mitigations: the resolver restarts on failure, three failures in 60 s run the failsafe (removes the chain and the drop-in, restarts the system resolver), a network-risk apply must be confirmed or it reverts.
  • Residual: a VPN client or a local application with its own resolver on loopback is not affected; per-link DNS of a VPN with routing domains can still win for those domains.
  • Captive portals: a portal blocks the encrypted upstream, so the login page may not load. The agent announces the portal with the relief steps (ADR-0030); enforcement is never lifted automatically, so a faked portal cannot weaken the machine.
  • What an assistant can reach. The server is a separate process run by the user, with no root and no network; it speaks to the core’s socket. Behind it, polkit decides: the user that runs an assistant needs only read, plan and proposals.create. A client that skips the server and calls the core directly gets the same refusals for apply, rollback, plugin management and proposals.decide (VM-tested as that user).
  • Writes. The only write path is a proposal. It is planned when created (the person sees real operations), bounded (20 pending, 8 per client), expires after a day, and is refused if the plan cannot be rolled back. Approval plans again and applies only if the operations match what was reviewed. Approval, rejection and creation are rows in the core’s audit chain naming the client.
  • Prompt injection. Text from outside, and the assistant’s own justification, is returned under untrusted and shown to people as quoted text marked as not checked. Control characters are refused in the justification and stripped in the core. A corpus runs through every read tool in CI. Residual: a model can be misled in what it tells the person; nothing here stops it from writing a misleading justification, which is why the plan, not the words, is what the person approves.
  • Exfiltration. Scopes per client, sensitive plugins closed and removed from answers, DNS history as counts, redaction to HMAC pseudonyms at one choke point, a size cap on answers, a rate limit. Residual: everything the scopes allow goes to the model the user connected.
  • Accountability. Every call is logged before it runs and the call is refused if the log cannot be written; lines are hash-chained. Residual: the log lives in the user’s directory, so the same user can delete it whole (see the report); the core’s chain is the one out of reach.
  • Availability. A flooding client is limited per process and, in the core, by the pending bounds. A request over 1 MiB is dropped unparsed.
  • Secrets in transit. A VPN profile carries a private key. It exists only in the plan (SQLite, root) and in the 0600 NetworkManager keyfile; previews, proposals and dry runs show “private file, N lines hidden” for any file written without group or other bits; the audit chain stores a digest of the ops, not their content. net.vpn is closed to every MCP client by default, so a key never passes through an assistant or its log.
  • Cutting the machine off. endpoint.isolate and the kill switch are network changes: the engine asks for a confirmation and reverts if nobody gives one (VM-tested). An empty isolation allow list needs an explicit lockdown. Chains sit ahead of the distro’s firewall, so another table’s accept cannot undo them; they are our own chains in our own table and rollback removes only them.
  • What the kill switch does not cover. It governs egress from this machine. Link-local traffic and the multicast groups neighbour discovery needs pass (they do not leave the link). A VPN server must be given as an address. Traffic that goes to the tunnel interface is trusted to be the tunnel: a local root can create an interface named wg*, which is outside the model (root can also remove the chain).
  • Hostile profile text. wg-quick hooks (PostUp, PreDown, Table, SaveConfig) are refused by name; every value is validated (keys, networks, endpoints, names) before it reaches a keyfile; names are limited to [a-z0-9-] so they are safe in a file name and an interface name.
  • Scanners and verifiers on hostile files. Fixed argv, no shell, a time limit, paths limited to plain characters before they enter a unit; file and signature names are Fluent arguments only. ClamAV and YARA parse attacker-controlled files, so the scan unit is sandboxed (ProtectSystem=strict, NoNewPrivileges, PrivateDevices, read-only home); under SELinux the distro’s own confinement of clamscan applies and the check says when it blocks scanning. Residual: the scanners run as root to be able to read what they scan.
  • Honest limits. Integrity and persistence are as good as the package manager’s database and the baseline the person accepted: an attacker who edits both is outside what a check on the same machine can see (AIDE with a database kept elsewhere, or a remote attestation, is the answer, and neither is in v1). The daemon’s hardened unit hides parts of the filesystem from plugins run as its children; own units per plugin (F10) lift that.

15. The HTTPS transport and the rule “nothing in the clear” (ADR-0042)

Section titled “15. The HTTPS transport and the rule “nothing in the clear” (ADR-0042)”
  • Front door. TLS 1.2 and 1.3 only, ALPN http/1.1, no plain listener and no flag to make one; a private key readable by others stops the server. Connections are capped (64), headers and bodies are bounded (1 MiB), every wait has a limit, and keep-alive is off.
  • Who may come in. A Bearer token ibx_<id>_<secret>: the id selects one record, the secret is checked against an Argon2id hash (a decoy hash runs for an unknown id so timing does not tell ids apart); expired tokens are refused; failed sign-ins are counted per address and the door closes past a limit (429), so the hash cost cannot be used to exhaust the machine. The Origin header must be allowed (none are by default), which keeps a web page from driving it.
  • What a token can do. Exactly what its client’s scopes allow, through the same pipeline as stdio (scopes, rate limit, audit line before the work, redaction, untrusted objects); the server runs as its own user in the assistants group, so polkit limits it to read, plan and propose.
  • Accountability. The audit chain records via: http:<token id>; the token secret appears in no log (verified in the VM, journal included).
  • Residual. A stolen token works until it is deleted from mcp.toml or expires; there is no per-request signature. A self-signed certificate is only as good as the fingerprint check on the client. Compromise of the machine running the server exposes the answers it can give, not the ability to apply changes.
  • Guard. xtask/src/egress.rs fails the build on a plaintext http:// address in production code, on a network listener outside the two known ones (the loopback resolver and the MCP server), and on a plain mode in the MCP transport.
  • Unencrypted DNS on a local network is allowed only for local-range addresses, announced to the person as information, and routed per domain; it protects nothing against someone on that network, which the message says.

16. Packaging: the release key and the uninstall

Section titled “16. Packaging: the release key and the uninstall”
  • Release key. The daemon is built with the release public key embedded (IBEXWALL_RELEASE_PUBKEY, crates/ibexwall-core/build.rs). It verifies the manifest signature of official and community plugins against it, and trusts a local plugin only with an explicit --dev-key. A build without a key trusts no plugin by itself. A packaged install therefore runs exactly the plugins it shipped: a tampered manifest is refused (checked in the VM matrix). The private key never enters the repository; cargo xtask stage --key-dir takes it from the maintainer’s directory at release time, and until then the test packages are signed with the development key.
  • Install. The maintainer scripts only create users and directories, switch on sockets that cost nothing until the core connects, and start them. They apply no IbexWall change: every change still needs a plan, a snapshot and the person’s approval.
  • Uninstall. ibexwalld --cleanup runs before the files are removed. It rolls back each transaction still in effect, newest first, through its own snapshot, and removes what is left of IbexWall’s nftables table. It cannot be pointed at anything else: the snapshots hold only what IbexWall changed. A transaction that cannot be undone is reported and does not stop the rest, and the removal itself does not fail. The user’s data stays until a purge.