Infrastructure migration in progress: some features are temporarily unavailable.
jomcgi.dev

embervm

STPA Control Analysis: embervm

logic @ b1514fe38 · security @ ccaba452d

Auto-generated STPA safety model: unsafe states this system can reach and control actions that get it there. Single or multiple lenses: logic (mission failure), security (deliberate attack), governance (data safety).

<details> <summary><b>How to read this</b>: STPA primer and diagram legend</summary>

STPA (System-Theoretic Process Analysis, Leveson) treats the system as controllers issuing control actions to controlled processes, with feedback flowing back. Instead of "what component fails," it asks "what control action, given or withheld at the wrong time, drives the system into an unsafe state?" Unsafe means a violation of this system's reason to exist. Multiple lenses (logic, security, governance) analyze the same control structure for different safety questions: mission failure, deliberate attack, data-processing violations. A finding appearing in multiple lenses is expected and correct.

Read top-down: Losses are outcomes we must prevent; Hazards are states leading to loss; diagrams show control structure (solid = control action, dashed = feedback); Unsafe Control Actions table is the core; Unsafe Feedback covers data channels (absent, stale, corrupted, spoofed). Every claim cites path:line; unbuilt elements are marked. Semantic stable IDs mean regenerating changes only findings that changed.

</details>

Control structure

Logical view

flowchart TD
  subgraph control_plane["control_plane"]
    dispatcher["Admission, quota gate, placement, dispatch"]
    node-registry["Dial-home registry + reconcile inventory"]
    s3-warmth-gc["Artifact TTL sweep (abort on partial listing)"]
    stateful-manager["Stateful wake/bank/blessing/quarantine lifecycle"]
  end
  subgraph ingress["ingress"]
    api["HTTP API + TokenReview auth"]
  end
  subgraph store["store"]
    op-log["Durable ordered journal (book of record)"]
  end
  api -- "api.admit_task" --> dispatcher
  dispatcher -- "dispatcher.quota_gate" --> op-log
  stateful-manager -- "stateful-manager.bless_generation" --> op-log
  stateful-manager -- "stateful-manager.destroy_orphan" --> node-registry
  node-registry -. "reported primed pool inventory (adopt_inventory)" .-> dispatcher
  node-registry -. "reported stateful VM inventory (dial-home NodeStatus), authoritative per invariant 5" .-> stateful-manager

Physical view

flowchart TD
  subgraph compute["compute"]
    egress-proxy["Brick-local egress credential proxy"]
    guest-vm["Firecracker guest VM"]
    node-envoy["node Envoy serving relay (DNAT)"]
    noded["noded brick daemon (privileged, /dev/kvm)"]
    shotter-chromium["Headless Chromium (content-driven fetches)"]
    shotter-guest-init["Shotter guest-init (PID 1): launches Chromium, hosts the proxy, serves /shim/ready"]
    shotter-proxy["In-guest egress allowlist proxy (image-baked policy)"]
  end
  subgraph control_plane["control_plane"]
    control-plane["Elixir control plane pod (1 replica, Recreate)"]
  end
  subgraph data["data"]
    s3-store["S3-compatible object store (SeaweedFS gateway)"]
  end
  subgraph external["external"]
    caller["External task/session/serving caller"]
    external-host["External destination (e.g. GitHub API)"]
  end
  control-plane -- "control-plane.grpc_command" --> noded
  egress-proxy -- "egress-proxy.inject_credential" --> external-host
  node-envoy -- "node-envoy.dnat_route" --> guest-vm
  noded -- "noded.artifact_verb" --> s3-store
  noded -- "noded.refuse_if_silenced" --> noded
  noded -- "noded.vsock_dispatch" --> guest-vm
  shotter-chromium -- "shotter-chromium.fetch_subresource" --> shotter-proxy
  shotter-proxy -- "shotter-proxy.forward_allowed" --> egress-proxy
  control-plane -. "contact signal behind the silence gate: a 2xx dial-home Register response and every successful WatchNode NodeStatus send refresh lastContact, which refuseIfSilenced compares against the 21600s bound" .-> noded
  noded -. "dial-home registration {node, pod_uid, address, boot_id} + NodeStatus" .-> control-plane
  s3-store -. "fetched artifact bytes on RestoreArtifact" .-> noded
  shotter-guest-init -. "GET /shim/ready 200 (CDP /json/version answered and one real trial capture produced non-empty PNG bytes)" .-> noded

