IP Tables as Firewall

IP Tables as Firewall

Overview

In this post, I want to share my IP tables notes based on my recently faced firewall and network connectivity issues on an cloud VM instance that costed me several hours to learn.

Context: After I created the VM instance from a cloud provider and accessed over SSH, I wanted to change the SSH port. I edited the sshd_config, added the new port to the cloud providers firewall settings. Restarted the ssh service. But I couldn't connect to that TCP port.

After digging into the IPTables, I learned the basics and toke notes for some details.

📚 IPs and ports in this post is randomized for security purposes.

IPTable basics

  • Check the current iptables chains of your machine.

    sudo iptables -L -n -v
    
  • If your VM is a fresh Debian machine, the output probably will look like:

    Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
     pkts bytes target     prot opt in     out     source               destination         
    
    Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
     pkts bytes target     prot opt in     out     source               destination         
    
    Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
     pkts bytes target     prot opt in     out     source               destination
    
    • These three chains are the main chains you should know.
    • In the chains above, there is no rules defined at all.
    • The state in between parentheses defines the default rule if nothing matches.
    • So the iptables are configured to accept any incoming, forwarding or outgoing requests.
    • If I add a rule for SSH and reject all remaining requests with the commands below:
    sudo iptables -I INPUT 1 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
    
    sudo iptables -I INPUT 2 -p tcp --dport 432 -j ACCEPT
    
    sudo iptables -A INPUT -j REJECT --reject-with icmp-host-prohibited
    
    • The iptables becames:
    Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
     pkts bytes target     prot opt in     out     source               destination         
       62  4672 ACCEPT     6    --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:22
      199  246K REJECT     0    --  *      *       0.0.0.0/0            0.0.0.0/0            reject-with icmp-host-prohibited
    
    Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
     pkts bytes target     prot opt in     out     source               destination         
    
    Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
     pkts bytes target     prot opt in     out     source               destination
    

Understanding the basics of the iptables output.

  • I want to explain the output based on this output:
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         
	 25  1820 ACCEPT     0    --  *      *       0.0.0.0/0            0.0.0.0/0            ctstate RELATED,ESTABLISHED
   62  4672 ACCEPT     6    --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:22
  199  246K REJECT     0    --  *      *       0.0.0.0/0            0.0.0.0/0            reject-with icmp-host-prohibited

Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         

Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination
  • As you can see, the only INPUT chain contains rules.

    • pkts: Total package count that matched the rule.
    • bytes: Total bytes that matches the rule.
    • target: Defines the ACCEPT or REJECT rule.
    • prot: Defines the network protocol. Most common protocols:
      • 0: Matches all protocols.
      • 1: ICMP
      • 6: TCP
      • 17: UDP
    • in: Defines the incoming network interface.
    • out Defines the outgoing network interface.
    • source: Defines the incoming IP address range.
    • destination: Defines the outgoing IP address range.
    • no name: The last column has no name but defines extras.
  • The kernel starts matching incoming requests with the rules. The match is sequential. So the order of the rules is important.

  • If we analyze the first rule:

    • If you don't have this rule, your server can talk out, but it will be "deaf" to the replies.
    • This accept "responses".
  • If we analyze the second rule:

     pkts bytes target     prot opt in     out     source               destination         
       62  4672 ACCEPT     6    --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:22
    
    • This rules accepts incoming connections (INPUT chain) from 0.0.0.0/0 (any IP address) to 0.0.0.0/0 (any IP address) TCP connections (prot 6) to port 22 (dpt:22).
    • In basic, this rule accepts incoming connections for TCP 22 from any IP address.
  • If we analyze the third rule:

     pkts bytes target     prot opt in     out     source               destination         
      199  246K REJECT     0    --  *      *       0.0.0.0/0            0.0.0.0/0            reject-with icmp-host-prohibited
    
    • This rules REJECT incoming connections (INPUT chain) from 0.0.0.0/0 (any IP address) to 0.0.0.0/0 (any IP address).
    • In basic, this rule rejects incoming connections for all connections.
    • Thanks to sequential matching, if a request doesn't match to any rules before this rule, it is rejected.
    • So if I run a web application on TCP port 80, I won't be able to connect to it with this iptables rules.

