Skip to content

Threat Model and Residual Risks

CYPT71 edited this page Aug 9, 2026 · 1 revision

Threat Model and Residual Risks

Version 2.0, reviewed 2026-08-04 (supersedes v1.0, 2026-07-27). This model covers source change through build, OCI layout, the project's own native Registry/SBOM/provenance/signing path, Kubernetes admission, the distributed control-plane/worker system, out-of-process plugins (Go/Python/JavaScript/ TypeScript/C#), the content-addressed cache, and native VMM execution (KVM today; HVF/WHPX in progress). It does not claim organizational certification.

What changed since v1.0: v1.0 was written for the v1-v3, GHCR+Cosign- centric architecture. Since then the project replaced Cosign/Fulcio/Rekor as the primary signing and publication path with a native one it owns (internal/signing — Go crypto/ed25519/ECDSA, no external CLI; internal/registry — native OCI Distribution client); external tooling is now retained only as an interoperability check in CI, never in the product path. v4 added a native VMM (internal/hypervisor/{kvm,hvf,whpx}, internal/microvm) as a third isolation mode alongside plain containers and the existing process sandbox. v5 added a distributed control-plane/worker system (cmd/platform-factory-control-plane, cmd/platform-factory-worker, internal/mtls, internal/control, internal/quota) and a local content-addressed cache (internal/cache). The plugin ecosystem grew from a single Go SDK to five languages with real importable SDKs (sdk/plugin-{python,js,csharp}). This revision folds all of that into the register below; it does not yet produce the eight separate per-component deep-dive documents the stabilisation roadmap calls for (builder, sandbox, plugins, Registry, publication, microVM, distributed scheduler, CAS each as their own page) — see "Open work" at the end.

Assets and security objectives