Control actions

ID View Control action Controller → Process Maturity Evidence
api.admit_task logical Admit task workload apidispatcher built projects/embervm/control/lib/embervm/router.ex:33
control-plane.grpc_command physical gRPC lifecycle command (BuildBase, Prime, Assign, Destroy, DeleteVolume, RestoreArtifact) control-planenoded built projects/embervm/proto/embervm/node/v1/node.proto:57
dispatcher.quota_gate logical Gate quota on admission dispatcherop-log built projects/embervm/control/lib/embervm/dispatcher.ex:258
egress-proxy.inject_credential physical Inject credential into request egress-proxyexternal-host built projects/embervm/ARCHITECTURE.md:710
node-envoy.dnat_route physical DNAT route to guest node-envoyguest-vm built projects/embervm/ARCHITECTURE.md:193
noded.artifact_verb physical Access artifact in store (get, put, delete, list, tag) nodeds3-store built projects/embervm/proto/embervm/node/v1/node.proto:463
noded.refuse_if_silenced physical Refuse local authority if control-plane contact stale nodednoded built projects/embervm/noded/server/server.go:619
noded.vsock_dispatch physical vsock dispatch to guest nodedguest-vm built projects/embervm/ARCHITECTURE.md:234
shotter-chromium.fetch_subresource physical Fetch subresource from rendered page shotter-chromiumshotter-proxy built projects/embervm/runtimes/shotter/guest-init/cmd/main.go:168
shotter-proxy.forward_allowed physical Forward request to allowlisted destination shotter-proxyegress-proxy built projects/embervm/runtimes/shotter/guest-init/cmd/proxy.go:391-405
stateful-manager.bless_generation logical Issue blessed generation stateful-managerop-log built projects/embervm/control/lib/embervm/stateful_manager.ex:676
stateful-manager.destroy_orphan logical Destroy orphaned VM stateful-managernode-registry built projects/embervm/control/lib/embervm/stateful_manager.ex:2027

Lens: logic

Scope. EmberVM is a Firecracker microVM platform: an Elixir control plane admits and dispatches task, session, serving, and stateful workloads onto brick pods, each run by a privileged Go noded daemon that supervises guest VMs over vsock and dial-homes to register with the control plane.

