The Future of Cloud Computing, Probably
Having used Containers, Kubernetes and Serverless (a lot!) over the last few years, I catch myself wondering ‘What next?' when it comes to more efficient, faster and secure units of computing. My initial research indicates that Unikernels could be a promising bet.
What are Unikernels?
Here’s how I summarised it for myself after a fair bit of reading:
The code-base and necessary components of run unikernels are typically a few MBs in size, thereby decreasing the possibility of multiple unnecessary and, potentially vulnerable components being installed. To top it off, System calls are usually unsupported as well. Additionally, randomising the memory layout(ASLR) makes it almost impossible for an attacker to invoke specific OS function calls.
- No Shell
- No Users
- Single Process
- Does not need underlying Host OS(typically runs on Hypervisor)
- Decreased Attack Surface
Types of Unikernels
Restricted to a single language with all the necessary interfaces provided and also allows language specific virtual machines to function.
Example: MirageOS, IncludeOS, HalVM, LING, runtime.js
Implemented using the POSIX(Portable Operating System Interface) standard which makes it possible to run most POSIX-Compliant applications and services with a few configuration changes without having to re-write them completely.
Example: OSv, Graphene, Nanos, Rumprun
VMs vs. Containers vs. Lightweight VMs vs. Unikernels
|Runs on a Hypervisor||Requires a Host OS||Requires a Host OS||Runs on a Hypervisor|
|Uses own Kernel + Allocated Resources||Relies on Host Kernel + Consumes Host Resources||Dedicated virtualized Kernel(KVM) + Allocated Resources||Built-in Kernel libraries + Allocated Resources|
|Multiple Processes||Multiple Processes||Multiple Processes||Single Process|
|Large Attack Surface||Relatively Low Attack Surface||Low Attack Surface||Minimal Attack Surface|
Comparison of High-level Architectures
Why isn’t everyone using this already!?
Although Unikernels have been around for a while, one of the main reasons it hasn’t been in the spotlight all that much is probably owing to the fact that most applications are designed to run as multi-process systems. Migrating them to a single-process, single address-space system can be challenging to say the least. Managing and using unikernels for someone not completely familiar with the system can be challenging as well, but there are projects that are making this relatively easy.
Thanks to DevSecOps and Microservices, a lot of the complexity in Operations and Security is fairly abstracted. But, asking developers to become experts at low-level C and learn all there is about Operating Systems, and the necessary libraries to write Unikernels for Applications isn’t going to end very well. Finally, debugging and testing can be quite challenging for the average developer unless they know their way around strace, ftrace and GDB.
Remember, you’re not going to be testing just the application code anymore. If the libraries, or the language are modified to work with unikernels, those will have to be tested as well.
For Unikernels to be widely used, the complexity behind them should ideally be abstracted, and the effort to implement, deploy and manage should be seamless.
I’ve decided to use Nanos unikernel by NanoVMs and their CLI tool ops to build, and deploy. It seems like the simplest option out there, and the documentation is fairly straight-forward as well.
Additionally, the fact that they support deployments on multiple cloud-providers right out of the box seemed pretty nifty. I’m going to leverage this feature and deploy on AWS.
Ops is going package everything to create an AMI(Amazon Machine Image), which I can then launch as an EC2 instance.
Since I didn’t want to mess with my machine, I got myself a lightsail server running ubuntu 20.04 LTS. Once the
necessary dependencies(aws-cli, qemu,
golang and ops) were installed on the server, I configured aws-cli
aws configure and, created an aws s3 bucket called
s3://understanding-unikernels that’s going to help
create the AMI.
The aws user I configured on my server for aws-cli has
AdministratorAccess policy attached to it and, also needs the
vmimport policy since ops is going to be importing AMI(s). This can be done from the
AWS Management Console by attaching the
managed policy to the aws user.
AdministratorAccesspolicy. I’m only using it since I’m testing things out, and it makes things a lot easier!
Create Snapshot Image
Now that all the prerequisites have been installed, and the necessary dependencies are configured, I’m going to create a
super-simple golang static file server that’s going to serve
index.html on port 8080.
index.html are created, we can build our code to create the
The last thing that needs to be done before we’re ready to create our unikernel is the configuration file for
This file is going to contain the s3 bucket-name that was previously created(
s3://understanding-unikernels), and the aws-region to create resources.
After running all the steps mentioned above, directory structure should be similar to the one below:
ops, we can now create and list out the unikernel machine-image(s) on our AWS account.
Once the unikernel has been imported onto AWS as an AMI, we can proceed to launch it as an EC2 instance with ops. Since the static file-server is configured to run on port 8080, we’ll need to mention that along with the AWS region as part of the ops command to run the instance.
One thing I observed was that ops defaults to the Instance type
t2.micro which uses the
xen hypervisor. The
--flavor flag in ops can be used to change the Instance type. Additionally,
support for AWS Nitro instances has been added as well, but the AMI needs
ENA(Elastic Network Adapter) enabled to work with Nitro instances which can be checked by running the following command:
The final step is to delete the EC2 instance, and the AMI that was created. Doing this with ops is extremely simple.
One major security issue is the fact that unikernels run the application, and the kernel together as a single process allowing applications(or attackers) to potentially call kernel-level functions. Supply chain attacks, which are a very likely threat can make this a reality.
Nanos however manages to retain different privileges for kernel and user code by having separate partitions thereby preventing such attacks to a certain extent. Additionally, arbitrary code execution can be prevented by leveraging the
exec_protectionflag that prevents the kernel from executing any code outside the main program executable.
For those of you interested in going further down this rabbit hole, here are a few links that will help:
- The Next Generation Cloud: Unleashing the Power of the Unikernel - Russell Pavlicek
- My VM is Lighter (and Safer) than your Container
- Hacking Unikernels Through Process Injection - Ian Eyberg
- Rainbows and Unikernels - Garrett Smith
- Assessing Unikernel Security - NCC Group Whitepaper
- A Security Perspective on Unikernels
- From the Ground Up: How We Built the Nanos Unikernel - Will Jhun, NanoVMs, Inc.