---
title: "Convincing Docker and Iptables to Play Nicely"
url: "/articles/convincing-docker-and-iptables-play-nicely"
type: article
date: 2019-08-08
updated: 2021-01-12
---

# Convincing Docker and Iptables to Play Nicely

#  Convincing Docker and Iptables to Play Nicely 

 There are some networking challenges associated with Docker. This is the first in two-part series exploring the workarounds we've found through hard-won experience.

 By

 [  Ben Chavet   ](/about/ben-chavet) 

 August 8, 2019

  

If youâ&#128;&#153;ve ever tried to set up Docker on a server that also needs to have local firewall rules, you know it can be a problem. Docker wants to take over, and, rightly so, given how complicated the internal Docker networking is, but you still need some control.

There are multiple tutorials out there that try to solve this problem, and Iâ&#128;&#153;ve been through them all. Many of them are outdated, some of them work until you hit an edge case, but none that I found truly had it figured out.

My solution for this was to put my Docker servers on a private network behind another server that acts as a firewall and router, so Docker could have full control of iptables. That is still a perfectly valid approach, but there is a real cost and maintenance overhead to have those extra servers.

I now have the perfect solution; Docker gets to manage its rules, I get to manage mine, and we donâ&#128;&#153;t get in each otherâ&#128;&#153;s way.

I like to use [ferm](http://ferm.foo-projects.org/) to manage iptables. It provides some really great features like the ability to use [functions](http://ferm.foo-projects.org/download/2.4/ferm.html#Functions), [variables](http://ferm.foo-projects.org/download/2.4/ferm.html#Variables), and [lists](http://ferm.foo-projects.org/download/2.4/ferm.html#Parameters) to create complex rules with an easy-to-read syntax. It also plays nicely with configuration management tools. In version 2.4, ferm introduced the `@preserve` feature, which was the missing link to get it to play nicely with Docker.

Docker already leaves the INPUT and OUTPUT chains alone, so my default ferm config still works:

```
# /etc/ferm/ferm.conf

# Default rules
domain (ip ip6) {
    table filter {
        # Default Policies
        chain INPUT policy DROP;
        chain OUTPUT policy ACCEPT;

        # loopback traffic
        chain INPUT interface lo ACCEPT;
        chain OUTPUT outerface lo ACCEPT;

        chain (INPUT OUTPUT) {
            # ICMP is very handy and necessary
            proto icmp ACCEPT;

            # connection tracking
            mod conntrack ctstate (RELATED ESTABLISHED) ACCEPT;
        }
    }
}

# Local rules
@include ferm.d/;

```

I keep all of my rules organized in `/etc/ferm/ferm.d`, which get included in alphabetical order by the `@include` at the end of the main config files. To make sure they are included in the order I expect, I add 2-digit numbered prefixes to the files. Then, I use my configuration management to manage all of those rules.

The first set of rules that I want to be sure ferm loads uses `@preserve` to tell it to leave all of the chains that Docker wants to control alone.

```
# /etc/ferm/ferm.d/00-docker.ferm

domain (ip ip6) {
    table filter {
        chain (DOCKER DOCKER-INGRESS DOCKER-ISOLATION-STAGE-1 DOCKER-ISOLATION-STAGE-2 FORWARD) @preserve;
    }

    table nat {
        chain (DOCKER DOCKER-INGRESS PREROUTING OUTPUT POSTROUTING) @preserve;
    }
}

```

Then, I include any local rules, like allowing SSH access to the server:

```
# /etc/ferm/ferm.d/20-in.ssh.ferm

domain (ip ip6) {
    table filter chain INPUT proto tcp dport 22 ACCEPT;
}

```

I leverage the DOCKER-USER table to manage remote access to Docker services, like allowing access to an HTTP service:

```
# /etc/ferm/ferm.d/20-in.docker.http.ferm

domain (ip ip6) {
    table filter chain DOCKER-USER

    # Incoming traffic bound for a docker service will come in
    # to the FORWARD chain on eth0 and exit on docker_gwbridge
    interface eth0 outerface docker_gwbridge

    # The destination port here is the port listening IN THE DOCKER CONTAINER
    # Often times that is the same as the host port, but not always
    proto tcp dport (80 443)

    ACCEPT;
}

```

Finally, I wrap up the config by setting the default actions for external access to Docker services:

```
# /etc/ferm/ferm.d/99-docker.ferm

domain (ip ip6) table filter chain DOCKER-USER {
    interface eth0 outerface docker_gwbridge {
        mod conntrack ctstate (RELATED ESTABLISHED) ACCEPT;
        DROP;
    }
    RETURN;
}

```

With this setup, I get the best of both worlds: fully managed iptables rules that work well with my configuration management, all while letting Docker handle all of its own rules.

 Published in:

- [  System Administration   ](/topics/system-administration)

 

   You must have JavaScript enabled to use this form.

## Get in touch with us

### Tell us about your project. We'd love to hear from you!

 First Name  

 Last Name  

 Your email address  

 Your organization  

 How you heard about us  

 Tell us more 

     Leave this field blank