Services written today share a common flaw: Being over-privileged. Node.js applications, for example, are capable of executing child processes, sending network requests, writing to the filesystem, and sending signals to other processes. A typical application requires a small subset of these features, and as malicious modules gain in popularity, the risk of these often-unnecessary features being abused only increases.
At Intrinsic, we've spent years building a product which applies the Principle of Least Privilege to Node.js. Our core product allows our customers to provide a list of policies describing what the application is capable of doing. This includes things like which binaries can be executed, how the filesystem can be interacted with, and what URLs the application can request. If an I/O operation hasn't been whitelisted, then it fails.
Recently we asked ourselves: if a new platform was designed for building middleware / microservices, one which followed the Principle of Least Privilege, and provided enough functionality to securely replace the most common microservice use-cases, what would such a system look like?
Osgood became our attempt to build such a platform. However, we didn't want such a tool to be completely unfamiliar to developers. So, we reached for a pile of technology already familiar to many.
The platform is built using Rust, a language heralded for its safety, and JavaScript is run in V8, a ridiculously fast JavaScript engine.
How does it work?
Osgood is available as a statically-linked binary that can be downloaded for Linux and MacOS. The binary can then be called with a path to a JavaScript application defining high level configuration, how to route incoming requests to different Osgood workers, and the security policies required by each worker.
The binary can easily be run on a laptop for doing local development. Once the application is ready, the code, as well as the Osgood binary, can be uploaded to a production server.
Osgood is generally useful in situations where logic needs to be performed on a server, or some sort of secret needs to be kept from a client, or when the only required outbound I/O is via HTTP requests.
Osgood provides the necessary APIs to intuitively build CRUD applications backed with technologies like CouchDB or Elasticsearch. Clients such as mobile apps and web browsers typically shouldn't be given unfettered access to a database, both for security purposes as well as preventing tight coupling. Using Osgood to maintain database credentials is a more secure approach, and transforming data into a common format helps avoid vendor lock-in.
Another use-case is providing an HTTP facade in front of existing backend API services. For example, if a mobile application wants to access data from two internal services, Osgood can make the two calls on behalf of the client and transform the resulting data. This also makes Osgood feasible as a GraphQL API.
Guiding Principles
A few principles helped guide us while designing Osgood. Since we're a security company it's very important that the platform be secure. Nobody is going to want to use Osgood if it's slower than other technologies, so it needs to be fast. And finally, we wanted to bring the Principle of Least Privilege into the hands of more programmers. This required us to make such an implementation extremely simple.
Osgood Applications are Secure
Policies are defined within Osgood Application files using JavaScript functions. These functions use the same syntax as the Intrinsic for Node.js HTTP policies.
Here's an example of a policy file which is able to interact with a few select GitHub APIs, as well as a CouchDB database:
// app.js
// global configuration
app.interface = '0.0.0.0';
app.port = 3000;
app.get('/user/:username', './worker.js', (policy) => {
policy.outboundHttp.allowGet('https://api.github.com/users/*/gists');
policy.outboundHttp.allowGet('https://api.github.com/users/*/repos');
policy.outboundHttp.allowGet('http://couchdb.local:5984/users/*');
policy.outboundHttp.allowPut('http://couchdb.local:5984/users/*');
policy.outboundHttp.allowPost('http://couchdb.local:5984/users');
policy.outboundHttp.allowDelete('http://couchdb.local:5984/users/*');
});
Many backend databases expose functionality via HTTP-based APIs—think CouchDB and Elasticsearch. Many third-party services expose their APIs via HTTP as well—such as GitHub and Stripe. Needless to say a lot of these middle-layer microservices can be built by exclusively communicating via HTTP.
Osgood is Fast
With a simple Hello, World!
benchmark, Osgood is able to serve around 40k requests per second.
$ wrk -c 100 -d 60 http://localhost:3000/hello
Running 1m test @ http://localhost:3000/hello
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 4.61ms 13.36ms 219.02ms 97.42%
Req/Sec 20.36k 3.17k 25.04k 91.00%
2422992 requests in 1.00m, 265.74MB read
Requests/sec: 40360.32
Transfer/sec: 4.43MB
Osgood is Simple
Osgood is available as a single statically-linked binary about 20MB in size. The only required argument for executing the binary is a single JavaScript file.
Let's take a look at a sample Osgood Application. This application represents a common task amongst microservices. The application accepts an input value in the request, makes multiple outbound HTTP requests using the value, transforms the resulting data in some manner, and responds with the combined dataset. This is the API facade pattern and is frequently used to reduce requests made by a client.
// worker.js
export default async (_request, context) => {
const username = context.params.username;
const [gists_res, repos_res] = await Promise.all([
fetch(`https://api.github.com/users/${username}/gists`),
fetch(`https://api.github.com/users/${username}/repos`),
]);
const [gists, repos] = await Promise.all([
gists_res.json(),
repos_res.json(),
]);
return { username, gists, repos };
}
Once you download a release you can then run this Osgood Application using the following command:
$ osgood app.js
In our next post we'll look at Hosting a Static Site and Contact Form with Osgood.
Top comments (5)
First I want to say I think this is cool . I do wonder tho, if it provides any benefit in something like a k8s cluster where the rules of communication are already well enforced. Also does node.js already need to be on the system or can it run in isolation?
Is it very different from deno?
Deno and Node.js both intend to be general-purpose platforms for running JavaScript code. Things like instantiating TCP servers are performed in user-land code. You could, for example, build a daemon with Deno or Node.js which subscribes to changes in stock prices and runs a raw SQL query when they change.
Osgood intends to be a server-only platform with extremely granular whitelist-only permissions for outbound I/O. It does not aim to be general purpose. For example, Osgood applications won't be able to instantiate TCP connections in user-land. However, they can call abstractions representing more complex operations performed in Rust, like user-land code being able to run some sort of read operation against a database.
In this regard I personally think of Osgood as being more like PHP, where instead of generating a TCP packet I would use built-in modules and configure a database connection and run a query.
Does osgood come with a standard library? If not, any plans to implement one or port node.js functionality? Would existing npm packages work?
We do plan on continuing to add features. For example, we'd like to include some sort of abstraction around database or filesystem access.
Some simpler packages, such as Lodash, can be passed through webpack and converted into an Osgood-compatible form.
Here's some more information relating to your questions:
thomashunter.name/presentations/in...