Template copy. This is the in-repo TEMPLATE copy of the whitepaper. The live demo fills the reproducibility block (image digests, hashes, deploy date) at deploy time. The live site may run an older published bundle than the newest repo commit — the block on the live site describes the running system.

The netcanon demo never keeps your config — here's the proof

Network configs are sensitive. A running-config can contain your addressing plan, SNMP strings, usernames, and topology. So the netcanon demo runs your session in a throwaway instance that self-destructs within 15 minutes, built so that retaining your data is not just against policy — it is architecturally unavailable. This page maps every claim to the exact control that enforces it and a procedure anyone can use to verify it.

The entire deployment is open source: github.com/netcanon/netcanon. The running system is reproducible from that repo, and its image digests, lockfile hash, and compose-file hash are published in the reproducibility block below.

This document is the in-repo template. The reproducibility block carries placeholders that the deploy step fills with the exact digests, hashes, and date of the running bundle. The live copy at /whitepaper describes the running system; this repo copy describes the mechanism. Every procedure below is a copy-paste command in the repo's VERIFY.md.

Threat model

We defend the visitor's pasted data against: (a) operator retention (us, by design), (b) cross-visitor leakage, (c) post-session forensic recovery from the host, (d) log-based leakage at any layer, (e) a compromised or buggy demo application instance. Out of scope: a fully compromised host kernel/hypervisor (no hosted demo can defend that — if your config is that sensitive, run netcanon locally: docker run --rm -p 8000:8000 ghcr.io/netcanon/netcanon; we make that path one click away).

Trusted Computing Base

Every claim below rests on a small set of components we ask you to trust: the warden (session manager + reverse proxy), the socket-proxy + create-body authz shim (the capability filter that fronts the Docker socket), Caddy (TLS termination), dockerd (the container runtime), and the host kernel/OS. These are the TCB. A compromise of any of them — most sharply the warden, which reaches the Docker socket — is host root, and host root voids all ten claims below. We do not pretend otherwise; we make the TCB as small and as auditable as we can:

(demo/warden/app.py, source: github.com/netcanon/netcanon).

