{"protocol":"clank-doc/1","frameworkVersion":"0.24.0","slug":"linux-host-certification","title":"Linux host certification","description":"@clank.run/framework/host certification runs real enforcement probes for a captured, static Docker deployment profile. Operators explicitly select a disposable Linux host with its own local Docker daemon and dedicated XFS mount. The framewo","group":{"id":"security","title":"Security and resilience"},"url":"https://docs.clank.run/docs/linux-host-certification","source":"docs/linux-host-certification.md","headings":["Prepare an explicit disposable profile","Certify and inspect","What passes each check","Freshness, interruption and recovery"],"tableOfContents":[{"id":"prepare-an-explicit-disposable-profile","title":"Prepare an explicit disposable profile","level":2},{"id":"certify-and-inspect","title":"Certify and inspect","level":2},{"id":"what-passes-each-check","title":"What passes each check","level":2},{"id":"freshness-interruption-and-recovery","title":"Freshness, interruption and recovery","level":2}],"markdown":"# Linux host certification\n\n`@clank.run/framework/host-certification` runs real enforcement probes for a captured,\nstatic Docker deployment profile. Operators explicitly select a disposable Linux host\nwith its **own local Docker daemon** and dedicated XFS mount. The framework does not\nformat disks, install tools, pull images or substitute a trusted process runner.\n\nA versioned report is green only when all seven checks pass, including verified cleanup.\nThe private operator report directory authenticates saved reports with a local 32-byte\nkey. Inspection rechecks the policy digest, host binding and expiry. Treat this directory\nand its key as operator credentials; application tenants must never access them.\n\n## Prepare an explicit disposable profile\n\nUse Node 22.16+, Bubblewrap, util-linux, nftables, iproute2, procps, XFS tools and Docker.\nV1 passing profiles require an operator process with real/effective UID 0, so every\nprivileged attempt uses the same root-owned host-wide lock. Delegated capability profiles\nare unsupported; an unprivileged diagnostic attempt can only produce a blocked result.\nPreload an operator-selected Node image, then use its immutable `@sha256` reference.\nMount a **new disposable** XFS filesystem with project accounting/enforcement (`prjquota`).\nNever point this proof at a shared Docker daemon, live project or a filesystem that may\ncontain someone else's quota allocations. Reserve a previously unused nonzero project\nID; certification refuses a quota ID with existing usage or limits.\n\nV1 proves the selected byte/inode ceilings together. Supported persistent byte limits\nare 4–64 MiB, aligned to 1 KiB; inode limits are 32–128. The mount needs at least three\ntimes the selected byte limit available for inside/outside positive controls. Larger\nproduction quotas require a separately supported certification profile; a small profile\ncertificate cannot authorize a different large policy. These ceilings bound the proof's\nallocation, rather than pretending to test an unlimited filesystem.\n\nContainer memory is an explicit `m`/`g` value from 128 MiB to 2 GiB; CPU limits are\n0.1–4 with at most three decimal places, and PID limits are 16–32,768. The probe verifies\nDocker's exact configured memory, swap, CPU and PID values as well as runtime restrictions.\n\nThe outbound policy uses the same validated public IPv4 CIDRs and pinned static hosts\nas the Docker launcher. Supply one controlled allowed address within the policy and one\ncontrolled denied public address outside it. An empty allowlist omits the allowed address.\nV1 cannot certify a policy that permits every public destination, because its required\ndenied-public positive control would have no valid address. Private, metadata and host\ndestinations remain denied even when a CIDR includes them.\n\nDuring the proof, these addresses route to a newly owned local network namespace; requests\ndo not contact their real external servers. The proof temporarily creates exact owned\nroutes/link/firewall/network/container resources on the disposable host. It restores IP\nforwarding to its prior value and verifies cleanup before publishing a passing report.\nThis samples representative traffic and static names pointing to the selected address;\nit does not attest the availability of every destination or an external DNS provider.\n\n## Certify and inspect\n\n```ts\nimport {\n  certifyLinuxHost, requireCurrentLinuxHostCertification,\n  type LinuxHostCertificationProfile,\n} from \"@clank.run/framework/host-certification\";\n\nconst profile: LinuxHostCertificationProfile = {\n  mode: \"docker-isolated\",\n  image: \"node@sha256:<replace-with-the-preloaded-64-hex-digest>\",\n  user: \"1000:1000\",\n  memory: \"512m\", cpus: \"1\", pidsLimit: 128,\n  diskQuota: {\n    mountDirectory: \"/disposable-xfs\",\n    hardBytes: 32 * 1024 * 1024, hardFiles: 64,\n  },\n  outboundNetwork: {\n    allowCidrs: [\"1.1.1.1/32\"],\n    hosts: { \"allowed.example.test\": \"1.1.1.1\" },\n  },\n  networkProbe: { allowedAddress: \"1.1.1.1\", deniedAddress: \"9.9.9.9\" },\n};\nconst selected = { directory: \"/root/clank-certification\", profile };\nconst report = await certifyLinuxHost({\n  ...selected, disposable: true, quotaId: 2147481001, ttlMs: 60 * 60 * 1000,\n});\nconsole.log(report.status, report.checks);\nawait requireCurrentLinuxHostCertification(selected); // Throws for every blocked/stale result.\n```\n\nThe API report contains digests and fixed capability/reason codes; it does not expose\nhost paths, image references, command output or application secrets. Invalid API/profile\ninput and unsafe report storage throw before a successful report is returned. Operational\nprobe denials produce a blocked report. Missing/unusable foundational paths, metadata\nwrites and failures to finish final cleanup can also throw; they never produce green.\n\nSave `{ \"directory\": \"…\", \"profile\": { … } }` as a bounded regular JSON file, then use:\n\n```sh\nclank-provider certify --config profile.json --quota-id 2147481001 --disposable\nclank-provider certification --config profile.json\n```\n\nBoth commands print JSON. Certification returns exit code 1 for a blocked report;\ninspection returns 1 for missing, active, invalid, expired, changed or blocked reports.\nThe inspection command does not start a provider or require its bearer token. Existing\nproviders remain compatible: requiring certification is an explicit admission decision,\nnot an implicit change to current deployment behavior.\n\n## What passes each check\n\n| Capability | Required evidence |\n| --- | --- |\n| `namespaces` | Real user/mount/network namespace creation, mapped UID and a distinct network namespace. |\n| `migrations` | Actual isolated SQLite migration succeeds; a later failed transaction preserves data and ledger, then the worker recovers. |\n| `sqlite-worker` | Endless SQL is terminated by the fixed worker bound while the parent serves timers; partial writes roll back and another request succeeds. |\n| `disk-quota` | Exact selected byte/inode denial, outside-quota writes, failed SQLite/WAL allocation and atomic rollback. |\n| `runner` | Production Docker launcher, selected immutable image/non-root user/resources, zero effective capabilities, no-new-privileges, read-only code/root, writable data, absent host secret and bounded tmpfs. |\n| `egress` | Controlled allowed/denied/private/metadata and host destinations are reachable before enforcement; selected policy then permits only the allowed control, with IPv6 disabled. |\n| `cleanup` | Child exit, no owned containers/network/firewall, removal of owned routes/link/files, zero owned quota usage and released limits, restored host binding. |\n\nNo caller can supply a callback or arbitrary executable to manufacture a probe result.\nCommands have fixed executables, clean environments, one-MiB output bounds and deadlines;\nSQLite retains its existing ten-second execution bound. Cancellation is observed between\ncapabilities and during privileged commands. An already running SQLite task completes or\nhits its fixed bound before cancellation cleanup; abort does not turn a skipped check green.\n\n## Freshness, interruption and recovery\n\nReports live for one hour by default, configurable from one minute to 24 hours. Inspection\nuses wall time and same-boot uptime, rejecting backwards clock movement and expiry. Host\nbinding includes boot/kernel/architecture, UID/groups/capabilities, relevant namespace and\nforwarding settings, selected mount identity/options, framework/runtime/enforcement-tool bytes, local Docker\ndaemon configuration and resolved image identity. Changes require a fresh certificate.\n\nThis is an expiring measurement at a point in time, not continuous monitoring or a promise\nthat privileged operators cannot change the host later. Arbitrary external policy files,\nevery possible resource-exhaustion behavior and all network destinations are not attested.\nKeep the profile identical when requiring current certification, and maintain normal host\nsecurity and deployment fencing. A locally compromised operator can replace the key and\ncode; local authentication does not claim protection from the host administrator.\n\nAttempts are exclusive per private directory and, for root, across the host through\n`/run/clank-host-certification.attempt`. The old report is invalidated before probing. A\nroot inspection also rejects this host-wide marker, including attempts using another\nprivate report directory, before reading a certificate and again after host sampling. A\ncompleted report is atomically renamed; a lost response can be recovered by inspection in\na new process. A forced process death leaves an attempt marker, so inspection remains\nblocked even if an earlier report existed. Attempts are never automatically unlocked by\nage or recycled into another live quota assignment.\n\nOn a forced interruption, an operator must first confirm the recorded PID/boot is no longer\nactive and clean the exact owned resources listed in the private attempt record (scratch,\nproject/owner label, link, controlled routes and reserved quota). Verify child/container\ntermination, firewall/network removal, original host policy and zero quota usage before\nremoving either attempt marker. Failed or uncertain owned cleanup retains both markers\nfor explicit operator recovery. Prefer disposing of the entire test VM after interruption.\nDo not clear a marker while a live attempt can still mutate the host. Completed attempts\nremove their markers automatically; failed verification remains blocked. V1 supplies no\nautomatic crash cleanup that could mistake another process or project for its own.\n\nPersistence is additive private key/report/attempt metadata; application databases and\nprovider control stores are unchanged. Rollback removes this optional admission requirement\nand its metadata only after owned attempts are stopped and cleaned. Quota IDs are never\nsilently borrowed from existing projects. Runtime remains dependency-free.\n"}