<details> <summary>Maturity detail</summary>
  • Built: task/session/serving/stateful/composite lifecycle managers, admission quota gate (fail-closed per-principal when a budget is configured), generation blessing (CP-issued pre-dispatch and checkpoint-abort self-heal via durable checkpoint_dispatched), delegated generation leases (BlessingLease, bounded and monotonic), orphan-destroy reconcile with an ACTIVATOR-origin adoption guard that runs before the destroy pass, S3 warmth GC that aborts the whole sweep on a partial listing, dial-home registration, brick-local egress credential injection scoped by egressTo, the shotter task-class guest (ADR embervm/035): a warm headless Chromium snapshot base behind an in-guest, image-baked, hard-allowlisted egress proxy, whose readiness is now gated on one real trial capture before /shim/ready flips, the noded gRPC control channel, authenticated by a static bearer token and scoped by a CiliumNetworkPolicy to control-plane-only ingress, enabled in production (#4693), SigV4-authenticated access to the S3 artifact store enforced by the gateway (enableAuth: true), enabled in production (#4708); the surviving embervm identity is one credential shared by every brick and the control plane, the brick silence timeout (ADR embervm/037) bounding node-local authority (activator wakes, group wakes, blessing-lease self-advance) once control-plane contact goes stale, armed at 21600s in production and dev, the warmth ownership transition guard (#4962): every brick refreshes a .alive claim per warmth segment and startup GC reaps only unclaimed pre-heartbeat directories or claims older than WarmthStaleAfter, principal-artifact envelope encryption and capability-gated restore in noded, reader path unconditional, writer and enforcement armed in production and dev as of 2026-08 (ADR embervm/033, #4691; status updated 2026-08-23 (#5233))
  • Designed-only: mTLS/SPIFFE certificate-based mutual transport auth for noded (#4693 delivered the bearer-token-plus-NetworkPolicy interim), node-local activator for stateful/composite wake beyond cold boot (partially landed, stateful activator currently only cold-boots), audience-scoped guest token, request-scoped GitHub tool mediation replacing host-keyed injection (ADR 055)</details>

Losses

ID Loss
L.integrity-loss Stored or served state (volume, snapshot, generation, artifact) diverges from truth and is acted on as correct
L.liveness-loss The fleet, a brick, or a workload becomes unable to admit or serve legitimate work
L.provenance-loss State, credentials, or execution history become attributed to the wrong principal, brick, or generation
L.secret-exposure Credential or key material becomes reachable by an unauthorized principal or guest
L.silent-incorrectness A caller receives a served or resumed result derived from stale, forged, or wrong state with no signal
L.unauthorized-access An actor obtains a capability, credential reach, or execution path beyond its principal or role

Hazards

ID View Hazard (unsafe state) → Losses Maturity
egress-workload-derivation-gap physical the per-workload egress forwarder allowlist noded receives is a hand-written if-chain over named workloads in the chart template, not a true derivation, so a future task/session/stateful workload that enables egress but is not added to this chain gets no forwarder and dial-times-out inside the guest instead of a loud, diagnosable deny L.liveness-loss built
host-keyed-credential-overreach physical host-keyed egress credential injection authorizes by destination host only, so a prompt-injected or compromised guest can shape any request to an allowlisted host and have the credential attached to it L.secret-exposure, L.unauthorized-access built
identity-hijack physical an actor holding the shared noded ServiceAccount token can re-register an existing brick's (node, pod_uid) at an address it controls and become the dial-home source the control plane treats as authoritative for that brick L.unauthorized-access, L.integrity-loss, L.provenance-loss built
shared-egress-allowlist-widening physical egress.internal.allowlist is global to the shared sidecar rather than scoped per workload, so shotter's two new frontend destinations become reachable by every other egress-enabled workload (today the claude runtime, later pi if granted egress) with no per-workload authorization check L.unauthorized-access built
shotter-broken-base-snapshot physical shotter's base snapshot is cut when /shim/ready first returns 200; readiness now proves more than protocol liveness because guest-init must answer CDP /json/version and then complete one real trial capture (navigate about:blank, screenshot, non-empty PNG bytes within 15s) before flipping ready, so a globally broken renderer or GL stack fails loudly at warm-up instead of baking into every clone, but the trial exercises only about:blank, so a page-specific rendering failure can still be baked into the base and fail per-invocation instead of failing once, loudly, at BuildBase L.liveness-loss, L.silent-incorrectness built
shotter-memmib-tmpfs-coupling physical the shotter workload's memMib budget and its guest-init tmpfs size are a YAML integer and a Go string literal in different directories with nothing in the build enforcing their relation, so raising either alone silently trades a legible ENOSPC on /tmp for an opaque guest OOM kill under load, or leaves /tmp undersized without freeing any more memMib L.liveness-loss, L.silent-incorrectness built
shotter-policy-drift physical the guest's baked destination policy (/etc/shotter-egress.json, a checked-in file copied verbatim into the image) and the sidecar's chart-driven egress.internal.allowlist are two independently hand-maintained sources of egress truth with no build-time check that they agree, so they can diverge in either direction: the guest could deny a destination the chart already permits (silent functional failure), or a future image rebuild could widen the baked allowlist beyond what any ADR or chart review anticipated L.unauthorized-access, L.silent-incorrectness built
store-credential-unscoped physical the S3 gateway now requires a valid SigV4-signed identity, but the embervm identity's credential is one secret rendered into every noded pod and the control plane, with bucket-wide read/write/list/tag reach over both the embervm and embervm-dev buckets; restore authorization is still storage-ACL-only, not scoped by (principal, lineage, brick, workload, generation, lease), so any brick holding the shared credential can write, substitute, or delete another principal's artifacts, bypassing noded's per-request artifact-verb checks entirely L.integrity-loss, L.silent-incorrectness built
unmodeled-checkpoint-abort logical the interruptible-bank checkpoint commit/abort protocol (generation-advance-then-delete-temp-then-resume ordering, resolve-timeout auto-abort, blessed-vs-self-bump discrimination) governs whether a resumed VM's generation pairing stays trustworthy, but is verified only by code comments, unlike the bank/relight pairing invariant it sits underneath L.integrity-loss, L.silent-incorrectness built

Unsafe control actions

ID View Control action Guideword Unsafe condition Severity → Hazards Evidence
egress-proxy.inject_credential.providing physical egress-proxy.inject_credential providing the proxy injects a real credential into any request whose destination host is allowlisted, regardless of what the guest-originated request actually asks that host to do, so a prompt-injected guest can direct the credentialed call medium host-keyed-credential-overreach projects/embervm/ARCHITECTURE.md:745

Unsafe feedback

Data channels (absent, stale, corrupted, spoofed) whose failure drives a controller into a hazard.

ID View Channel Guideword Unsafe condition Severity → Hazards Evidence
dial-home.unauthorized-source physical nodedcontrol-plane: dial-home registration {node, pod_uid, address, boot_id} + NodeStatus unauthorized-source re-registering an existing (node, pod_uid) at a different address is accepted unconditionally and expires the prior instance, so identity is self-asserted under a ServiceAccount shared by every brick rather than bound to the actual brick high identity-hijack projects/embervm/control/lib/embervm/node_registry.ex:1320
shotter-readiness.corrupted physical shotter-guest-initnoded: GET /shim/ready 200 (CDP /json/version answered and one real trial capture produced non-empty PNG bytes) corrupted readiness no longer gates on protocol liveness alone: guest-init runs one real trial capture (navigate about:blank over CDP, screenshot, non-empty PNG bytes required within 15s) before /shim/ready flips, so a globally non-rendering browser now fails BuildBase loudly; the residual staleness is that the trial page is static about:blank, so readiness still cannot prove rendering of real fetched pages, and a page-specific break baked into the base surfaces per invocation instead of once at BuildBase low shotter-broken-base-snapshot projects/embervm/runtimes/shotter/guest-init/cmd/main.go:127-139,183-206
warmth-fetch.unauthorized-source physical s3-storenoded: fetched artifact bytes on RestoreArtifact unauthorized-source the gateway now enforces SigV4 (enableAuth: true) and rejects requests signed by no identity, but the embervm identity's access key is one credential shared by every brick and the control plane rather than scoped per principal, lineage, brick, workload, or generation, so an object noded restores as warmth may have been written by any brick holding that shared credential, not necessarily the legitimate owner of the lineage being restored; the finer per-principal binding (envelope encryption plus capability-gated restore) is built and enforced in production and dev (#5233 status update) medium store-credential-unscoped projects/embervm/chart/templates/_noded-pod.tpl:353
<details> <summary><b>Not UCAs</b>: 13 examined and rejected</summary>
  • a fleet-wide freeze of new session/serving/stateful placement during a genuine control-plane outage longer than the 6h brick silence bound (ADR embervm/037): a deliberate, accepted trade-off (ADR embervm/037 consequences): the gate only refuses NEW work, never banks or destroys anything, so live VMs keep running and held warmth stays intact; normal service resumes immediately once contact returns (noded/server/server.go:608-625), and 6h was sized specifically to exceed a routine control-plane roll
  • a leaked Chromium CDP target accumulating across shotter invocations: each invocation restores its own CoW clone that is destroyed after the response, so a leak cannot outlive one clone; within a clone, closeCDPTarget runs on every return path via defer immediately after target creation, including navigation and capture errors (projects/embervm/chart/templates/workload-shotter.yaml, guest-init/cmd/cdp.go:467-481)
  • a redirect or subresource on a mapped page escaping the shotter-proxy allowlist: the proxy is the only egress path (--proxy-server with loopback-only bypass, no direct-network fallback), so a redirect or a subresource fetch to an unmapped host issues a fresh CONNECT/absolute request through the same proxy, is re-parsed and refused per connection by handleConnection, and is checked by the same resolve() call as any other request, never trusted because it originated from an already-allowed page (projects/embervm/runtimes/shotter/guest-init/cmd/main.go:168-175, guest-init/cmd/proxy.go:293-313,391-405)
  • a restored clone resuming a page or CDP target left over from a prior invocation: createCDPTarget opens a fresh about:blank target per invocation specifically so an hours-later restore never inherits a stale page or target (projects/embervm/runtimes/shotter/guest-init/cmd/cdp.go:46-47)
  • checkpoint-abort auto-bump producing an unblessed generation: the resolve-timeout auto-abort lane (blessedGeneration: 0) is the ONLY case that produces it, and StatefulStore correctly quarantines it as a fail-closed signal rather than treating it as a bug (noded/server/stateful.go:605-610)
  • no per-principal daily budget configured in the reference deployment: spend is still bounded by admission caps and concurrency, not unbounded; cutoff is an admission action by design (invariant 4), and quota fails closed the moment a budget IS set (control/lib/embervm/dispatcher.ex:265-269, deploy/values.yaml)
  • node-local activator on stateful defaults BlessedGeneration to 0 during control-plane absence: a zero generation fails pairing and forces a cold boot rather than an incorrect relight, matching invariant 4's fail-open-to-cold-boot rule (noded/server/stateful_activator.go:361)
  • orphan-destroy racing a live node-woken (ACTIVATOR-origin) stateful VM: adopt_activator_stateful_vms runs on the same reconcile pass before the orphan-destroy loop, and the loop explicitly skips activator_origin? vms as belt-and-suspenders (control/lib/embervm/stateful_manager.ex:2031-2038)
  • shotter-proxy refusing every destination when /etc/shotter-egress.json is missing or malformed: LoadProxyConfig returns a zero-value config on any read or parse error, and the zero value's nil maps make resolve() refuse everything; the failure mode is fail-closed and surfaces as a bounded per-request refusal, not a silent bypass (projects/embervm/runtimes/shotter/guest-init/cmd/proxy.go:78-99)
  • shotter-proxy.forward_allowed checked against the requested top-level host instead of the actually-dialled destination: resolve() applies the host mapping first and validates the resulting mapped destination against the allowlist for both CONNECT targets and Host-header-derived absolute-form requests, so the value checked and the value dialled are always the same one; a request for an unmapped or mismatched host fails resolve() before any vsock dial (projects/embervm/runtimes/shotter/guest-init/cmd/proxy.go:211-247)
  • the S3 artifact-store gateway accepting anonymous, unsigned requests (formerly hazard anonymous-store-access): closed in production: SeaweedFS S3 auth is enabled (enableAuth: true, projects/platform/seaweedfs/values.yaml:181) with the embervm identity policy (s3-identities.json), so an unsigned request is now rejected at the gateway; the residual gap, that the surviving identity is shared fleet-wide rather than scoped per principal, is tracked separately as hazard store-credential-unscoped (#4708, closed)
  • the brick silence gate firing while the control plane is actually alive, wrongly refusing activator wakes, group wakes, or blessing-lease self-advancement (ADR embervm/037 wrong-timing): rejected: lastContact advances only on authenticated control-plane contact through two independent channels, a 2xx dial-home POST on the roughly 30s jittered register loop (noded/server/register.go:96-98,198-203) and every successful WatchNode NodeStatus send on the 2s liveness interval (noded/server/server.go:70,1801), compared with time.Now's monotonic reading so an NTP step cannot arm or disarm the gate (noded/server/server.go:595-597); tripping it requires both channels to fail simultaneously for the full 21600s bound (deploy/values.yaml:195), which is itself the brick partition the gate exists to catch, and even a false trip is bounded refusal of NEW work with live VMs, banks, and held warmth untouched until contact returns
  • the noded gRPC control channel accepting BuildBase/Prime/Assign/Destroy/DeleteVolume/RestoreArtifact from any pod-network caller (formerly hazard open-node-control-channel): closed in production: the gRPC surface is now bearer-token authenticated (EMBERVM_NODED_BEARER_TOKEN gates unaryAuthInterceptor/streamAuthInterceptor, noded/cmd/main.go:286-293) AND a CiliumNetworkPolicy restricts gRPC-port ingress to only the control-plane pod's own selector labels, excluding noded's own brick labels (chart/templates/noded-networkpolicy.yaml:14-23); both bearerTokenSecret.enabled and networkPolicy.enabled are true in deploy/values.yaml (#4693, closed)</details>

Open questions

  • Whether a chart conformance test should pin the stateful/composite activator port ranges (5400-5419) into noded-networkpolicy.yaml: the 2026-08-22 first enable omitted them and dropped a cold wake to noded:5401 for 14 minutes (deploy/values.yaml:163-166), and nothing structural prevents a repeat edit from doing the same.
  • Whether egress.internal.allowlist should become scoped per egress-enabled workload rather than global to the sidecar (ADR embervm/035 open question 1); shared-egress-allowlist-widening tracks the safety consequence of leaving it global.
  • Whether the store-credential-unscoped residual (a shared, bucket-wide S3 identity) should be re-scoped per principal now that the ADR embervm/033 rollout is armed end to end in production and dev (kekRoot, EMBERVM_ARTIFACT_ENCRYPTION, store.encrypt, requireRestoreCapability; #4691, status updated 2026-08-23 (#5233)): what survives of the residual is write/delete reach and key custody, since reads of encrypted warmth yield ciphertext.

Lens: security

Scope. Deliberate attacks on EmberVM's control structure: an adversary who forges, withholds, or replays control actions or feedback. Same control structure as the logic lens, analyzed for attack rather than honest failure.

<details> <summary>Maturity detail</summary>
  • Built: bearer-token authentication on every unary and streaming gRPC call plus CiliumNetworkPolicy restricting noded ingress to control-plane pod selector labels (#4693); SigV4-authenticated access to S3 artifact store enforced by gateway (#4708); brick dial-home registration bound to the noded pod's projected token claim identity (#4707, closed by #5049); per-principal envelope encryption of mutable state and capability-gated restore authorization (#4691, armed in production and dev 2026-08); the brick silence timeout (ADR embervm/037) bounding node-local authority once control-plane contact goes stale, armed at 21600s in production and dev; warmth ownership transition guard (#4962) refreshing .alive claims and reaping unclaimed pre-heartbeat directories
  • Designed-only: mTLS/SPIFFE certificate-based mutual transport auth for noded, request-scoped GitHub tool mediation replacing host-keyed injection (ADR agents/055), digest-verified manifests for restored artifact integrity (ADR embervm/033 decision 3), per-workload egress allowlist scoping (ADR embervm/035)</details>

Losses

ID Loss
L.capacity-exhaustion A malicious tenant or unauthorized actor exhausts shared placement capacity, quota, or concurrency
L.credential-theft Egress credential, store credential, or bearer token is obtained and used by an unauthorized actor
L.host-compromise A brick pod is compromised via a VMM escape or noded vulnerability and holds privileged access, store credential, and bearer token
L.tenant-data-corruption Another tenant's mutable state is written, deleted, or substituted by an unauthorized principal or brick
L.tenant-data-disclosure Another tenant's memory snapshot, session workspace, or state is read by an unauthorized principal

Hazards

ID View Hazard (unsafe state) → Losses Maturity Status Issue
firecracker-no-jailer physical noded execs the firecracker binary directly, as root, inside a privileged pod; the only per-VM containment added is a mount namespace for vsock isolation. Firecracker's built-in seccomp filter is active, but a VMM escape lands in a process holding /dev/kvm, the fleet-shared store credential, and the noded bearer token, becoming the compromised-brick adversary instantly. L.host-compromise built none #5255
host-keyed-credential-overreach physical host-keyed egress credential injection authorizes by destination host only, so a prompt-injected or compromised guest can shape any request to an allowlisted host and have the credential attached to it L.credential-theft built designed
identity-hijack physical an actor holding the shared noded ServiceAccount token can re-register an existing brick's (node, pod_uid) at an address it controls and become the dial-home source the control plane treats as authoritative for that brick L.credential-theft, L.host-compromise built enforced-prod
shared-egress-allowlist-widening physical egress.internal.allowlist is global to the shared sidecar rather than scoped per workload, so shotter's two new frontend destinations become reachable by every other egress-enabled workload (today the claude runtime, later pi if granted egress) with no per-workload authorization check L.credential-theft built none #5320
store-credential-unscoped physical the S3 gateway now requires a valid SigV4-signed identity, but the embervm identity's credential is one secret rendered into every noded pod and the control plane, with bucket-wide read/write/list/tag reach over both the embervm and embervm-dev buckets; restore authorization is still storage-ACL-only, not scoped by (principal, lineage, brick, workload, generation, lease), so any brick holding the shared credential can write, substitute, or delete another principal's artifacts, bypassing noded's per-request artifact-verb checks entirely L.tenant-data-corruption, L.tenant-data-disclosure built none #4691

Unsafe control actions

ID View Control action Guideword Unsafe condition Severity → Hazards Status Issue Evidence
api.admit_task.providing logical api.admit_task providing a malicious principal submits a workload that references another principal's artifact ref or lineage, bypassing the invariant 3 boundary (no mutable VM or snapshot lineage crosses a principal); the principal-scoped keyspace for the shared prefix remains designed but not implemented high store-credential-unscoped enforced-prod #4691 projects/embervm/control/lib/embervm/dispatcher.ex:130
control-plane.grpc_command.providing physical control-plane.grpc_command providing an attacker on the cluster network calls noded's gRPC surface (BuildBase, Assign, Destroy, RestoreArtifact) directly from anywhere on the pod network without a legitimate identity high identity-hijack enforced-prod #4693 projects/embervm/chart/templates/noded-networkpolicy.yaml:14-23
egress-proxy.inject_credential.providing physical egress-proxy.inject_credential providing a prompt-injected guest gets a real credential attached to a request it authored, not one the operator intended; the decided direction (request-scoped tool mediation for at least the git credential class) is drafted in ADR agents/055 and not yet the default for every credential high host-keyed-credential-overreach enforced-prod projects/embervm/ARCHITECTURE.md:745
noded.artifact_verb.providing physical noded.artifact_verb providing a compromised brick or a credential holder can write, evict, or substitute another principal's artifact; the store credential is still bucket-wide and fleet-shared, so a compromised brick can delete or overwrite any principal's artifacts high store-credential-unscoped enforced-prod #4691 projects/embervm/chart/templates/_noded-pod.tpl:353

Unsafe feedback

Data channels (absent, stale, corrupted, spoofed) whose failure drives a controller into a hazard.

ID View Channel Guideword Unsafe condition Severity → Hazards Status Issue Evidence
dial-home.unauthorized-source physical nodedcontrol-plane: dial-home registration {node, pod_uid, address, boot_id} + NodeStatus unauthorized-source an attacker re-registers an existing (node, pod_uid) at a different address using the shared ServiceAccount every brick already holds, attempting to become the dial-home source the control plane treats as authoritative for that brick; the register route now refuses a body whose pod_uid or node does not match the bound projected token's own claims, a token with no pod binding, or an unset noded SA high identity-hijack enforced-prod projects/embervm/control/lib/embervm/router.ex:909-913
warmth-fetch.unauthorized-source physical s3-storenoded: fetched artifact bytes on RestoreArtifact unauthorized-source a memory snapshot or artifact noded restores as warmth may have been written by any brick holding that shared credential, not necessarily the legitimate owner of the lineage being restored; the credential itself is still one identity with bucket-wide write, list, and delete reach high store-credential-unscoped enforced-prod #4691 projects/embervm/chart/templates/_noded-pod.tpl:353
<details> <summary><b>Not UCAs</b>: 1 examined and rejected</summary>
  • a guest with no cluster credential accessing the store directly: noded is the sole store client; guests hold no cluster credential by construction, and have no network path to the store (ARCHITECTURE.md section 9)</details>

Open questions

  • Whether digest-verified manifests for artifact integrity (ADR embervm/033 decision 3) should be implemented to detect deliberate artifact substitution by a credential holder
  • Whether per-workload egress allowlist scoping (ADR embervm/035 open question 1) should be implemented to prevent shared-egress-allowlist-widening
  • Whether request-scoped GitHub tool mediation (ADR agents/055) should replace host-keyed egress credential injection
  • shared egress allowlist provides no per-workload isolation (ADR embervm/035), making destinations added for one workload reachable by all egress-enabled workloads with no per-workload authorization check