44

My goal is to limit access to docker containers to just a few public IP addresses. Is there a simple, repeatable process to accomplish my goal? Understanding only the basics of iptables while using Docker's default options, I'm finding it very difficult.

I'd like to run a container, make it visible to the public Internet, but only allow connections from select hosts. I would expect to set a default INPUT policy of REJECT and then only allow connections from my hosts. But Docker's NAT rules and chains get in the way and my INPUT rules are ignored.

Can somebody provide an example of how to accomplish my goal given the following assumptions?

  • Host public IP 80.80.80.80 on eth0
  • Host private IP 192.168.1.10 on eth1
  • docker run -d -p 3306:3306 mysql
  • Block all connection to host/container 3306 except from hosts 4.4.4.4 and 8.8.8.8

I'm happy to bind the container to only the local ip address but would need instructions on how to set up the iptables forwarding rules properly which survive docker process and host restarts.

Thanks!

GGGforce
  • 669
  • 2
  • 7
  • 10

5 Answers5

48

Two things to bear in mind when working with docker's firewall rules:

  1. To avoid your rules being clobbered by docker, use the DOCKER-USER chain
  2. Docker does the port-mapping in the PREROUTING chain of the nat table. This happens before the filter rules, so --dest and --dport will see the internal IP and port of the container. To access the original destination, you can use -m conntrack --ctorigdstport.

For example:

iptables -A DOCKER-USER -i eth0 -s 8.8.8.8 -p tcp -m conntrack --ctorigdstport 3306 --ctdir ORIGINAL -j ACCEPT
iptables -A DOCKER-USER -i eth0 -s 4.4.4.4 -p tcp -m conntrack --ctorigdstport 3306 --ctdir ORIGINAL -j ACCEPT
iptables -A DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport 3306 --ctdir ORIGINAL -j DROP

NOTE: Without --ctdir ORIGINAL, this would also match the reply packets coming back for a connection from the container to port 3306 on some other server, which is almost certainly not what you want! You don't strictly need this if like me your first rule is -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT, as that will deal with all the reply packets, but it would be safer to still use --ctdir ORIGINAL anyway.

