Understanding Unikernels: The Future of Cloud Computing, Probably
Tags: security, cloud, cybersecurity, unikernels
Published: 2021-02-12 15:48:28

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:

> **Single purpose compute environments packaged with necessary dependencies, runtime, libraries, kernel capabilities and everything else required boot and run Application code, and System code in a single address space with No Operating System, No Users and No Shell resulting in smaller, faster and a more secure system.**

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.

**Summary**:
* No Shell
* No Users
* Single Process
* Does not need underlying Host OS(typically runs on Hypervisor)
* Decreased Attack Surface

### Types of Unikernels

#### Clean Slate

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

#### Legacy

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

| VMs | Containers | Lightweight VMs | 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

![VMs vs. Containers vs. Lightweight VMs vs. Unikernels](https://dev-to-uploads.s3.amazonaws.com/i/c6exr95fy4998bexxm4t.png)

---

## 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 bequite 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.

---

## Implementing Unikernels

I've decided to use [Nanos](https://nanos.org/) unikernel by [NanoVMs](https://nanovms.com/?via=nithinjois) and their CLI tool [ops](https://ops.city/) 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](https://nanos.org/thebook#infrastructure) 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.

> **Disclaimer:** *I'm a tiny, tiny investor in [NanoVMs](https://nanovms.com/?via=nithinjois).*

### Prerequisites

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](https://aws.amazon.com/cli/), [qemu](https://www.qemu.org/download/), [golang](https://golang.org/doc/install) and [ops](https://ops.city/)) were installed on the server, I configured aws-cli by running `aws configure` and, created an aws s3 bucket called `s3://understanding-unikernels` that's going to help create the AMI.

_prerequisites.sh_

```bash
apt update && apt -y upgrade
echo "Installing golang"
wget https://golang.org/dl/go1.15.7.linux-amd64.tar.gz && tar -C /usr/local -xzf go1.15.7.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin
echo "Installing AWS-CLI and qemu"
apt install -y awscli qemu
echo "Installing ops"
curl https://ops.city/get.sh -sSfL | sh
```

![Installed prerequisites and respective versions](https://dev-to-uploads.s3.amazonaws.com/i/gbc4gp5osznaurc3bu1s.png)

### IAM Permissions

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](https://console.aws.amazon.com/iam/home?region=us-west-2#/users) by attaching the `vmimport` managed policy to the aws user.

> I definitely *do not* recommend using `AdministratorAccess` policy. I'm only using it since I'm testing things out, and it makes things a lot easier!

> If **Cloud Security**, **Containers**, **Kubernetes Security** and **DevSecOps** is something you're interested in, be sure to check out [AppSecEngineer](https://appsecengineer.com/?via=nithinjois) for self-paced trainings with state-of-the-art hands-on Labs!

![IAM Permissions](https://dev-to-uploads.s3.amazonaws.com/i/job7o0oocdxnbjyabovz.png)

### 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.

_server.go_

```go
package main

import (
    "log"
    "net/http"
)

func main() {
    fs := http.FileServer(http.Dir("static"))
    http.Handle("/", fs)

    log.Println("Listening...on 8080")
    http.ListenAndServe(":8080", nil)
}
```

_static/index.html_

```html
<!doctype html>
<html>
<head>
<meta charset="utf-8">
    <title>Hello!</title>
</head>
<body>
    <h1>Understanding Unikernels!</h1>
</body>
</html>
```

Once `server.go` and `index.html` are created, we can build our code to create the `server` executable.

```bash
GOOS=linux go build server.go
```

The last thing that needs to be done before we're ready to create our unikernel is the configuration file for `ops`. This file is going to contain the s3 bucket-name that was previously created(`s3://understanding-unikernels`), and the aws-region to create resources.

_config.json_

```json
{
    "Dirs" : ["static"],
    "CloudConfig" :{
        "ProjectID" :"test-project",
        "Zone": "<region>",
        "BucketName":"<bucket-name>"
    },
    "RebootOnExit": true
}
```

After running all the steps mentioned above, directory structure should be similar to the one below:

_directory structure_

```
understanding-unikernels/
├── config.json
├── server
├── server.go
└── static
    └── index.html
```

By leveraging `ops`, we can now create and list out the unikernel machine-image(s) on our AWS account.

```bash
ops image create -t aws -c config.json -a server

ops image list -t aws -z <region>
```

![Create Snapshot Image](https://dev-to-uploads.s3.amazonaws.com/i/l683dyc5qocmftw1cww3.png)

### Create Instance

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.

```bash
ops instance create -c config.json -t aws -z <region> -i server-image --port 8080
```

![Create Instance](https://dev-to-uploads.s3.amazonaws.com/i/9k0pm6j5glame7ryal5e.png)

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](https://github.com/nanovms/nanos/pull/1365) 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:

```bash
aws ec2 describe-images --image-id <ami-id> --query "Images[].EnaSupport"
```

### Delete Instance

The final step is to delete the EC2 instance, and the AMI that was created. Doing this with ops is extremely simple.

```bash
ops instance delete -t aws -z <region> <instance-id>

ops instance list -t aws -z <region>

ops image delete <ami-name> -t aws -z <region> 
```

![Delete Instance](https://dev-to-uploads.s3.amazonaws.com/i/m4vzkp6gey2q7bjo4ucy.png)

---

## Security Considerations

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_protection` flag that [prevents the kernel from executing any code outside the main program executable.](https://github.com/nanovms/nanos/pull/1251)

---

## Interesting Projects

> *If you are working on a cool unikernel project that's not mentioned below, please reach out to me. I'd love to give it a shot and add it to the list!*

- [ToroKernel](https://torokernel.io/)
- [Nanos](https://nanos.org/)
- [MirageOS](https://mirage.io/)
- [IncludeOS](https://www.includeos.org/)
- [OSv](http://osv.io/)
- [Graphene](https://grapheneproject.io/)
- [MiniOS](https://wiki.xenproject.org/wiki/Mini-OS)

---

## References

> 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](https://www.youtube.com/watch?v=8UgiPODw3CY)
- [My VM is Lighter (and Safer) than your Container](http://cnp.neclab.eu/projects/lightvm/lightvm.pdf)
- [Hacking Unikernels Through Process Injection - Ian Eyberg](https://hackernoon.com/hacking-unikernels-through-process-injection-a-step-by-step-guide-oobk323f)
- [Rainbows and Unikernels - Garrett Smith](https://www.youtube.com/watch?v=cUvNths_5RA)
- [Assessing Unikernel Security - NCC Group Whitepaper](https://www.nccgroup.trust/globalassets/our-research/us/whitepapers/2019/ncc_group-assessing_unikernel_security.pdf)
- [A Security Perspective on Unikernels](https://arxiv.org/abs/1911.06260)
- [From the Ground Up: How We Built the Nanos Unikernel - Will Jhun, NanoVMs, Inc.](https://www.youtube.com/watch?v=0v21hGvCDPY)

---

*Originally published at https://nithinjois.substack.com.*