Docker Modifies the IPTables

  • In 2026, you will most likely install Docker into the VM. After installing the Docker, the iptables will be like the following:

    • The below outputs are from a fresh Debian generic cloud image.
    Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
     pkts bytes target     prot opt in     out     source               destination         
    37601  103M ACCEPT     0    --  *      *       0.0.0.0/0            0.0.0.0/0            ctstate RELATED,ESTABLISHED
      268 17944 ACCEPT     6    --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:22
      577  278K REJECT     0    --  *      *       0.0.0.0/0            0.0.0.0/0            reject-with icmp-host-prohibited
    
    Chain FORWARD (policy DROP 0 packets, 0 bytes)
     pkts bytes target     prot opt in     out     source               destination         
        0     0 DOCKER-USER  0    --  *      *       0.0.0.0/0            0.0.0.0/0           
        0     0 DOCKER-FORWARD  0    --  *      *       0.0.0.0/0            0.0.0.0/0           
    
    Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
     pkts bytes target     prot opt in     out     source               destination         
    
    Chain DOCKER (1 references)
     pkts bytes target     prot opt in     out     source               destination         
        0     0 DROP       0    --  !docker0 docker0  0.0.0.0/0            0.0.0.0/0           
    
    Chain DOCKER-BRIDGE (1 references)
     pkts bytes target     prot opt in     out     source               destination         
        0     0 DOCKER     0    --  *      docker0  0.0.0.0/0            0.0.0.0/0           
    
    Chain DOCKER-CT (1 references)
     pkts bytes target     prot opt in     out     source               destination         
        0     0 ACCEPT     0    --  *      docker0  0.0.0.0/0            0.0.0.0/0            ctstate RELATED,ESTABLISHED
    
    Chain DOCKER-FORWARD (1 references)
     pkts bytes target     prot opt in     out     source               destination         
        0     0 DOCKER-CT  0    --  *      *       0.0.0.0/0            0.0.0.0/0           
        0     0 DOCKER-INTERNAL  0    --  *      *       0.0.0.0/0            0.0.0.0/0           
        0     0 DOCKER-BRIDGE  0    --  *      *       0.0.0.0/0            0.0.0.0/0           
        0     0 ACCEPT     0    --  docker0 *       0.0.0.0/0            0.0.0.0/0           
    
    Chain DOCKER-INTERNAL (1 references)
     pkts bytes target     prot opt in     out     source               destination         
    
    Chain DOCKER-USER (1 references)
     pkts bytes target     prot opt in     out     source               destination
    
    • 📚 Docker creates custom chains in the iptables for port forwarding and bridge networks.
    • ⚠️ These custom rules mostly overwrites the custom rules so you should be careful.
  • If you run a Nginx container with port forwarding, you will something different.

    sudo docker run -d --rm -p 80:80 nginx
    
  • Check the running container:

    berk@iptables-test:~$ sudo docker ps 
    CONTAINER ID   IMAGE     COMMAND                  CREATED          STATUS          PORTS                                 NAMES
    918983a0abf8   nginx     "/docker-entrypoint.…"   13 seconds ago   Up 12 seconds   0.0.0.0:80->80/tcp, [::]:80->80/tcp   vigorous_johnson
    
  • Check the iptables again:

    berk@iptables-test:~$ sudo iptables -L -n -v
    Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
     pkts bytes target     prot opt in     out     source               destination         
    43300  169M ACCEPT     0    --  *      *       0.0.0.0/0            0.0.0.0/0            ctstate RELATED,ESTABLISHED
      268 17944 ACCEPT     6    --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:22
      590  279K REJECT     0    --  *      *       0.0.0.0/0            0.0.0.0/0            reject-with icmp-host-prohibited
    
    Chain FORWARD (policy DROP 0 packets, 0 bytes)
     pkts bytes target     prot opt in     out     source               destination         
        0     0 DOCKER-USER  0    --  *      *       0.0.0.0/0            0.0.0.0/0           
        0     0 DOCKER-FORWARD  0    --  *      *       0.0.0.0/0            0.0.0.0/0           
    
    Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
     pkts bytes target     prot opt in     out     source               destination         
    
    Chain DOCKER (1 references)
     pkts bytes target     prot opt in     out     source               destination         
        0     0 ACCEPT     6    --  !docker0 docker0  0.0.0.0/0            172.17.0.2           tcp dpt:80
        0     0 DROP       0    --  !docker0 docker0  0.0.0.0/0            0.0.0.0/0           
    
    Chain DOCKER-BRIDGE (1 references)
     pkts bytes target     prot opt in     out     source               destination         
        0     0 DOCKER     0    --  *      docker0  0.0.0.0/0            0.0.0.0/0           
    
    Chain DOCKER-CT (1 references)
     pkts bytes target     prot opt in     out     source               destination         
        0     0 ACCEPT     0    --  *      docker0  0.0.0.0/0            0.0.0.0/0            ctstate RELATED,ESTABLISHED
    
    Chain DOCKER-FORWARD (1 references)
     pkts bytes target     prot opt in     out     source               destination         
        0     0 DOCKER-CT  0    --  *      *       0.0.0.0/0            0.0.0.0/0           
        0     0 DOCKER-INTERNAL  0    --  *      *       0.0.0.0/0            0.0.0.0/0           
        0     0 DOCKER-BRIDGE  0    --  *      *       0.0.0.0/0            0.0.0.0/0           
        0     0 ACCEPT     0    --  docker0 *       0.0.0.0/0            0.0.0.0/0           
    
    Chain DOCKER-INTERNAL (1 references)
     pkts bytes target     prot opt in     out     source               destination         
    
    Chain DOCKER-USER (1 references)
     pkts bytes target     prot opt in     out     source               destination
    
  • In the INPUT chain, there is still no rule for accepting TCP 80. But however, the Nginx container is accepting and responding to the requests

    curl -v telnet://192.168.1.115:80
    *   Trying 192.168.1.115:80...
    * Connected to 192.168.1.115 (192.168.1.115) port 80 (#0)
    

And here we are digging some details.

  • Before a request is matching a rule, the kernel first checks the PREROUTING chain. Docker creates a NAT PREROUTING rule for container requests.
  • To check the NAT PREROUTING rules, run:
sudo iptables -t nat -L PREROUTING -n -v --line-numbers
  • The output will look like:
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
num   pkts bytes target     prot opt in     out     source               destination         
1       33   988 DOCKER     0    --  *      *       0.0.0.0/0            0.0.0.0/0            ADDRTYPE match dst-type LOCAL
  • This makes sure that all incoming requests are forwarded to the Docker NAT PREROUTING chain before the default INPUT chain.
  • If you want to check the DOCKER NAT chain, run:
    sudo iptables -t nat -L DOCKER -n -v --line-numbers
    
  • The output will be like:
    Chain DOCKER (2 references)
    num   pkts bytes target     prot opt in     out     source               destination         
    1        2   120 DNAT       6    --  !docker0 *       0.0.0.0/0            0.0.0.0/0            tcp dpt:80 to:172.17.0.2:80
    
    • This rule makes the port forwarding magic.
    • If the request is not coming from docker0 interface and the port is TCP 80, it forwards the NATs the requests for 172.17.0.2:80.
    • The 172.17.0.2:80 IP address is the internal Docker IP address of the Nginx container.
    • If no request matches this Docker NAT rules, the request goes goes back to the default INPUT chain.
    • So even if there is no ACCEPT rule for TCP port 80 for the NGINX container, Docker creates a backdoor in the iptables to control the requests.

Quick Commands

  • Summary information:
    • Even if there is no ACCEPT rules for the containers you run, Docker creates a backdoor in the iptables to control and allow the requests.
    • If you create or update a system service (like SSH) use a port that is not listed in the INPUT chain, the iptables will reject the connection if the latest rule is reject.
  • Check the PREROUTING table:
sudo iptables -t nat -L PREROUTING -n -v --line-numbers
  • Check the NAT rules:
sudo iptables -t nat -L DOCKER -n -v --line-numbers
  • Check all the chains:
sudo iptables -L -n -v
  • Check all the ACCEPT rules from all chains:
sudo iptables -L -n -v | grep ACCEPT
  • To allow responses in the INPUT chain:
sudo iptables -I INPUT 1 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
  • To allow port 22:
sudo iptables -I INPUT 2 -p tcp --dport 22 -j ACCEPT
  • To reject all the remaining connections:
sudo iptables -A INPUT -j REJECT --reject-with icmp-host-prohibited
  • To delete a firewall rule:
    • Check the firewall rules by lines
    sudo iptables -L INPUT --line-numbers -n
    
    • Delete the INPUT
    sudo iptables -D INPUT 6