SystemParadox
  • 827
  • 9
  • 14
  • Should this be edited [to include the](https://github.com/moby/moby/issues/22054#issuecomment-466663033) `--ctdir`? I use `-m conntrack --ctstate NEW --ctorigdstport 3306 --ctdir ORIGINAL` – lonix Sep 15 '19 at 19:41
  • @Ionix, yes it should, although I've only just worked out why it's been confusing me. I've added a bit of explanation. – SystemParadox Sep 16 '19 at 11:04
  • 8
    Note the default `DOCKER-USER` table contains the entry: `-A DOCKER-USER -j RETURN` which will run before the above if you use `-A`. A solution is to insert rules at the head in reverse order with `-I`. – BMitch Sep 16 '19 at 18:29
  • 1
    @BMitch Or [even better yet](https://unrouted.io/2017/08/15/docker-firewall/), add all rules in a new `FILTERS` chain, and `-I` insert new rules (like you said), to jump to it: `-I INPUT -j FILTERS` and `-I DOCKER-USER -i eth0 -j FILTERS` – lonix Sep 17 '19 at 11:02
  • @BMitch However, I just checked my server, and the return rule is not there, maybe the latest docker version no longer inserts it? Good idea to use `-I` though, just to be safe. – lonix Sep 17 '19 at 11:06
  • @lonix I'm seeing the return rule in 19.03.1 and 18.09.7 on Debian and Ubuntu respectively. I mainly point it out because I've had others point out my similar answer didn't work for them until switching to `-I`. – BMitch Sep 17 '19 at 13:06
  • 1
    I first had a look at this help page from docker: https://docs.docker.com/network/iptables/ and just managed to block my entire LAN network. How great!? Your solution, however, worked. What I'd like to know, though, is whether we could block all ports instead of just a few. Would it work as expected without the `--ctorigdstport`? Then I can open a few ports (placing a few rules before this one) if necessary and then have everything else blocked. – Alexis Wilke Feb 21 '20 at 01:54
  • the way I could block all incoming to my MySQL was to set just on top of the default RETURN in DOCKER-USER the following: ```iptables -I DOCKER-USER -i eth0 -j DROP```. All the incoming traffic to my docker host generates on eth0, so a bottom rule like this worked well as far as I know. – RicarHincapie Sep 30 '21 at 15:45
11

With Docker v.17.06 there is a new iptables chain called DOCKER-USER. This one is for your custom rules: https://docs.docker.com/network/iptables/

Unlike the chain DOCKER it is not reset on building/starting containers. So you could add these lines to your iptables config/script for provisioning the server even before installing docker and starting the containers:

-N DOCKER
-N DOCKER-ISOLATION
-N DOCKER-USER
-A DOCKER-ISOLATION -j RETURN
-A DOCKER-USER -i eth0 -p tcp -m tcp --dport 3306 -j DROP
-A DOCKER-USER -j RETURN

Now the port for MySQL is blocked from external access (eth0) even thought docker opens the port for the world. (These rules assume, your external interface is eth0.)

Eventually, you will have to clean up iptables restart the docker service first, if you messed it too much trying to lock down the port as I did.

ck1
  • 119
  • 1
  • 2
  • I miss why this DOCKER-USER table is any different than any other user-added table.. It has no filter pre-applied to it so you still have to specify the interface names yourself. If you create a "MY-CHAIN" and insert it into the FORWARD chain it will have the same result, no? – ColinM Dec 04 '17 at 18:13
  • Yes, it makes a difference, because Docker inserts the DOCKER-USER chain into the FORWARD chain: `-A FORWARD -j DOCKER-USER` `-A FORWARD -j DOCKER-ISOLATION` That's why, the custom instructions are executed before the DOCKER chain. – ck1 Dec 05 '17 at 18:26
  • 2
    Note that if you use `--dport` inside DOCKER-USER this must match the *internal* IP of the container service, *not* the exposed port. These often match but not always and this could easily conflict with other services so I still argue that this DOCKER-USER solution is half-baked. – ColinM Dec 06 '17 at 22:03
  • @ColinM could this be fixed by using ctorigdstport instead? – IceFire Oct 13 '21 at 08:49
5

UPDATE: While valid in 2015, this solution is no longer the right way to do it.

The answer seems to be in Docker's documentation at https://docs.docker.com/articles/networking/#the-world

Docker’s forward rules permit all external source IPs by default. To allow only a specific IP or network to access the containers, insert a negated rule at the top of the DOCKER filter chain. For example, to restrict external access such that only source IP 8.8.8.8 can access the containers, the following rule could be added: iptables -I DOCKER -i ext_if ! -s 8.8.8.8 -j DROP

What I ended up doing was:

iptables -I DOCKER -i eth0 -s 8.8.8.8 -p tcp --dport 3306 -j ACCEPT
iptables -I DOCKER -i eth0 -s 4.4.4.4 -p tcp --dport 3306 -j ACCEPT
iptables -I DOCKER 3 -i eth0 -p tcp --dport 3306 -j DROP

I didn't touch the --iptables or --icc options.

GGGforce
  • 669
  • 2
  • 7
  • 10
  • 1
    If you do `iptables -vnL DOCKER`, the destination ports are all ports within the container. If I get that right, that means the rules above would only affect the port `3306` within the container - that is, if you were to `-p 12345:3306` your container, your rule would _still_ be the one required to lock down the access (i.e. `--dport 12345` wouldn't work), because the DOCKER chain's ACCEPT rules are post-NAT. – sunside Nov 06 '15 at 13:31
  • That's right, the rules need to relate to the ports within the containers. – GGGforce Nov 06 '15 at 13:35
  • 1
    Hum, that's sort of ugly if you happen to run multiple containers that use, say, an internal NGINX to do reverse proxying (e.g. Zabbix, a custom load balancer, etc.) because it would require you to know the container's IP in advance. I'm still searching to a solution for that problem that does not require `--iptables=false`, because this seems to be the worst choice of them all. – sunside Nov 06 '15 at 13:38
  • Thank you! You've solved my issue after many hours of searching. Now I'm finally able to jail MySQL just to my home IP address without exposing the soft underbelly to the entire world. – Matt Cavanagh Jul 16 '17 at 19:45
  • 2
    The DOCKER chain is not supposed to be directly manipulated by the user! Use the DOCKER-USER chain for that. Check the accepted answer. – Paul-Sebastian Nov 08 '18 at 19:56
  • To re-enforce Paul-Sebastian Manoie's comment, when you restart docker (`systemctl restart docker`) your rules are gone. Time to re-instate them gives hacker time to access your normally blocked ports. – Alexis Wilke Feb 21 '20 at 01:57
4

UPDATE: While this answer is still valid the answer by @SystemParadox using DOCKER-USER in combination with --ctorigdstport is better.

Here is a solution which persists well between restarts and allows you to affect the exposed port rather than the internal port.

iptables -t mangle -N DOCKER-mysql
iptables -t mangle -A DOCKER-mysql -s 22.33.44.144/32 -j RETURN
iptables -t mangle -A DOCKER-mysql -s 22.33.44.233/32 -j RETURN
iptables -t mangle -A DOCKER-mysql -j DROP
iptables -t mangle -A PREROUTING -i eth0 -p tcp -m tcp --dport 3306 -j DOCKER-mysql

I've built a Docker image that uses this method to automatically manage the iptables for you, using either environment variables or dynamically with etcd (or both):

https://hub.docker.com/r/colinmollenhour/confd-firewall/

Alexis Wilke
  • 2,057
  • 1
  • 18
  • 33
ColinM
  • 691
  • 8
  • 19
1

Building on the excellent accepted answer by @SystemParadox, I wanted to prevent all traffic (TCP and UDP) from all external hosts to the published ports of all running containers, since I'll be reverse proxying to the container ports that I actually want to access from the outside.

The rule that worked for me to achieve this:

iptables -I DOCKER-USER -i eth0 -m conntrack --ctdir ORIGINAL -j DROP

This inserts the rule to the top of the DOCKER-USER chain. Adjust accordingly with the actual name(s) of your external network interface(s), or the desired positions in the chain (with -I DOCKER-USER <number>).

The conntrack module with ctdir seems to be important here, since I had some problems with docker-composed containers losing access to the external world with variants I tried without them. For example: iptables -I DOCKER-USER -i eth0 ! -s 127.0.0.1 -j DROP, suggested by Jeff Geerling in a blog post from 2020, itself sourced from the official docs, prevented my containers from communicating with external IPs.

JK Laiho
  • 195
  • 9