Skip to content

Instantly share code, notes, and snippets.

@liejuntao001
Last active May 10, 2020 09:30
Show Gist options
  • Save liejuntao001/266f2c5a5e85be70201eee9bcbd2b4a4 to your computer and use it in GitHub Desktop.
Save liejuntao001/266f2c5a5e85be70201eee9bcbd2b4a4 to your computer and use it in GitHub Desktop.
*filter
:INPUT ACCEPT [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
:FILTERS - [0:0]
:DOCKER-USER - [0:0]
-F INPUT
-F DOCKER-USER
-F FILTERS
# BASIC Allow
-A INPUT -i lo -j ACCEPT
# Chain to FILTERS
-A INPUT -j FILTERS
-A DOCKER-USER -i eth0 -j FILTERS
# COMMON FIREWALL RULES
# ALLOW something
-A FILTERS -p tcp -m multiport --dports 80,443 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT
# DENY something
-A FILTERS -p icmp --icmp-type echo-request -j REJECT
###################################################################
### special cases for servers
### please modify by the server
### end special cases
############################################################
# FINAL REJECT
# Optional logging
-A FILTERS -m limit --limit 5/min -j LOG --log-prefix "iptables_INPUT_denied: " --log-level 7
-A FILTERS -j REJECT
COMMIT
@jakommo
Copy link

jakommo commented May 8, 2020

Thanks for posting this and your medium post, I'm search for a smooth docker/iptables solution for quite some time and this looks promising.
Unfortunately I can't get it working correctly and I'm wondering if I do something wrong or if there is something missing.

Using your example above, I only changed the interface name to match the one from my machine, but when I apply it, my machine can not talk to anything on the network anymore.
I guess thats because something like -A FILTERS -m state --state ESTABLISHED,RELATED -j ACCEPT is missing that allows other machines talking back to mine when I ping or curl something for testing.

The other thing I noticed is that the allow in https://gist.github.com/liejuntao001/266f2c5a5e85be70201eee9bcbd2b4a4#file-iptables-conf-template-L22 also seems to match when the target port in docker is one of those.
Example: if I run a container like sudo docker run --rm --name nginx -p 8080:80 nginx and use port 8080 on the outside forwarded to port 80 in the container, i can still do a curl http://192.168.1.5:8080 for another machine in the network and it is able to connect, even though I haven't opened port 8080.

Am I missing something or does it not differentiate between exposed ports and ports in the container for the allow?
Also tried the original example form the https://unrouted.io/2017/08/15/docker-firewall/ post, but it behaves similar.

while docker is running, iptables -S looks like:

-P INPUT ACCEPT
-P FORWARD DROP
-P OUTPUT ACCEPT
-N DOCKER
-N DOCKER-ISOLATION-STAGE-1
-N DOCKER-ISOLATION-STAGE-2
-N DOCKER-USER
-N FILTERS
-A INPUT -i lo -j ACCEPT
-A INPUT -j FILTERS
-A FORWARD -j DOCKER-USER
-A FORWARD -j DOCKER-ISOLATION-STAGE-1
-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -o docker0 -j DOCKER
-A FORWARD -i docker0 ! -o docker0 -j ACCEPT
-A FORWARD -i docker0 -o docker0 -j ACCEPT
-A DOCKER -d 172.17.0.2/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 80 -j ACCEPT
-A DOCKER-ISOLATION-STAGE-1 -i docker0 ! -o docker0 -j DOCKER-ISOLATION-STAGE-2
-A DOCKER-ISOLATION-STAGE-1 -j RETURN
-A DOCKER-ISOLATION-STAGE-2 -o docker0 -j DROP
-A DOCKER-ISOLATION-STAGE-2 -j RETURN
-A DOCKER-USER -i wlp3s0 -j FILTERS
-A FILTERS -p tcp -m multiport --dports 80,443 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT
-A FILTERS -p icmp -m icmp --icmp-type 8 -j REJECT --reject-with icmp-port-unreachable
-A FILTERS -m limit --limit 5/min -j LOG --log-prefix "iptables_INPUT_denied: " --log-level 7
-A FILTERS -j REJECT --reject-with icmp-port-unreachable

@liejuntao001
Copy link
Author

liejuntao001 commented May 9, 2020 via email

@jakommo
Copy link

jakommo commented May 9, 2020

Thanks for the quick reply. So just by binding it to the private ip, I don't see a different behaviour.

Didn't fully get your last sentence about the public ip. This machine only has one interface wlp3s0 with private ip 192.168.1.5.

I did play with it a little more, but when I remove allow 80,44 in line 22 and use something like:

-A FILTERS -s 192.168.1.20/32 -p tcp -m multiport --dports 8080 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT

Then 192.168.1.20 still can not connect to the nginx running on 8080. What makes it work is using the port inside the container like:

-A FILTERS -s 192.168.1.20/32 -p tcp -m multiport --dports 80 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT

Then I can curl from 192.168.1.20 to http://192.168.1.5:8080 and it works.
However, this allows connections to any container that is using port 80 internally. I.e. if I start another container like sudo docker run --rm --name nginx2 -p 192.168.1.5:8181:80 nginx
Then I can curl from 192.168.1.20 to http://192.168.1.5:8181 and it works.

What I want to achieve is that only certain hosts on the 192.168.1.0 net can reach certain services on this machine provided by docker.

How would you go about only allowing 192.168.1.20 access to the nginx running like sudo docker run --rm --name some-nginx -p 192.168.1.5:8080:80 nginx, without also allowing it access to another nginx that is running with 8181->80?

@liejuntao001
Copy link
Author

You have a different use case.
Mine is for hosts with 2 interfaces. One is the public IP address serving 80/443, another is private IP serving other internal services. The rules in this example block the unexpected access from the private IP interface.

The official document could help your use case.
https://docs.docker.com/network/iptables/

You could instead allow connections from a source subnet. The following rule only allows access from the subnet 192.168.1.0/24:
$ iptables -I DOCKER-USER -i ext_if ! -s 192.168.1.0/24 -j DROP

As you have this line
-A DOCKER-USER -i wlp3s0 -j FILTERS
above rule is like
-A FILTERS -i wlp3s0 ! -s 192.168.1.0/24 -j DROP

@liejuntao001
Copy link
Author

After some study I found this line will allow access to container port 80 when jumped from DOCKER-USER to FILTERS

-A DOCKER-USER -i wlp3s0 -j FILTERS
-A FILTERS -p tcp -m multiport --dports 80,443 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT

Here the "80, 443" are not the port on the host side of the binding, but container side
e.g. 8080:80, 8080, the left side, host side port, and 80, the right side, container side.

If you modify the rule as
-A FILTERS -p tcp -m multiport --dports 443 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT
then 8080 port is not accessible from external.

@jakommo
Copy link

jakommo commented May 10, 2020

Thanks for looking into this, really appreciate the help. I think I will stick with my current setup and using double rules in INPUT+DOCKER-USER for the per host per service mapping.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment