10 releases (6 stable)

1.3.0 Jan 7, 2024
1.2.1 Dec 13, 2020
1.2.0 Jul 13, 2020
1.0.1 Sep 28, 2019
0.0.1-placeholder Jun 1, 2017

#315 in Network programming

MIT/Apache

220KB
3.5K SLoC

DFW - Docker Firewall Framework in Rust

  1. Overview
    1. Example
  2. Getting started
  3. Configuration
  4. Troubleshooting
  5. IPv6 support
    1. Example: webserver reachable via IPv6
  6. Breaking changes
    1. Coming from v0.x to v1.x
  7. Supported architectures
  8. Supported Docker versions
  9. Version bump policy
  10. License
    1. Contribution

Overview

DFW is conceptually based on the Docker Firewall Framework, DFWFW. Its goal is to make firewall administration with Docker simpler, but also more extensive by trying to replace the Docker built-in firewall handling.

This is accomplished by a flexible configuration that defines how the firewall should be built up. While DFW is running, Docker container events will be monitored and the rules rebuilt when necessary.

One of the key-features of DFW (and DFWFW before it) is to not require the running containers to publish their ports on the host (à la docker container run --publish 80:8080), but rather use the network-address translation (NAT) features of the host-firewall to forward packets directly to the port in the container.1

DFW supports the following firewall backends:

  • iptables
  • nftables

You can choose the one that works best for you.2

