Docker: Restricting in- and outbound network traffic
Tags: firewall, security, docker, linux
Published: 2017-10-10 14:58:36

---
title: "Docker: Restricting in- and outbound network traffic"
published: true
description: 
tags: firewall, security, docker, linux
---

Imagine a scenario in which you might have a stinky module deep in your dependency graph. A dependency that wants to do something evil – a malware. "But I'm isolating everything in a Docker container at runtime!", you might say. Indeed, that helps when the evil module tries to mess up with your filesystem or other host related aspects. But what if the module wants to phone home?”

I thought about that problem today and want to share my approach with you. Before I headed straight into tinkering, I created the following acceptance criteria:

  1. The container should accept in- and outbound traffic from and to a known network
  2. The container should block in- and outbound traffic from and to all other networks
  3. The application within the container should run as a non-privileged user

"A privileged user is necessary for restricting network traffic." was my first thought which conflicts with the third acceptance criteria. Meh!

After implementing some test probes, I settled with the following solution:

  * Provisioning of a base image which ships with a non-privileged user and an `ENTRYPOINT` script
  * The `ENTRYPOINT` script gets executed when the container starts, defines the [iptables](http://www.netfilter.org/projects/iptables/index.html) rules and starts the given application as the configured non-privileged user afterwards.

Let's have a look at the actual solution. The __Dockerfile__:

```docker
FROM node:8-alpine

LABEL maintainer="AndrÃ© KÃ¶nig <andre.koenig@gmail.com>"

RUN apk add --update curl iptables sudo && \
    addgroup -S app && adduser -S -g app app

COPY entrypoint.sh /entrypoint.sh

ENTRYPOINT ["/entrypoint.sh", "--"]
```

As you can see, pretty straight forward: Using a base image – in this case the Node.js 8 Alpine image, adding a new user group and creating a new user. The second part copies the `entrypoint.sh` script into the container and defines it as an [ENTRYPOINT](https://docs.docker.com/engine/reference/builder/#entrypoint). Nothing special here. So let's define the actual `entrypoint.sh`:

```sh
#!/usr/bin/env sh

#
# iptables configuration
#
# The following allows in- and outbound traffic
# within a certain `CIDR` (default: `192.168.0.0/24`),
# but blocks all other network traffic.
#
ACCEPT_CIDR=${ALLOWED_CIDR:-192.168.0.0/24}

iptables -A INPUT -s $ACCEPT_CIDR -j ACCEPT
iptables -A INPUT -j DROP
iptables -A OUTPUT -d $ACCEPT_CIDR -j ACCEPT
iptables -A OUTPUT -j DROP

#
# After configuring `iptables` as root, execute
# the passed command as the non-privileged `app` user.
#
sudo -u app sh -c "$@"
```

## Usage examples

After bulding the image via `docker build -t node-sandbox .`, let's test drive this new sandboxed environment.

For example, in order to test if there is really no outbound traffic, try:

```sh
docker run --privileged -it --rm node-sandbox "curl https://google.com"
```

`curl` should give up after some time and you will see an `curl: (6) Could not resolve host: google.com` error.

On the other hand, communicating with a system in your local network should work. So if you live in the `192.168.0.0/24` network and you have something running on `192.168.0.1` (maybe your wifi router), you should see a response when executing:

```sh
docker run --privileged -it --rm node-sandbox "curl http://192.168.0.1"
```

The attentive reader might have noticed that the `entrypoint.sh` checks if the environment variable `ALLOWED_CIDR` is set and takes the `CIDR` notated network instead of the fallback:

```sh
docker run --privileged -it --rm -e "ALLOWED_CIDR=10.0.0.0/8" node-sandbox "curl http://10.0.0.1"
```

## Conclusion

One of my clients had to tranform highly sensible user data within a Node.js-based application over and over again. Because of high security standards within that organization, they anticipated an isolated network environment in which no  	possible evil dependency could ever reach out and send sensible information to an external system. Despite the fact that the application doesn't run within a container orchestrator where you have fine-grained control over the network stack, but on a user machine, this seems to be a solution that works quite well. If you face a similar situation, this approach might assist you and I'm happy to read your views.