Asset Objective
Source and workflows only reviewed changes execute with privileged CI permissions
Input executable packaged bytes equal the approved build output
OCI layout authentic descriptors, no path escape, deterministic bytes
Published image immutable digest maps to validated content, signed by a key this project controls
Signature identity only the designated signer (CI workflow identity or an operator's own key) is trusted
Cluster admission unsigned or wrongly signed images are denied
Workload non-root, least privilege, read-only root, bounded resources
Evidence enough provenance to reconstruct the decision to deploy
Control-plane state only certificates bearing the correct role authenticate as worker/operator; state survives restart undamaged
Tenant quota/budget one tenant cannot starve another of scheduler capacity or wall-clock budget
Content-addressed cache a cache entry's digest is recomputed and checked before reuse, never trusted on lookup alone
Plugin host a plugin (any of five languages) cannot read host secrets, escape its filesystem/network namespace, or exceed its resource limits
MicroVM guest/host boundary a compromised guest cannot reach host resources through virtio devices or the KVM/HVF/WHPX interface

Actors, trust boundaries, and assumptions

Potential adversaries include an untrusted pull-request author, a compromised developer workstation or token, a malicious binary supplier, a registry attacker, a network attacker, a compromised GitHub Action or runner, a cluster user, an attacker already executing inside the workload, a malicious or buggy out-of-process plugin (in any of the five supported languages), an attacker impersonating a worker or control-plane peer, a tenant abusing shared scheduler capacity, and an attacker who has broken out of the guest OS in a MicroVM but not yet the hypervisor.

Explicitly out of scope: a nation-state adversary with a hardware supply chain implant in the CI runner or host CPU; a break of Ed25519 or SHA-256 themselves; a kernel 0-day in the host used to escape KVM/HVF/WHPX (mitigated by choosing the MicroVM isolation mode, not eliminated by it); a malicious Go toolchain release (mitigated by pinning, not eliminated); physical access to build or cluster infrastructure.

Trust boundaries are: developer → GitHub; untrusted PR → CI; compiler → builder; builder → independent verifier; build job → publishing job; published image → native signature verifier; Kubernetes API request → admission webhook; admitted pod → host kernel; control-plane ↔ worker (mTLS, role-checked via internal/mtls.HasRole — chaining to the trusted CA is not sufficient, the certificate's Organization must also declare the expected role); host process → out-of-process plugin (fresh user/network/IPC/UTS namespace plus rlimits, see T21); MicroVM guest → host (KVM/HVF/WHPX boundary plus the virtio device surface in internal/hypervisor/virtio).

We assume GitHub OIDC claims, SHA-256, Ed25519, the Go toolchain, runner isolation, Kubernetes authorization, the mTLS CA's private key is not compromised, and the host kernel/hypervisor behave according to their documented security properties. Where Cosign/Fulcio/Rekor are still used (CI interoperability proof only, not the product path — see "What changed" above), we assume they behave according to their documented properties too, but a Cosign compromise no longer affects the primary signing path.

TCB by execution mode

platform-factory run supports three isolation modes with different trusted computing bases; picking the wrong one for a given threat model silently under-protects the workload.

Mode Isolation mechanism Host-level sandboxing of the isolation layer itself Residual risk
container Docker/Podman namespaces + cgroups (delegated to the chosen runtime) Provided by the runtime, not this project shared kernel; only as strong as the operator's Docker/Podman config
plugin/stage sandbox (internal/plugin, internal/executor) fresh user/mount/PID/network/IPC/UTS namespaces, rlimits, filesystem read-only remount Applied by this project directly (sandbox_linux.go) shared host kernel; Linux-only, falls back to unsandboxed elsewhere (see T21)
microvm (KVM today; HVF/WHPX in progress) hardware virtualization — separate kernel per guest Not currently applied. cmd/platform-factory-runtime self-reports "seccomp":{"enabled":false} and empty namespaces/cgroups in its OCI runtime feature announcement; internal/hypervisor/sandbox defines Config{Seccomp, Namespaces, Cgroups, DropPrivileges} but is not wired into the VMM process's own startup path (confirmed by grep: no caller outside its own package/tests) if the VMM process itself is compromised (not the guest — the host process emulating virtio devices), it runs with none of the belt-and-suspenders hardening the guest boundary would otherwise get; guest-to-guest and guest-to-host isolation still rests entirely on KVM hardware virtualization, which is intact

The MicroVM mode's guest isolation is real and independent of this gap (KVM hardware virtualization does not depend on internal/hypervisor/sandbox). The gap is specifically: if an attacker finds a bug in this project's own VMM host process (e.g. in virtio device emulation, internal/hypervisor/virtio), today nothing extra stops that compromised host process from acting with the runtime's full ambient privileges. Closing T22 (below) is the fix.

Threat and control register

STRIDE: Spoofing, Tampering, Repudiation, Info disclosure, DoS, Elevation of privilege. Rows T01–T20 are the v1.0 register, carried forward; T09–T11 are revised for the native signing/Registry path. T21–T30 are new for v4/v5.

ID Threat STRIDE Initial risk Preventive/detective controls Residual risk
T01 malicious PR obtains a write token E critical no pull_request_target; default read permissions; privileged jobs only on trusted triggers; workflow policy test low: GitHub or action isolation failure
T02 action/tag is retargeted T high actions SHA-pinned, narrow bootstrap allowlist, Dependabot review low: compromised pinned action commit
T03 compiler or dependency compromise T high standard-library builder, pinned Go version, CodeQL, govulncheck, reproducible rebuild evidence medium: both rebuilds share toolchain/runner lineage
T04 malicious or replaced input binary T critical CI builds from reviewed source; ELF verification; checksums; provenance outside layout medium: builder packages any executable the caller supplies
T05 tar path traversal or special file T,E critical clean absolute destination validation; regular-file checks; independent layout verifier; negative mutations low: parser discrepancy or unknown format edge case
T06 partial/corrupt layout accepted T high fresh temp output, atomic rename, descriptor size/digest validation low: storage corruption after verification
T07 nondeterministic hidden host input I high fixed metadata; no environment, credentials, source tree, or host CA copying; rebuild comparison low: toolchain or filesystem implementation variance
T08 artifact swapped between jobs T high named artifacts, digest evidence, re-verification before publish medium: Actions artifact service compromise
T09 mutable registry tag substituted T critical deploy and sign by digest; native Ed25519 signature verification (internal/signing) checked before use; admission pattern requires digest low: registry availability or SHA-256 break
T10 unauthorized signer accepted S critical verification is against a specific public key the operator pins (KeyStore.PublicKey), not an open trust root; CI's own key is generated/persisted per internal/signing's KeyStore, scoped to that workflow medium: whichever process holds the private key (CI runner, or an operator's local filestore/macOS Keychain) is now the entire trust root — no transparency-log cross-check by default the way Rekor gave v1.0
T11 signature verification unavailable or skipped D medium platform-factory publish fails closed without --allow-incomplete-evidence; --sign/--sbom required for production per CLI validation medium: --allow-incomplete-evidence is a real, reachable escape hatch — a misused or scripted flag silently drops the guarantee this row exists to give
T12 admission controller bypass E critical fail-closed webhook, namespace opt-in validation, RBAC separation, negative unsigned test medium: cluster-admin can change/bypass policy
T13 tag-to-digest race T high Kubernetes manifest uses immutable digest; admission matches digest low
T14 privileged runtime or writable root E critical production security context, non-root config, capability drop, seccomp, no service token medium: consumer can omit external runtime settings
T15 container escape E critical minimal image, seccomp/capability controls; microVM option for stronger isolation medium: shared host-kernel vulnerability
T16 secrets exposed in image/log/artifact I high builder never reads secrets; ephemeral registry login; evidence excludes tokens medium: application binary may embed secrets
T17 denial of service via huge payload D medium CI timeouts, resource limits, operator input controls medium: builder currently has no explicit maximum input size
T18 incompatible OCI consumer D medium Docker/containerd/Skopeo CI with recorded versions medium: untested runtime/version or implementation divergence
T19 benchmark regression unnoticed D low repeatable benchmark artifacts and payload-size comparison medium: no hard gate due shared-runner noise
T20 vulnerable application packaged correctly critical out of scope for packaging; SBOM/provenance and consumer scanning required high: secure packaging does not make payload code secure
T21 out-of-process plugin (Go/Python/JS/TS/C#) reads host secrets, escapes its filesystem, or exhausts resources I,E,D high internal/plugin.Start relaunches the plugin in fresh user/network/IPC/UTS namespaces (sandbox_linux.go); project root remounted read-only; sensitive directories masked with tmpfs; RLIMIT_CORE=0, RLIMIT_FSIZE=16MiB, RLIMIT_NOFILE=256, RLIMIT_CPU=60s; environment fully replaced, never inherited from host; protocol carries no secret-typed capability medium: sandbox falls back to unsandboxed launch where namespaces are unavailable (non-Linux hosts, or Linux hosts without user-namespace support); no memory limit (RLIMIT_AS was tried and reverted — it broke Go runtime startup, see roadmap) so a plugin can still exhaust host memory
T22 native VMM host process (not the guest) is compromised, e.g. via a bug in virtio device emulation E high KVM hardware virtualization isolates the guest regardless of this gap high, openinternal/hypervisor/sandbox's seccomp/cgroups/privilege-drop is not wired into cmd/platform-factory-runtime's actual startup path (self-reported "seccomp":{"enabled":false}); a compromised VMM host process today runs with full ambient host privileges, not just guest-equivalent ones. See "TCB by execution mode".
T23 worker or operator identity spoofed against the control-plane S critical mTLS with role-checked certificates (internal/mtls.HasRole requires both a valid chain to the CA and the correct Subject Organization) medium: any certificate the CA issues for a role can act as that role — compromise of the CA's signing key, or an over-broad issuance process, defeats this
T24 tenant exceeds its fair share of scheduler capacity or wall-clock budget, starving others D high internal/quota enforces per-tenant CPU/memory/storage/network/parallelism quotas; internal/pipeline.Scheduler.Budget bounds wall-clock time independent of caller context and reports budget_exceeded distinctly from canceled medium: quota enforcement is opt-in at the control-plane (per roadmap, "wired as an opt-in per-tenant lease quota") — a deployment that doesn't enable it gets none of this
T25 poisoned or collided content-addressed cache entry is reused without recomputation T high internal/cache is digest-addressed; consumption-time digest verification is the documented pipeline invariant (stage materialization re-verifies before use, per the v2 pipeline design) low: requires a SHA-256 collision or a bug in the verify-before-reuse path itself
T26 control-plane state corrupted or lost across restart (crash, disk failure, concurrent write) T,D high internal/control/persistence.go handles save/restore explicitly; failures surface as structured Warn logs via internal/observability (commit 7eecb82) rather than being silently dropped medium: logging a failed save is not the same as recovering from one — no documented backup/replication strategy for control-plane state itself
T27 budget/resource tracker measures the wrong process, silently under-enforcing T medium internal/budget.Tracker was deliberately wired into internal/oci.Build (single-process), not internal/executor (spawns a separate sandboxed process per stage) — the roadmap records this was caught before shipping: Tracker reads RUSAGE_SELF, which would have measured the wrong process if wired into the multi-process path low, for the code as wired today: correct target chosen. Reopens if a future change wires internal/budget into per-stage executor accounting without addressing the process-boundary mismatch.
T28 GHCR/Cosign interoperability path (still present for CI proof) diverges from the native path's decision T low both paths sign/verify the same digest; CI treats Cosign as a cross-check, not a gate low: a divergence would be a CI signal, not a silent production bypass, since Cosign is not in the product path
T29 five-language plugin protocol implementations (Go/Python/JS/TS/C#) diverge in framing or validation, letting one accept something another would reject T medium all five are conformance-tested against the same platform-factory-conformance plugin suite (5/5 checks each, verified 2026-08-04 for Go/Python/JS locally, TS/C# via CI) medium: conformance testing proves the five pass the same checks, not that they reject the same novel malformed input outside the suite's five cases
T30 MicroVM guest network reaches unintended host or peer resources E,I high internal/networking provides guest DNS/network scoping; virtio-net is the only network device surface exposed to the guest medium: no dedicated adversarial test suite for guest network escape found in this pass — see "Open work"

Abuse cases tested

CI proves rejection of missing, corrupted, truncated, extra, symlinked, and wrong-media-type blobs; invalid indexes; wrong-architecture or dynamically linked CI binaries; unsafe entrypoints; duplicate labels/files; incomplete workflow hardening; unsigned Kubernetes admission; and incorrect signer identity (both the native path and, as an interoperability check, Cosign). The plugin protocol's own conformance suite additionally proves rejection of an unknown method (404), a missing Content-Type (fatal), and an oversized frame — run for real against Go/Python/JavaScript in this revision (bash examples/sdk/run.sh, 5/5 on each, 2026-08-04).

Explicit residual-risk acceptance

The project accepts that a cluster administrator can bypass admission, a shared-kernel container can be affected by a kernel vulnerability, hosted runner benchmarks vary, external transparency/identity services can be unavailable, the library accepts caller-supplied executable content, and application vulnerabilities remain the consumer's responsibility. As of this revision it additionally accepts, until T22 is closed, that the native VMM host process runs without seccomp/cgroups/privilege-drop of its own — guest isolation is intact (KVM), but the host-side VMM process is not self-sandboxed — and that quota/budget enforcement (T24) is opt-in, not a default a deployment gets for free.

Production owners must record their own acceptance or mitigation for T03, T04, T08, T10, T11, T14–T22, T24, T30. High-sensitivity workloads should use isolated runners, mirror dependencies and transparency evidence, require two-person workflow review, enforce admission in every workload namespace, scan the payload, enable per-tenant quota explicitly, and prefer the MicroVM consumer over plain containers where T22's gap is acceptable given their own risk tolerance.

Open work

Not yet done in this revision, tracked here rather than claimed complete:

  • Eight dedicated per-component threat models (builder, sandbox, plugins, Registry, publication, MicroVM, distributed scheduler, CAS) as separate documents. This revision folds their content into one unified register (T01–T30) instead; splitting them out is real remaining work, not a rewrite of what's here.
  • Trust-flow diagrams. None added this revision.
  • Adversarial/guest-network-escape test suite for MicroVM (T30) beyond the existing KVM boot and virtio device tests.
  • Closing T22 (wire internal/hypervisor/sandbox into cmd/platform-factory-runtime's actual VMM startup path) — currently the single highest-severity open gap this revision found.

Review triggers

Review this model after a new media type/compression algorithm, new external action or dependency, permission expansion, new registry/signer/admission mechanism, supported runtime addition, a new hypervisor backend (HVF/WHPX moving from in-progress to shipped), T22 closing, security incident, or at least annually.

Clone this wiki locally