-
Notifications
You must be signed in to change notification settings - Fork 0
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.
| 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 |
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.
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.
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, open — internal/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" |
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).
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.
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/sandboxintocmd/platform-factory-runtime's actual VMM startup path) — currently the single highest-severity open gap this revision found.
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.
© 2026 CYPT71
platform-factory
Core
- Architecture and OCI Layout
- Next-generation Architecture
- Architecture Decision Records
- Security Model
- Threat Model and Residual Risks
- Independent Security Review Process
- CLI Reference
- Project Configuration and Dependency Freezing
- mTLS Configuration
- Meine Graal
CI/CD
Running an image
- Production Adoption Guide
- Dockerfile Consumer
- Local Dev (Podman/macOS)
- MicroVM Support
- MicroVM Administration
- Large-image streaming
Operating