1 This only applies if you use IPv4 on your host. If you want to have IPv6-support, you still need to publish the ports. See [IPv6 support](#configuration-ipv6) for more information.
2 Please make sure to not mix firewall-backends: if you are already using one on your host, do not use the other one with DFW.

Example

Assume that you want to run a reverse proxy in Docker that should proxy traffic to a web-application that is also running in Docker. With the regular tools provided by Docker you would simply host-bind the port, put the two Docker containers on the same Docker network and would have a working solution.

While this works quite well, having Docker handling the firewall rules has a few potential drawbacks:

  1. Traffic to host-mounted ports is not restricted by default.

    While this usually is the desired behaviour, it might hurt you if you just want to launch a service and test it locally.3

    With DFW you have to be explicit: if you want your service to be reachable, you have to configure exactly which container should be reachable from where. While this does incur an upfront cost in terms of effort, it can reward you afterwards by ensuring you don't accidentally expose a service you didn't intend to expose.

  2. Traffic between containers on the same Docker network is not restricted.

    Again: most of the time this is the desired behaviour. When it isn't though, Docker does not give you the tools to restrict this traffic.

    DFW allows you to configure exactly how you want containers to be able to communicate with each other, both in the same Docker network and across Docker networks.

    In the example above we want the reverse proxy to communicate with the web-application, but the web-application should not be able to initiate a connection to the reverse proxy. DFW allows you to implement this scenario.

If you have not encountered the drawbacks described above and are happy with the features provided by Docker, you might not need DFW. But if you have, or are simply interested in trying DFW out, take a look at the reverse proxy example which will work you through the proposed example.

3 You can of course bind the port to `127.0.0.1`, but you have to be explicit about that, which is easy to forget.

Getting started

If you are starting fresh, the first step is to decide on a firewall backend:

  • nftables

    nftables can be seen as a newer generation of iptables, and it will replace iptables in most Linux distributions at some point. (It already is the default in e.g. Debian 10 Buster.)

    If you are starting fresh on a host where you have not used either backend yet, nftables is the suggested backend.

  • iptables

    While iptables is the older netfilter implementation, it is still a valid firewall-backend and still finds extensive use across many distributions.

    If you are already using iptables and have a configuration that you don't want to re-do, feel free to use the iptables backend with DFW.

Once you have decided which backend you want to use, please consult the backend-specific documentation on how to proceed further:

If you are already a user of DFW and are looking to upgrade to a newer version, consult the matching migration documentation:

Configuration

The general configuration happens across six categories:

  • global_defaults

    This category defines global, default values to be used by DFW and the other categories.

    Field reference.

  • backend_defaults

    This category defines configuration values that are specific to the firewall-backend used.

    Field reference for nftables.

    Field reference for iptables.

  • container_to_container

    This controls the communication between containers and across Docker networks.

    Field reference.

  • container_to_wider_world

    This controls if and how containers may access the wider world, i.e. what they can communicate across the OUTPUT chain on the host.

    Field reference.

  • container_to_host

    To restrict or allow access to the host, this section is used.

    Field reference.

  • wider_world_to_container

    This controls how the wider world, i.e. whatever comes in through the INPUT chain on the host, can communicate with a container or a Docker network.

    Field reference.

  • container_dnat

    This category allows you to define specific rules for destination network address translation, even or especially across Docker networks.

    Field reference.

See the examples and configuration types for detailed descriptions and examples of every configuration section.

Additionally, you can configure general behavior of DFW using command-line arguments, which are described by executing dfw --help:

dfw
Docker firewall framework, in Rust

USAGE:
    dfw [OPTIONS]

OPTIONS:
        --burst-timeout <TIMEOUT>
            Time to wait after a event was received before processing the rules, in milliseconds

            [default: 500]

    -c, --config-file <FILE>
            Set the configuration file

        --check-config
            Verify if the provided configuration is valid, exit afterwards.

        --config-path <PATH>
            Set a path with multiple TOML configuration files

        --container-filter <FILTER>
            Filter the containers to be included during processing

            [default: running]

    -d, --docker-url <URL>
            Set the URL to the Docker instance (e.g. unix:///tmp/docker.sock)

        --disable-event-monitoring
            Disable event monitoring

        --dry-run
            Don't touch firewall-rules, just show what would be done. Note that this requires Docker
            and the containers/networks referenced in the configuration to be available. If you want
            to check the config for validity, specify --check-config instead.

        --firewall-backend <BACKEND>
            Select the firewall-backend to use

            [default: nftables]
            [possible values: nftables, iptables]

    -h, --help
            Print help information

    -i, --load-interval <INTERVAL>
            Interval between rule processing runs, in seconds (0 = disabled)

            [default: 0]

        --log-level <SEVERITY>
            Define the log level

            [default: info]

    -m, --load-mode <MODE>
            Define if the config-fields get loaded once, or before every run

            [default: once]
            [possible values: once, always]

        --run-once
            Process rules once, then exit.

    -V, --version
            Print version information

Troubleshooting

If you are experiencing issues with DFW, you can consult the troubleshooting documentation for known potential obstacles. If you don't find your issue covered, feel free to open a GitHub issue describing your problem.

IPv6 support

If you make a container publicly available, DFW will use "destination NATting" and "masquerading" to redirect incoming packets to the correct internal IP of the container, and then correctly redirect the reponses back to the original requester. Every default installation of Docker does not assign private IPv6 addresses to networks and containers, it only assigns private IPv4s.

Generally there is also no need for private IPv6 addresses: Docker uses a proxy-binary when host-binding a container-port to perform the translation of traffic from the host to the container. This host-binding is compatible with both IPv4 and IPv6, which means internally a single IPv4 is sufficient.

As mentioned, DFW does work differently: since it uses NAT to manage traffic, it effectively would have to translate incoming packets from IPv6 to IPv4 and the responses from IPv4 to IPv6, something that is not supported by nftables and iptables.

The consequence of this is that if you want your services to be reachable via IPv6, you have to ensure the following things:

  1. You have to publish the ports of the containers you want to be able to reach on your host through the Docker-integrated run-option --publish.

    The host-port you select here is the one under which it will be reachable publicly later, i.e. if you want your webserver to be reachable from host-ports 80 and 443, you need to publish the container ports under 80 and 443.

  2. In your wider-world-to-container rule, the host-port part of your exposed port must match the port you published the container ports under (although it doesn't have to match the container-port itself).

    As part of the wider-world-to-container rule DFW will create the firewall-rules necessary for the host-bound ports to be reachable via IPv6. For this to work the ports need to match the ports you have selected when publishing the container-ports.

    (If you are having trouble, make sure you don't have expose_via_ipv6 set to false in your wider-world-to-container rule.)

Example: webserver reachable via IPv6

Let's assume you want to run a webserver as a Docker container and want ports 80 for HTTP and 443 for HTTPS on your host to forward to this container. The container you use internally uses ports 8080 and 8443 for HTTP and HTTPS respectively.

The following is how you have to configure the container:

$ docker run \
    --name "your_container" \
    --network "your_network" \
    --publish 80:8080 \
    --publish 443:8443 \
    ...

This is how you'd configure your rule:

[[wider_world_to_container.rules]]
network = "your_network"
dst_container = "your_container"
expose_port = [
    "80:8080",
    "443:8443",
]

The result of this is that your container will be reachable from the host-ports 80 and 443, from both IPv4 and IPv6.

Breaking changes

Coming from v0.x to v1.x

Starting with version 1.0, DFW introduced the nftables backend and made it the default firewall-backend used. If you are upgrading DFW but don't want to switch to nftables, you can provide the --firewall-backend iptables parameter to DFW (this requires at least DFW v1.2).

Please note that no matter if you transition to nftables or not, v1.0 introduced breaking changes to the configuration. Please consult the migration documentation on how to update your configuration.

Supported architectures

The Docker image for DFW is pre-built for the following architectures:

  • amd64 (a.k.a. x86_64)
  • arm64 (a.k.a. aarch64)
  • arm/v7 (specifically armhf)

You don't have to do anything special to use the correct architecture: just docker pull pitkley/dfw:1.3.0. Docker will take care of pulling the image that matches the architecture of your host.

In general, DFW should be able to run on any architecture that Rust supports and for which the nftables or iptables binaries exist.

Supported Docker versions

At least Docker 1.13.0 is required.

DFW is continuously and automatically tested with the following stable Docker versions (using the latest patch-version each):

  • 24.0
  • 23.0
  • 20.10
  • 19.03
  • 18.09
  • 18.06

Docker versions between 18.03 and 1.13 should also work, but are not automatically tested anymore due to lacking Docker BuildKit support.

Version bump policy

In general, the versioning scheme for DFW follows the semantic versioning guidelines:

  • The patch version is bumped when backwards compatible fixes are made (this includes updates to dependencies).
  • The minor version is bumped when new features are introduced, but backwards compatibility is retained.
  • The major version is bumped when a backwards incompatible change was made.

Special cases:

  • A bump in the minimum supported Rust version (MSRV), which for DFW currently is 1.67.0, will be done in minor version updates (i.e. they do not require a major version bump).

  • DFW is available both as a binary for direct use and as a library on crates.io.

    The target audience of DFW are the users of the binary, and support for the library's public API is only provided on a best-effort basis.

    Thus, changes that break the API of the library will be done in minor version updates, i.e. consumers of the library might have to expect breaking changes in non-major releases.

License

DFW is licensed under either of

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in DFW by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

Dependencies

~20–36MB
~558K SLoC