wasi-sockets
wasi-sockets copied to clipboard
restricted access to certain domains
With WASI there's the chance to introduce security guarantees not possible with POSIX. One I'd be really looking forward to, would be to allow only access to certain domain names.
For being able to do so, it would be required to combine name resolution and connect
calls. If we would be able to directly call connect
with a domain name and (optionally?) the desired address family, we would be able to design a capability that only allows connecting to certain domain names.
Without that, sockets can only be limited to (potentially large) IP ranges, and code calling connect
would always have the possibility to connect to any IP address in this range.
As an example: I'd like to use a client library for accessing AWS S3 buckets and I'd like to ensure that this library isn't doing requests to anything else. I'd like to be able to pass a capability to that library that only allows access to the specific domain of the bucket (my_bucket.s3.amazonaws.com) and nothing else.
In a two-step approach with name resolution and address-based connect
, there could be a capability that limits the name resolution to only resolve that domain but for the socket I'd need to allow access to all of AWS's IP-ranges for the desired region. There would be nothing that prevents the library from doing any other requests to any of the IPs in those ranges.
If we would be able to directly specify domain names as remote addresses in the socket's connect
, we could limit it to only that single domain, and wouldn't need to allow access to the whole set of IP-ranges.
This would be a huge step forward in reducing software supply chain risks.
Thanks for reaching out!
Even though it is not part of the official spec goals, it is very much a personal goal of mine to facilitate domain name based firewalling. A couple examples of the intended level of permissions can be seen here.
What is a hard goal, however, is that:
Toolchains must be able to provide a POSIX compatible interface on top of the functions introduced in this proposal.
Merging connect
& DNS resolution into one call (while from an API point of view much nicer), would immediately break compatibility with all existing software.
A hypothetical alternative could be to provide two distinct APIs: connect-to-ip
& connect-to-hostname
. However, the same issue then still applies: all existing software would (at least initially) make use of connect-to-ip
. This makes the extra security provided by connect-to-hostname
"opt-in", which is not a nice track to be on.
In general, the limiting factor is the abstraction level this proposal is targeting. BSD-compatible sockets don't have a notion of domain names. So any solution that applies domainname-based restrictions on sockets is going to be an imperfect solution in some way or another. But that shouldn't stop us from trying to get the most out of it, ofcourse!
The current direction I'm thinking in, is to let the Wasm module "proof" the relationship between a domain name and an ip address to the embedder by performing a DNS lookup. Example:
- The embedder has a rule that allows TCP connections to the domain name "example.com" on port 22.
- The Wasm module calls
resolve-addresses
to perform a lookup of "example.com", which resolves to123.1.2.3
- The embedder remembers the address in a dictionary, mapping resolved addresses back to their domain name(s).
- The Wasm module calls
connect(sock, network, Ipv4SocketAddress("123.1.2.3", 22))
- The embedder looks up the ip address in its dictionary and finds that the address is associated with "example.com" (bullet 3). Additionally, the embedder also knows there's a rule allowing connections to "example.com" on port 22 (bullet 1). Therfore the
connect
call may continue. Otherwise it fails.
A while ago I made a POC to demonstrate this principle.
This workflow is based on the presumption that applications usually perform a DNS lookup using the WASI-provided resolve-addresses
function before performing the connect
. It does not work for applications that ship their own DNS client.
I don't know if this methodology is sound. There might be edge-cases.
Other solutions are also welcome, ofcourse. Let me know what you think!
Thanks a lot for sharing your thoughts on this!
I think it makes sense to think about network capabilities in two different ways. For libraries which are not aware of capabilities, the network capability is more like a static link-time capability. It has no way to pass it around, and any call to connect
just implicitly passes that static network capability to it, as the caller isn't even aware of that additional parameter. I really like your ideas here as I think that's probably the maximum that is achievable in this context. The network capability would probably be scoped at the granularity of modules. Once some code calls resolve-addresses
on a permitted domain name, the network capability acquires also permissions for the resolved IPs. By this, any code in that module also is permitted to connect to those IPs from that point of time on.
I'd also like to keep the door open for a dynamic network capability, though. One that can be explicitly passed around by code that is aware of the existence of such a capability. It would also have a method to derive a more restricted capability from it (e.g. network2 = derive-restricted(network, ["my_bucket.s3.amazonaws.com"])
or something in that regard) and it would be more similar to a directory file descriptor in the WASI filesystem APIs. wasmtime/docs/WASI-capabilities.md also hints at something that would go more into that direction.
The relationship between those two is probably quite similar to the relationship between open
and open-at
.
Does this make sense?
I'd also like to keep the door open for a dynamic network capability, though.
I don't think the current proposal closes any of those doors :) The Component Model already ensures that any WASI interface is fully virtualizable. In my mind, the exact implementation of attenuation (domain-based, ip-based, portnumber-based, protocol-based, ...) is better handled by each individual Wasm module itself. At least for the MVP.