August 22, 2026
Ambient authority is the bug, and WebAssembly makes you name it
Every containment control worth writing about has the same shape. Take a thing that can already reach everything, and spend an engagement taking capability away from it. No route off the box unless it goes through a broker. No credential reachable from inside. No filesystem beyond the one directory you mounted on purpose.
It works. It is also backwards, and it is worth being precise about why.
A Linux process can open a socket because processes can open sockets. Not because anyone decided this one should. It can read /etc/passwd, see the process table, and reach whatever the network reaches, and it arrives holding all of that before it has run a single line of your code. That property has a name: ambient authority. Authority that is present in the environment rather than granted to the actor.
Every sandbox, every network policy, every seccomp profile is a fence built after the fact around something that started with everything. You are not granting permissions. You are clawing them back, one class at a time, and hoping you thought of them all.
The inversion
A WebAssembly component starts with nothing, and the reason is worth stating precisely, because the obvious version of it is wrong.
It is not that the instruction set lacks a syscall opcode. Java bytecode has no syscall opcode either, and the applet sandbox still failed, because the guest could reach a large ambient class library that could. The opcode was never the control. What matters is that a component's every interaction with the world outside itself is an import: external authority that arrives named, supplied at instantiation. Imports are not only functions and they are not only supplied by a host. A component can import interfaces, types, values, and whole components, and one component can satisfy another's imports, which is what makes a call between two components a boundary in its own right rather than an internal detail. What holds across all of it is that nothing arrives unnamed. There is no ambient namespace to reach into and no global open or connect sitting in scope, and a call names an entry in an index space that contains only what the module was given, type-checked against a signature, rather than an address you can arrive at by arithmetic. If nothing wires up an interface for sockets, the component has no way to express opening one.
So "starts with nothing" is the shape of the model rather than a promise about your deployment. Hosts differ on what they wire up without being asked. Several hand over clocks and a random number generator by default and make you opt in to files and network, which means the honest sentence is that a component holds exactly what the host granted it and nothing else, and that the granting is the part worth reading.
That is the whole argument. The list of capabilities is a written artifact instead of an emergent property of whatever operating system the thing landed on. Everything below is what that buys and what it costs.
What it buys, in the shape of a real system
Consider a class of environment that punishes conventional architecture: constrained hardware, no reliable connectivity, no live certificate revocation lookups, and no package downloads at deploy time. Sites that get one box rather than a rack, where whatever ships has to be self-contained and stay correct for a long time without anyone visiting.
Now put an identity stack there. An OIDC provider, a certificate authenticator handling smartcard login, an administrative console, a user-facing portal, an internal identity store, and something to load signed configuration. Built conventionally that is half a dozen containers, each one carrying an operating system userland and a language runtime it barely uses, each one a separately patched, separately scanned, separately reviewed artifact. Components do not remove that work, since each is still separately built and patched with its own dependency tree to audit. What shrinks is the operating system underneath them.
Built as components it is one process. The runtime is the only thing with an operating system underneath it, and each component is a module of one to a few megabytes.
Be careful with that comparison, because the flattering version of it is against a distribution base image, and a team already writing Rust would ship a static binary in a scratch image at five to twenty megabytes. The honest claim is narrower: you drop six OS userlands and six copies of the runtime plumbing, and you keep one engine. On hardware where the entire budget is a few hundred megabytes that is often the difference between fitting and not, and it is worth measuring for your own workload rather than taking a ratio on faith. Precompiling modules ahead of time to avoid a startup compile, which you would do on this hardware, inflates the artifacts several times over.
Two things follow that matter more than the size.
The boundary is legible in both directions. Each component reaches outward only through the interfaces the host wired up for it. Inward, the runtime binds to loopback behind a default-deny network policy, so the hardened entry point that terminates TLS is the only path in from off the box. That second half is ordinary network hygiene rather than anything WebAssembly gave you, and it is worth saying so. Loopback is not an authentication boundary either: anything else sharing that host, a sidecar, a debug shell, a request-forgery bug in some other local service, is already inside the line. What you get is a claim narrow enough to assert on in CI instead of drawing in a diagram.
One artifact runs in more places. The same component runs anywhere a host provides the interfaces it imports, which is a narrower claim than it first sounds, because hosts differ on what they offer and on how policy is expressed. Precompiled artifacts are pinned to a target and a runtime version and are not portable at all. Within those limits you are shipping a module and a manifest rather than an image full of an operating system, and that does travel across a container runtime, a cluster, an appliance, and bare hardware.
In a setting where every component eventually gets security-reviewed and accredited, less surface to review is real, and a capability manifest gives a reviewer something concrete to argue about. It is not free. Assessors have decades of container tooling, hardening benchmarks, and control mappings, and close to none of that exists for components yet. A dependency inventory is buildable but not automatic: cargo-auditable embeds the crate graph in the artifact, wasm-tools reads it back out, and a CycloneDX SBOM is a build step away. None of that is the default the way a scanner parsing an operating system package database is, so it exists if you put it in the pipeline on purpose and produces nothing at all if you do not. A grant of wildcard outbound HTTP is exactly as opaque as a container. Expect the manifest to help you argue the boundary, and expect to spend the savings explaining the substrate. Over the life of a program the trade is probably favourable; on the first accreditation it very likely is not.
Where the model leaks
This is the part that decides whether an architecture survives contact.
Coarse grants rebuild what you just removed. A capability is only as narrow as you made it. Hand a component a preopened root directory and it can walk the filesystem by name, which is POSIX with an extra step. Let it inherit the network, or give it an outbound allowlist of every host, and it reaches whatever the box reaches. Those are one-line configuration choices and they are what teams actually ship when a deadline arrives. Worse, handles are ambient within a component: every line of code inside it, including every crate you linked, can use every capability that component holds. The model gives you a boundary you can read. It does not make that boundary fine-grained for you.
Something is still native, and it is the worst thing to have native. You can terminate TLS inside a component now, and the pieces exist to do it. But the mature, auditable implementations with hardware key support are still native, and on constrained hardware that is usually the call you make. So the piece facing the network, holding the private key, and parsing attacker-controlled input is the one piece you did not shrink, and in a single-process design it is not adjacent to the trusted computing base, it is inside it. There is a second edge here that the capability model does nothing about: the client identity that terminator establishes crosses into the sandbox as data, usually a header. Nothing stops a component believing a forged one. Stripping that header from inbound requests before setting your own is your job, and a boundary you have to test.
Capabilities say nothing about resources. A component granted no interfaces at all can still burn CPU or grow its linear memory until the host process dies, and the host process is now the entire stack. Consolidating six containers into one gave up the kernel's ability to kill one of them and leave the others running. Execution fuel, epoch interruption, and per-instance memory ceilings exist for exactly this, and every one of them is opt-in.
The host runtime is your trusted computing base. A capability is exactly as good as the runtime that granted it. A sandbox escape in the engine is a full compromise of everything the engine hosts, and now everything is in one process. You have traded many patchable containers for one runtime you must track closely and update deliberately.
The ecosystem will refuse to compile. Less of it than it used to. wasm32-wasip2 supports std, so files and sockets are host capabilities rather than absent, and crates like socket2 name the target explicitly now. What still stops a build is threads, native libraries that want a C toolchain, platform-specific APIs, and the long tail of crates nobody has ported. Sometimes the answer is a different crate, sometimes it is writing the thing yourself, and either way it lands in your estimate. Anyone who tells you the porting cost is small has not tried it with a cryptography dependency.
Observability is worse. Debugging, profiling, and stack traces are all further behind native tooling than you expect on day one. Budget for it.
The ground is still moving. As of the middle of 2026 the second generation of the interface layer is settled enough to build on, the third is the live migration target, and pieces like an in-sandbox TLS interface are still experimental. Versions shift, and a design done today carries an upgrade cost a container-based one does not.
The environment has its own leaks, and one of them is in the scenario above. No live revocation lookup means a revoked credential keeps authenticating until someone carries a fresh list to the box. That is a property of the deployment rather than of WebAssembly, and it is the sharpest risk in the design, so it needs an answer rather than a shrug: revocation lists shipped as signed configuration, a hard maximum staleness, and a decision made in advance about whether the box fails closed when that window expires. Failing open is a choice someone should make on purpose and write down.
None of that argues against the approach. It argues for naming the trade honestly, because the failure mode here is a team that believes the sandbox removed a risk when it relocated it.
Why Rust, specifically
Not because Rust is memory-safe. That claim is true and it is also what everyone says.
The useful version is that the two work on different axes, and being precise about which does what matters more than the slogan.
Rust makes the memory error unlikely, mostly. The exceptions are unsafe blocks and C compiled in through binding crates, which is to say the cryptography, which is to say the code you least wanted it in.
The sandbox does not make the error less likely, and inside the module it does not help at all. Linear memory has no address space randomisation and no guard pages between allocations. Reaching past the end of linear memory traps. Reaching past the end of one allocation into the next, which is the overflow you actually get, does not, because it never leaves the memory the module already owns. Intra-module corruption is therefore arguably more deterministic to exploit than the native equivalent. What the sandbox bounds is what an attacker gets afterwards. Code does not live in linear memory, so there is no shellcode to inject. Return addresses are not addressable. Indirect calls are checked against a type signature, which is coarse control-flow integrity for free.
So the ceiling is real, and it is worth stating exactly: everything in that component's memory, which includes its keys and every session it has touched, plus full use of every capability that component was granted. That is a great deal better than the box. It is not "a handful of named capabilities" either.
The practical reasons matter too, with one correction to the version of this argument you usually hear. No garbage collector means no collector heap, no pauses, and allocation behaviour you can reason about. It does not mean a flat footprint. Linear memory grows and never shrinks, because there is an instruction to grow it and none to give it back, so one oversized request pins that memory for the life of the instance. Six components is six linear memories, though that alone is not the difference: six native processes have six private heaps too. What containers built on a shared base image get and components do not is one page-cache copy of the executable and library pages sitting behind all six, and whether that matters is a measurement on your workload rather than a rule. Predictable is the right word. Small is something you measure and then bound with an instance memory ceiling. As of the middle of 2026 the component model toolchain is furthest along in Rust, so you spend your time on the system rather than on the build. And the compiler's strictness pays back where these systems are hardest to get right, which is the error paths nobody exercises until the day connectivity drops. With a caveat: a Result you cannot ignore is still a Result you can unwrap, and an unwrap in a component is a trap that poisons the instance. On a box nobody visits, that is precisely the failure you were trying to design out, so the strictness has to be enforced by lint policy rather than assumed.
How you would know any of it is true
A capability manifest is a claim. Claims get tested, in both directions.
Assert that a component with an empty allowlist cannot reach the network, and that the attempt fails the way you expect rather than hanging. Then assert that the same component with one entry granted reaches exactly that and nothing beside it. Assert that a component with no filesystem interface cannot read the config the component next to it can read.
The second half of each pair is the half people skip, and skipping it is how you end up with a suite that passes because everything is broken. A test that only checks denials also passes on a component that was never instantiated at all, so prove it got far enough to make the call: a link-time failure and a runtime denial are different results and only one of them is the boundary working.
Two more pairs belong in that suite, and they are the ones that separate a real harness from a reassuring one.
The confused deputy. A component with no network capability calling one that has it is not the bug. That is delegation, and it is what a broker is for. The bug is a caller steering the deputy into exercising authority the caller was never entitled to, so the test is not that the proxy call fails. It is that the deputy authorizes against the caller rather than against itself: a caller allowed one destination cannot name a second and be served, and cannot get an action out of the deputy that its own request did not justify. That is the defining failure of capability systems, and it is the one thing here that no runtime can catch for you, because the deputy is doing exactly what it was granted. The engine sees a legitimate call. Only your test sees a boundary crossed. If your components call each other, and they do, this is the test that matters most.
The limits you said were opt-in. A component in a hot loop must be interrupted rather than hanging the host, and one allocating without bound must hit its ceiling and trap rather than taking the process with it. This is the risk the section above names, and a risk you have named but never asserted on is a risk you are describing.
The allowlist read the way an attacker reads it. If entries are hostnames, try an IP literal, an unusual port, a redirect, an address that resolves to loopback, and the cloud metadata address. "Nothing beside it" is doing a lot of undefined work in that sentence, and an allowlist you have only exercised with the happy-path hostname is a string comparison you are hoping is a boundary.
None of this is specific to WebAssembly. It is the same positive and negative control the agent sandbox harness applies to containers, and it is the difference between a boundary you have measured and one you have described.
Where this fits
WebAssembly components are not a replacement for the work of deciding where boundaries belong. They change what enforces a boundary once you have decided, and they make the resulting grant legible in a way an operating system never has.
The decision itself is unchanged: what should this thing be able to reach, what happens when it does the wrong thing, and how would you know. That question is the same whether the answer is a network policy, a sandbox, or a manifest of named capabilities, and it is the question worth getting right first. Zero trust architecture, explained without the buzzwords covers how to think about it before the substrate is chosen.
Deciding where the boundaries go?
The zero-trust architecture engagement works out where implicit trust is still hiding, what should enforce each boundary, and a phased path to get there. Whether the substrate is containers, WebAssembly components, or both, the design question is the same one.
See the engagement