a socket-proxy / authz filter that permits just create, start, list (GET /containers/json, including label filters), inspect, and remove — denying everything else (exec, commit, build, images, networks, volumes, …), not the arbitrary, root-equivalent Docker API. Because the socket proxy cannot inspect a create body, a small create-body authz shim (demo/warden/authz_shim.py) in front of it enforces a whole-body default-deny allowlist on every POST /containers/create: the raw wire body may contain only the fields of the canonical create-body template (the spec in demo/warden/constants.py serialized through the pinned Docker SDK), each pinned to that template (image digest, read-only rootfs, the tmpfs set, no bind mount, network mode, memory/pids/cpu caps, CapDrop: [ALL] with no CapAdd, security-opts) — any other field, in any letter case, is rejected, so an added Privileged, CapAdd, bind mount, or NetworkMode: host cannot slip through. Only the demo.* labels and the two per-instance random env keys may differ. This shim is counted in the Trusted Computing Base alongside the ≤ 500-line warden. (The case-fold is load-bearing: Docker's Go JSON decoder matches struct fields case-insensitively, so the allowlist folds every key to lower case before checking — a lowercase binds cannot smuggle a host mount past it.)

tmpfs; its only contact with your instance is proxying HTTP to it.

small VPS whose configuration lives in the same public repo.

If you cannot extend that trust, the honest answer is the one on the front page: run netcanon locally with docker run … and trust nothing of ours.

Claims, controls, proofs

# Claim Control Proof (abridged; full: VERIFY.md) Tier
1Your session runs in its own isolated instanceWarden creates one container per session; routing is by the HttpOnly nc_route cookie — a bearer credential valid for the life of the session, not a single-use token; instances unreachable except via the wardenTwo browser profiles: each header shows a distinct instance-id chip (instance_id, a display id — not the routing token); end session A → A's now-dead cookie 404s every request while B is unaffectedV·O
2The instance cannot write to diskread_only: true rootfs; both declared VOLUME paths (/app/data and /app/configs) backed by tmpfs (RAM), so no anonymous host-disk volume is ever created; zero named or anonymous volumesdocker inspect shows ReadonlyRootfs=true and Mounts = tmpfs only; docker diff clean outside tmpfs; docker volume ls empty after teardownO
3Nothing survives the sessionTeardown = container.remove(v=True, force=True) (container and any anonymous volume); tmpfs freed by kernel; the hard TTL is enforced across two independent enforcement domains (the warden and host systemd), three mechanisms — (1) the warden's in-RAM reaper at deadline = assignment_time + HARD_TTL (900 s from assignment), (2) the warden's startup label-sweep, and (3) an independent host systemd timer (swept every 60 s) that force-removes any demo.*-labeled container older than HARD_TTL + POOL_MAX_AGE = 1200 s even if the warden is dead; sooner in practice via the 10-min idle reclaim (sooner under heavy load), pagehide sendBeacon, and heartbeat timeout (≤ 2 min for a closed foreground tab, ≤ ~4 min for a throttled background tab)Destroy → docker ps -a and docker volume ls empty of it; kill the warden mid-session → the systemd timer still removes it within ~20 min of creation; canary paste + host-wide grep finds nothing post-teardownO
4Your paste appears in no log, anywhereInstance log driver none; warden logs metadata-only (schema enumerated) and never interpolates a request body — including in exception handlers / error paths; Caddy access logging disabled for the whole demo vhost (log { output discard }); journald volatile (RAM)Canary-string grep across journald + filesystems; warden log-schema test asserts no body field on any path, error paths includedO
5RAM contents can't leak to disk (swap or core dump)Swap disabled host-wide; memswap_limit == mem_limit; core dumps disabled host-widekernel.core_pattern set to discard, systemd-coredump Storage=none + ProcessSizeMax=0, apport neutered, docker.service LimitCORE=0swapon --show empty; inspect shows limits equal; cat /proc/sys/kernel/core_pattern shows the discard sink; force a crash → no core file writtenO
6The instance can't phone anywhereInstances on an internal: true network (no egress); ICC is all-or-nothing, so isolation is enforced by explicit nftables rules: warden→instance ALLOW, instance→instance DENY, instance→warden DENYFrom inside an instance: every outbound connect fails; instance→instance fails; instance→warden failsO
7Even netcanon's encryption key is never written to diskThe warden injects a per-instance random NETCANON_FERNET_KEY (Tier-1 of netcanon's key resolution), so the file fallback at data/.fernet_key is never reached — no Fernet key file is ever created; the key lives only in the instance's RAMFrom the host via the raw host Docker socket (not the warden's path — its allowlist forbids exec), docker exec/docker inspect shows the env key set and no .fernet_key on disk (/app/data is tmpfs regardless); gone with the instanceO
8The demo can't silently degrade isolation under loadHard instance cap (MAX_ACTIVE = 32); per-IP concurrent-session cap (PER_IP_MAX_CONCURRENT = 2); above 80% occupancy the idle TTL tightens (600 s → 300 s, applied retroactively within ~10–20 s of the crossing, loosening back below 70% — hysteresis), reclaiming untouched tabs faster while the 15-min hard ceiling is left intact; the longest-idle (never-translated) session is reclaimed before anyone is refused — but never one younger than 120 s (a mid-paste session is protected; if every session is under that floor the demo returns 503 rather than reclaiming one); at the cap the demo returns 503 rather than sharing an instanceSaturate the cap; observe 503 + busy UI, never a shared instance (the third concurrent session from your IP is refused — that you can see)O
9The published deployment is exactly reproducible (and the live host is operator-attested to match)Images pinned by digest; requirements.lock hash-locked; compose SHA-256 publishedDigests + hashes below; make verify reproduces them from the repo — it attests the published artifact, not this live host (no hosted demo can remotely prove its own runtime; that is what the local docker run path is for)O
10No tracking on the demo pageThe page sets no cookies, no localStorage, no analytics, no third-party requests; the only cookie in play is the warden's HttpOnly, SameSite=Strict, Secure session-routing cookie — a bare instance binding, not an identifier (disclosed under "What we do see")View source (self-contained, all CSS/JS inline); devtools Network shows no third-party requests and Application → Cookies shows only the routing cookieV

Tier — how far you have to trust us. V = visitor-verifiable now: you can confirm it from your own browser, on the live site, trusting nothing of ours. O = operator-attested + locally reproducible: confirming it needs host access, so we commit the exact command output in VERIFY.md — and, because no hosted demo can remotely prove its own runtime, you can reproduce every O row yourself by running the identical stack locally with docker run …. make verify attests the published repo/artifact, not this live host. A row marked V·O carries both components: claim 1 is the case — you can watch the instance-id chip differ and the dead-cookie 404 from your own browser (V), while "never a shared instance" is attested at the container boundary (O).

What we do see

