mirror of
https://github.com/LCTT/TranslateProject.git
synced 2024-12-26 21:30:55 +08:00
481 lines
18 KiB
Markdown
481 lines
18 KiB
Markdown
[#]: subject: "Notes on running containers with bubblewrap"
|
||
[#]: via: "https://jvns.ca/blog/2022/06/28/some-notes-on-bubblewrap/"
|
||
[#]: author: "Julia Evans https://jvns.ca/"
|
||
[#]: collector: "lkxed"
|
||
[#]: translator: " "
|
||
[#]: reviewer: " "
|
||
[#]: publisher: " "
|
||
[#]: url: " "
|
||
|
||
Notes on running containers with bubblewrap
|
||
======
|
||
Hello! About a year ago I got mad about Docker container startup time. This was
|
||
because I was building an [nginx playground][1]
|
||
where I was starting a new “container” on every HTTP request, and so for it to
|
||
feel reasonably snappy, nginx needed to start quickly.
|
||
|
||
Also, I was running this project on a pretty small cloud machine (256MB RAM), a
|
||
small CPU, so I really wanted to avoid unnecessary overhead.
|
||
|
||
I’ve been looking for a way to run containers faster since then, but I couldn’t
|
||
find one until last week when I discovered
|
||
[bubblewrap][2]!! It’s very fast and I
|
||
think it’s super cool, but I also ran into a bunch of fun problems that I
|
||
wanted to write down for my future self.
|
||
|
||
#### some disclaimers
|
||
|
||
* I’m not sure if the way I’m using bubblewrap in this post is maybe not how it’s intended to be used
|
||
* there are a lot of sharp edges when using bubblewrap in this way, you need to
|
||
think a lot about Linux namespaces and how containers work
|
||
* bubblewrap is a security tool but I am not a security person and I am only
|
||
doing this for weird tiny projects. you should definitely not take security
|
||
advice from me.
|
||
|
||
Okay, all of that said, let’s talk about I’m trying to use bubblewrap to run
|
||
containers fast and in a relatively secure way :)
|
||
|
||
#### Docker containers take ~300ms to start on my machine
|
||
|
||
I ran a quick benchmark to see how long a Docker container takes to run a
|
||
simple command (`ls` ). For both Docker and Podman, it’s about 300ms.
|
||
|
||
```
|
||
$ time docker run --network none -it ubuntu:20.04 ls / > /dev/null
|
||
Executed in 378.42 millis
|
||
$ time podman run --network none -it ubuntu:20.04 ls / > /dev/null
|
||
Executed in 279.27 millis
|
||
```
|
||
|
||
Almost all of this time is overhead from docker and podman – just running `ls`
|
||
by itself takes about 3ms:
|
||
|
||
```
|
||
$ time ls / > /dev/null
|
||
Executed in 2.96 millis
|
||
```
|
||
|
||
I want to stress that, while I’m not sure exactly what the slowest part of
|
||
Docker and podman startup time is (I spent 5 minutes trying to profile them and
|
||
gave up), I’m 100% sure it’s something important.
|
||
|
||
The way we’re going to run containers faster with bubblewrap has a lot of
|
||
limitations and it’s a lower level interface which is a lot trickier to use.
|
||
|
||
#### goal 1: containers that start quickly
|
||
|
||
I felt like it *should* be possible to have containers that start essentially
|
||
instantly or at least in less than 5ms. My thought process:
|
||
|
||
* creating a new namespace with `unshare` is basically instant
|
||
* [containers are basically just a bunch of namespaces][3]
|
||
* what’s the problem?
|
||
|
||
#### container startup time is (usually) not that important
|
||
|
||
Most of the time when people are using containers, they’re running some
|
||
long-running process inside the container like a webserver, so it doesn’t
|
||
really matter if it takes 300ms to start.
|
||
|
||
So it makes sense to me that there aren’t a lot of container tools that
|
||
optimize for startup time. But I still wanted to optimize for startup time :)
|
||
|
||
#### goal 2: run the containers as an unprivileged user
|
||
|
||
Another goal I had was to be able to run my containers as an unprivileged user
|
||
instead of root.
|
||
|
||
I was surprised the first time I learned that Docker actually runs containers
|
||
as root – even though I run `docker run ubuntu:20.04` as an unprivileged user (`bork` ), that
|
||
message is actually sent to a daemon running as root, and the Docker container
|
||
process itself also runs as root (albeit a `root` that’s stripped of all its
|
||
capabilities).
|
||
|
||
That’s fine for Docker (they have lots of very smart people making sure that
|
||
they get it right!), but if I’m going to do container stuff *without* using
|
||
Docker (for the speed reasons mentioned above), I’d rather not do it as root to
|
||
keep everything a bit more secure.
|
||
|
||
#### podman can run containers as an non-root user
|
||
|
||
Before we start talking about how to do weird stuff with bubblewrap, I want to
|
||
quickly talk about a much more normal tool to run containers: podman!
|
||
|
||
Podman, unlike Docker, can run containers as an unprivileged user!
|
||
|
||
If I run this from my normal user:
|
||
|
||
```
|
||
$ podman run -it ubuntu:20.04 ls
|
||
```
|
||
|
||
it doesn’t secretly run as root behind the scenes! It just starts the container
|
||
as my normal user, and then uses something called “user namespaces” so that
|
||
*inside the container* I appear to be root.
|
||
|
||
The other cool thing aboud podman is that it has exactly the same interface as
|
||
Docker, so you can just take a Docker command and replace `docker` with
|
||
`podman` and it’ll Just Work. I’ve found that sometimes I need to do some extra
|
||
work to get podman to work in practice, but it’s still pretty nice that it has
|
||
the same command line interface.
|
||
|
||
This “run containers as a non-root user” feature is normally called “rootless
|
||
containers”. (I find that name kind of counterintuitive, but that’s what people call it)
|
||
|
||
#### failed attempt 1: write my own tool using runc
|
||
|
||
```
|
||
runc
|
||
```
|
||
|
||
I knew that Docker and podman use
|
||
[runc][4] under the hood, so I thought –
|
||
well, maybe I can just use `runc` directly to make my own tool that starts
|
||
containers faster than Docker does!
|
||
|
||
I tried to do this 6 months ago and I don’t remember most of the details, but basically
|
||
I spent 8 hours working on it, got frustrated because I couldn’t get anything
|
||
to work, and gave up.
|
||
|
||
One specific detail I remember struggling with was setting up a working `/dev`
|
||
for my programs to use.
|
||
|
||
#### enter bubblewrap
|
||
|
||
Okay, that was a very long preamble so let’s get to the point! Last week, I
|
||
discovered a tool called `bubblewrap` that was basically exactly the thing I
|
||
was trying to build with `runc` in my failed attempt, except that it actually
|
||
works and has many more features and it’s built by people who know things about
|
||
security! Hooray!
|
||
|
||
The interface to bubblewrap is pretty different than the interface to Docker –
|
||
it’s much lower level. There’s no concept of a container image – instead you
|
||
map a bunch of directories on your host to directories in the container.
|
||
|
||
For example, here’s how to run a container with the same root directory as your
|
||
host operating system, but with only read access to that root directory, and only write access to `/tmp`.
|
||
|
||
```
|
||
bwrap \
|
||
--ro-bind / / \
|
||
--bind /tmp /tmp \
|
||
--proc /proc --dev /dev \
|
||
--unshare-pid \
|
||
--unshare-net \
|
||
bash
|
||
```
|
||
|
||
For example, you could imagine running some untrusted process under bubblewrap
|
||
this way and then putting all the files you the process to access in `/tmp`.
|
||
|
||
#### bubblewrap runs containers as an unprivileged (non-root) user
|
||
|
||
Like podman, bubblewrap runs containers as a non-root user, using user
|
||
namespaces. It can also run containers as root, but in this post we’re just
|
||
going to be talking about using it as an unprivileged user.
|
||
|
||
#### bubblewrap is fast
|
||
|
||
Let’s see how long it takes to run `ls` in a bubblewrap container!
|
||
|
||
```
|
||
$ time bwrap --ro-bind / / --proc /proc --dev /dev --unshare-pid ls /
|
||
Executed in 8.04 millis
|
||
```
|
||
|
||
That’s a big difference! 8ms is a lot faster than 279ms.
|
||
|
||
Of course, like we said before, the reason bubblewrap is faster is that it does
|
||
a lot less. So let’s talk about some things bubblewrap doesn’t do.
|
||
|
||
#### some things bubblewrap doesn’t do
|
||
|
||
Here are some things that Docker/podman do that bubblewrap doesn’t do:
|
||
|
||
* set up overlayfs mounts for you, so that your changes to the filesystem don’t affect the base image
|
||
* set up networking bridges so that you can connect to a webserver inside the container
|
||
* probably a bunch more stuff that I’m not thinking of
|
||
|
||
In general, bubblewrap is a much lower level tool than something like Docker.
|
||
|
||
Also, bubblewrap seems to have pretty different goals than Docker – the README
|
||
seems to say that it’s intended as a tool for sandboxing desktop software (I
|
||
think it comes from [flatpak][5]).
|
||
|
||
#### running a container image with bubblewrap
|
||
|
||
I couldn’t find instructions for running a Docker container image with
|
||
bubblewrap, so here they are. Basically I just use Docker to download the
|
||
container image and put it into a directory and then run it with `bwrap` :
|
||
|
||
There’s also a tool called [bwrap-oci][6] which looks cool but I
|
||
couldn’t get it to compile.
|
||
|
||
```
|
||
mkdir rootfs
|
||
docker export $(docker create frapsoft/fish) | tar -C rootfs -xf -
|
||
bwrap \
|
||
--bind $PWD/rootfs / \
|
||
--proc /proc --dev /dev \
|
||
--uid 0 \
|
||
--unshare-pid \
|
||
--unshare-net \
|
||
fish
|
||
```
|
||
|
||
One important thing to note is that this doesn’t create a temporary overlay
|
||
filesystem for the container’s file writes, so it’ll let the container edit
|
||
files in the image.
|
||
|
||
I wrote a post about [overlay filesystems][7] if
|
||
you want to see how you could do that yourself though.
|
||
|
||
#### running “containers” with bubblewrap isn’t the same as with podman
|
||
|
||
I just gave an example of how to “run a container” with bubblewrap, and you
|
||
might think “cool, this is just like podman but faster!”. It is not, and it’s
|
||
actually unlike using podman in even more ways than I expected.
|
||
|
||
I put “container” in scare quotes because there are two ways to define “container”:
|
||
|
||
* something that implements [OCI runtime specification][8]
|
||
* any way of running a process in a way that’s somehow isolated from the host system
|
||
|
||
bubblewrap is a “container” tool in the second sense. It definitely provides
|
||
isolation, and it does that using the same features – Linux namespaces – as
|
||
Docker.
|
||
|
||
But it’s not a container tool in the first sense. And it’s a lower level tool
|
||
so you can get into a bunch of weird states and you really need to think about
|
||
all the weird details of how container work while using it.
|
||
|
||
For the rest of the post I’m going to talk about some weird things that can
|
||
happen with bubblewrap that would not happen with podman/Docker.
|
||
|
||
#### weird thing 1: processes that don’t exist
|
||
|
||
Here’s an example of a weird situation I got into with bubblewrap that confused
|
||
me for a minute:
|
||
|
||
```
|
||
$ bwrap --ro-bind / / --unshare-all bash
|
||
$ ps aux
|
||
... some processes
|
||
root 390073 0.0 0.0 2848 124 pts/9 S 14:28 0:00 bwrap --ro-bind / / --unshare-all --uid 0 bash
|
||
... some other processes
|
||
$ kill 390073
|
||
bash: kill: (390073) - No such process
|
||
$ ps aux | grep 390073
|
||
root 390073 0.0 0.0 2848 124 pts/9 S 14:28 0:00 bwrap --ro-bind / / --unshare-all --uid 0 bash
|
||
```
|
||
|
||
Here’s what happened
|
||
|
||
* I started a bash shell inside bubblewrap
|
||
* I ran `ps aux`, and saw a process with PID `390073`
|
||
* I try to kill the process. It fails with the error `no such process`. What?
|
||
* I ran `ps aux`, and still see the process with PID `390073`
|
||
|
||
What’s going on? Why doesn’t the process `390073` exist, even though `ps` says it does? Isn’t that impossible?
|
||
|
||
Well, the problem is that `ps` doesn’t actually list all the processes in your
|
||
current PID namespace. Instead, it iterates through all the entries in `/proc`
|
||
and prints those out. Usually, what’s in `/proc` is actually the same as the processes on your system.
|
||
|
||
But with Linux containers these things can get out of sync. What’s happening in
|
||
this example is that we have the `/proc` from the host PID namespace, but those
|
||
aren’t actually the processes that we have access to in our PID namespace.
|
||
|
||
Passing `--proc /proc` to bwrap fixes the issue – `ps` then actually lists the correct processes.
|
||
|
||
```
|
||
$ bwrap --ro-bind / / --unshare-all --dev /dev --proc /proc ps aux
|
||
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
|
||
bork 1 0.0 0.0 3644 136 ? S+ 16:21 0:00 bwrap --ro-bind / / --unshare-all --dev /dev --proc /proc ps au
|
||
bork 2 0.0 0.0 21324 1552 ? R+ 16:21 0:00 ps aux
|
||
```
|
||
|
||
Just 2 processes! Everything is normal!
|
||
|
||
#### weird thing 2: trying to listen on port 80
|
||
|
||
Passing `--uid 0` to bubblewrap makes the user inside the container `root`. You
|
||
might think that this means that the root user has administrative privileges
|
||
inside the container, but that’s not true!
|
||
|
||
For example, let’s try to listen on port 80:
|
||
|
||
```
|
||
$ bwrap --ro-bind / / --unshare-all --uid 0 nc -l 80
|
||
nc: Permission denied
|
||
```
|
||
|
||
What’s going on here is that the new root user actually doesn’t have the
|
||
**capabilities** it needs to listen on port 80. (you need special permissions
|
||
to listen on ports less than 1024, and 80 is less than 1024)
|
||
|
||
There’s actually a capability specifically for listening on privileged ports
|
||
called `CAP_NET_BIND_SERVICE`.
|
||
|
||
So to fix this all we need to do is to tell bubblewrap to give our user that
|
||
capability.
|
||
|
||
```
|
||
$ bwrap --ro-bind / / --unshare-all --uid 0 --cap-add cap_net_bind_service nc -l 80
|
||
(no output, success!!!)
|
||
```
|
||
|
||
This works! Hooray!
|
||
|
||
#### finding the right capabilities is pretty annoying
|
||
|
||
bubblewrap doesn’t give out any capabilities by default, and I find that
|
||
figuring out all the right capabilities and adding them manually is kind of
|
||
annoying. Basically my process is
|
||
|
||
* run the thing
|
||
* see what fails
|
||
* read `man capabilities` to figure out what capabilities I’m missing
|
||
* add the capability with `--cap-add`
|
||
* repeat until everything is running
|
||
|
||
But that’s the price I pay for wanting things to be fast I guess :)
|
||
|
||
#### weird thing 2b: --dev /dev makes listening on privileged ports not work
|
||
|
||
```
|
||
--dev /dev
|
||
```
|
||
|
||
One other strange thing is that if I take the exact same command above (which
|
||
worked!) and add `--dev /dev` (to set up the `/dev/` directory), it causes it to not work again:
|
||
|
||
```
|
||
$ bwrap --ro-bind / / --dev /dev --unshare-all --uid 0 --cap-add cap_net_bind_service nc -l 80
|
||
nc: Permission denied
|
||
```
|
||
|
||
I think this might be a bug in bubblewrap, but I haven’t mustered the courage
|
||
to dive into the bubblewrap code and start investigating yet. Or maybe there’s
|
||
something obvious I’m missing!
|
||
|
||
#### weird thing 3: UID mappings
|
||
|
||
Another slightly weird thing was – I tried to run `apt-get update` inside a bubblewrap Ubuntu container and everything went very poorly.
|
||
|
||
Here’s how I ran `apt-get update` inside the Ubuntu container:
|
||
|
||
```
|
||
mkdir rootfs
|
||
docker export $(docker create ubuntu:20.04) | tar -C rootfs -xf -
|
||
bwrap \
|
||
--bind $PWD/rootfs / \
|
||
--proc /proc\
|
||
--uid 0 \
|
||
--unshare-pid \
|
||
apt-get update
|
||
```
|
||
|
||
And here are the error messages:
|
||
|
||
```
|
||
E: setgroups 65534 failed - setgroups (1: Operation not permitted)
|
||
E: setegid 65534 failed - setegid (22: Invalid argument)
|
||
E: seteuid 100 failed - seteuid (22: Invalid argument)
|
||
E: setgroups 0 failed - setgroups (1: Operation not permitted)
|
||
.... lots more similar errors
|
||
```
|
||
|
||
At first I thought “ok, this is a capabilities problem, I need to set
|
||
`CAP_SETGID` or something to give the container permission to change groups. But I did that and it didn’t help at all!
|
||
|
||
I think what’s going on here is a problem with UID maps. What are UID maps?
|
||
Well, every time you run a container using “user namespaces” (which podman is
|
||
doing), it creates a mapping of UIDs inside the container to UIDs on the host.
|
||
|
||
Let’s look that the UID maps! Here’s how do that:
|
||
|
||
````
|
||
[[email protected]][9]:/# cat /proc/self/uid_map
|
||
0 1000 1
|
||
[[email protected]][10]:/# cat /proc/self/gid_map
|
||
1000 1000 1
|
||
|
||
```
|
||
This is saying that user 0 in the container is mapped to user 1000 on in the
|
||
host, and group 1000 is mapped to group 1000. (My normal user's UID/GID is 1000, so this makes sense). You can find out
|
||
about this `uid_map` file in `man user_namespaces`.
|
||
|
||
All other users/groups that aren't 1000 are mapped to user 65534 by default, according
|
||
to `man user_namespaces`.
|
||
|
||
### what's going on: non-mapped users can't be used
|
||
|
||
The only users and groups that have been mapped are `0` and `1000`. But `man user_namespaces` says:
|
||
|
||
> After the uid_map and gid_map files have been written, only the mapped values may be used in system calls that change user and group IDs.
|
||
|
||
`apt` is trying to use users 100 and 65534. Those aren't on the list of mapped
|
||
users! So they can't be used!
|
||
|
||
This works fine in podman, because podman sets up its UID and GID mappings differently:
|
||
```
|
||
|
||
$ podman run -it ubuntu:20.04 bash
|
||
[[email protected]][11]:/# cat /proc/self/uid_map
|
||
0 1000 1
|
||
1 100000 65536
|
||
[[email protected]][12]:/# cat /proc/self/gid_map
|
||
0 1000 1
|
||
1 100000 65536
|
||
```
|
||
|
||
All the users get mapped, not just 1000.
|
||
|
||
I don’t quite know how to fix this, but I think it’s probably possible in
|
||
bubblewrap to set up the uid mappings the same way as podman does – there’s an
|
||
[issue about it here that links to a workaround][13].
|
||
|
||
But this wasn’t an actual problem I was trying to solve so I didn’t dig further
|
||
into it.
|
||
|
||
#### it works pretty great!
|
||
|
||
I’ve talked about a bunch of issues, but the things I’ve been trying to do in bubblewrap
|
||
have been very constrained and it’s actually been pretty simple. For example, I
|
||
was working on a git project where I really just want to run `git` inside a
|
||
container and map a git repository from the host.
|
||
|
||
That’s very simple to get to work with bubblewrap! There were basically no weird problems!
|
||
It’s really fast!
|
||
|
||
So I’m pretty excited about this tool and I might use it for more stuff in the
|
||
future.
|
||
|
||
--------------------------------------------------------------------------------
|
||
|
||
via: https://jvns.ca/blog/2022/06/28/some-notes-on-bubblewrap/
|
||
|
||
作者:[Julia Evans][a]
|
||
选题:[lkxed][b]
|
||
译者:[译者ID](https://github.com/译者ID)
|
||
校对:[校对者ID](https://github.com/校对者ID)
|
||
|
||
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
|
||
|
||
[a]: https://jvns.ca/
|
||
[b]: https://github.com/lkxed
|
||
[1]: https://jvns.ca/blog/2021/09/24/new-tool--an-nginx-playground/
|
||
[2]: https://github.com/containers/bubblewrap
|
||
[3]: https://jvns.ca/blog/2016/10/10/what-even-is-a-container/
|
||
[4]: https://github.com/opencontainers/runc
|
||
[5]: https://flatpak.org/
|
||
[6]: https://github.com/projectatomic/bwrap-oci
|
||
[7]: https://jvns.ca/blog/2019/11/18/how-containers-work--overlayfs/
|
||
[8]: https://opencontainers.org/about/overview/
|
||
[9]: https://jvns.ca/cdn-cgi/l/email-protection
|
||
[10]: https://jvns.ca/cdn-cgi/l/email-protection
|
||
[11]: https://jvns.ca/cdn-cgi/l/email-protection
|
||
[12]: https://jvns.ca/cdn-cgi/l/email-protection
|
||
[13]: https://github.com/containers/bubblewrap/issues/468
|