Honesty section — required. We (the operator) can observe: standard host operational metadata (session count, lifecycle timestamps, destroy reasons, status codes), our host provider's infrastructure-level traffic metadata, and whatever a TLS-terminating proxy inherently sees in memory while streaming your request. Two more things, stated outright:

(fetch('/api/v1/migration/plan'), href="/migrate") and has no root-path support, so the warden routes you to your one instance by session, not by path. To do that it sets a single cookie — Set-Cookie: nc_route=<token>; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=900 — carrying only the opaque session token. That token is a bearer credential valid for the life of the session (not single-use), which is exactly why the cookie is HttpOnly, Secure, and SameSite=Strict — JS cannot read or forge it. It is not an analytics identifier and it dies with your session. Because the cookie is browser-global, a browser holds exactly one live session (a fresh mint replaces the previous one); the ≤ 2-concurrent-sessions-per-IP cap is a separate guardrail bounding distinct devices behind one IP. The demo page itself still sets nothing (claim 10).

concurrent-session cap (≤ 2 concurrent sessions per IP) and a sliding-window mint rate limit (≤ 30 mints / 600 s per IP), the warden keeps a small per-IP record in memory only — never logged, held in memory for up to 10 minutes after your last request, never written to disk, gone on warden restart. A rate limiter must outlive any single session, so the record is evicted 10 min after your last request rather than on session end.

We keep none of the in-memory transit category — the controls above are what make that checkable rather than a promise. If you cannot accept in-memory transit through our proxy, run netcanon locally; that path is on the demo's front page.

Reproducibility block

The deploy step fills these tokens for the running bundle. In this repo template they are placeholders; the live /whitepaper shows the real values, and make verify (run from deploy/) prints the same digests + compose hash for you to compare.

netcanon image    : ghcr.io/netcanon/netcanon@sha256:<NETCANON_IMAGE_DIGEST>   (base python:3.14-slim-bookworm)
requirements.lock : sha256:<REQUIREMENTS_LOCK_SHA>   (hash-locked; glibc/manylinux wheels — not Alpine/musl)
warden image      : <WARDEN_IMAGE_REF>@sha256:<WARDEN_IMAGE_DIGEST>
socket-proxy image: <SOCKET_PROXY_IMAGE_REF>@sha256:<SOCKET_PROXY_IMAGE_DIGEST>   (TCB component — the capability filter fronting docker.sock)
caddy image       : <CADDY_IMAGE_REF>@sha256:<CADDY_IMAGE_DIGEST>
compose sha256    : <COMPOSE_SHA256>
deployed          : <DEPLOY_DATE>   hard TTL: 900s (15 min)   idle TTL: 600s (10 min)   instance cap (MAX_ACTIVE): 32

Residual risks, stated plainly

Trusted Computing Base — the honest worst case

The sharpest residual risk is not exotic: it is us. The warden, the socket-proxy + authz shim, Caddy, dockerd, and the host are trusted (see the threat model's TCB subsection). A compromise of the warden — which holds the Docker socket — is host root, and host root voids all ten claims above: it could snapshot RAM, disable the reaper, or read into any instance. We bound (not eliminate) this by keeping the warden tiny (≤ 500 auditable lines), non-root, read-only, reachable to Docker only through a capability-filtered socket proxy plus a small create-body authz shim that whole-body default-deny-validates every POST /containers/create against the canonical instance spec (both counted in the TCB), so create is not the arbitrary, root-equivalent Docker API — and never execing into instances — all of it in the public repo. If that residual is unacceptable for your data, run netcanon locally and trust none of it.

guarantees (out of scope in the threat model).

heartbeat-timeout path reclaims your instance in ≤ 2 min for a closed foreground tab, ≤ ~4 min for a throttled background tab, and a tab left open but untouched is reclaimed at the 10-minute idle TTL (sooner under heavy load). Both of those only ever shorten the session. The guarantee that requires nothing from your browser — no beacon, no heartbeat, no activity — is the hard TTL, enforced across two independent enforcement domains (the warden and host systemd), three mechanisms, with two honest bounds: while the warden is live it destroys your instance ≤ 15 minutes after it was assigned to you; and even if the warden crashes, an independent host systemd timer (swept every 60 s) force-removes any demo.*-labeled container ≤ ~20 minutes after it was created (older than HARD_TTL + POOL_MAX_AGE = 1200 s). A crash can therefore widen the ceiling from 15 to at most ~20 minutes — it can never lift it; nothing keeps your instance alive indefinitely.

trust the artifact you can verify today, not us.