I've talked quite a lot about iptables but haven't really shown how I actually put it into practice. Today I'll share a shell script I wrote to set up IPTables on my Fedora 25 Desktop.

Download

Warning: Make sure you check its appropriate for your needs and you understand its functionality before running it.

The shell script sets up the following

  • Multiple user based chains for NFS, SSH, and HTTP
  • Organized TCP / UDP chains
  • Source restriction based on the use of IPsets

Structure and Functions of the Script

We begin by defining the function configures our SSH hardening chain

The ssh hardening chain does the following

  • Blocks a source address that has attempted >=3 connections in 1min
  • Blocks a source address that has attempted >=5 connections in 10min
  • Logs successful connections
config_ssh_chain(){
iptables --append SSH --match conntrack --ctstate NEW --match recent --set --name SSH

iptables --append SSH --match conntrack --ctstate NEW --match recent --update --seconds 60 --hitcount  2 --rttl --name SSH --jump DROP

iptables --append SSH --match conntrack --ctstate NEW --match recent --update --seconds 600 --hitcount 4 --rttl --name SSH --jump DROP

iptables --append SSH --match conntrack --ctstate NEW --jump LOG --log-prefix "iptables-sshaccept: "
iptables --append SSH --match conntrack --ctstate NEW --jump ACCEPT

}

Next that function configures the HTTP ratelimiting chain

  • Restricts access based on an IPset
  • Limits to a maximum of 50 concurrent connections from a source
config_http_chain(){
### Creating the set
#ipset --create allowed_http hash:ip
#ipset --add allowed_http 10.20.30.40 

### Block connections NOT in ipset 
### (in other words, allow only IPs in set)
iptables --append HTTP --proto tcp --match set ! --match-set allowed_http src --jump REJECT --reject-with tcp-reset

### Max of 50 concurrent connections per src ip
iptables --append HTTP --match connlimit --connlimit-above 10 --connlimit-mask 32 --jump DROP

### Accept connections	
iptables --append HTTP --jump ACCEPT
}

Next the NFS chain

  • Allows connections via an IPSet based on MAC/IP pairs
config_nfs_chain(){
### Creating the set
#ipset --create allowed_nfs macipmap --network 192.168.1.0/24
#ipset --add allowed_nfs 192.168.1.2,aa:bb:cc:dd:ee:ff

### Drop ip/mac pairs not found in this set
iptables --append NFS --match set ! --match-set allowed_nfs src --jump DROP
}

Next we specify the TCP ports we wish to allow

  • Allow SSH; pass it to the SSH chain
  • Allow HTTP/HTTPS; pass to the HTTP chain
config_tcp_accept_chain() {
iptables --append TCP_ACCEPT --proto tcp --dport 22  --jump SSH
iptables --append TCP_ACCEPT --proto tcp --dport 443 --jump HTTP
iptables --append TCP_ACCEPT --proto tcp --dport 80  --jump HTTP
}

Then this function sets up all the TCP related rules in the INPUT chain

  • Drop invalid connections (New without syn/xmas/null)
  • Traffic to NFS ports on a particular interface jumps to the NFS chain
  • Allow ports specified in the TCP_ACCEPT
  • Reset all other new TCP connections
config_tcp() {
### We drop connections that are NEW but lack the syn flag
iptables --append INPUT --proto tcp --match conntrack --ctstate NEW --match tcp ! --syn --jump DROP

### We drop "XMAS" packets
iptables --append INPUT --proto tcp --tcp-flags ALL ALL --jump DROP
iptables --append INPUT --proto tcp --tcp-flags ALL NONE --jump DROP
	
### NFS traffic within a particular interface gets passed on to the NFS chain
iptables --append INPUT --match conntrack --ctstate NEW  --proto tcp --syn --match multiport --dports 111,2049 --in-interface enp3s0 --jump NFS

### We then accept ports specified in the TCP accept chain
iptables --append INPUT --match conntrack --ctstate NEW  --proto tcp --syn --jump TCP_ACCEPT

### We then reset NEW tcp connections
iptables --append INPUT --match conntrack --ctstate NEW  --proto tcp --syn --jump REJECT --reject-with tcp-reset
}

It's basically the same story for UDP

  • We accept DNS/DHCP from the virbr0 and virbr1 interfaces (used for libvirt, you may wish to remove)
  • DROP Samba, mDNS, and UPnP traffic
  • NFS ports on the interface jumps to the NFS chain
  • Reject everything else
config_udp_accept_chain(){
#iptables --append UDP_ACCEPT --proto udp --dport 53 --jump ACCEPT
}
config_udp(){
### We accept DNS+DHCP from our VMs
iptables --append INPUT --in-interface virbr0 --proto udp --match multiport --dports 67,53 --jump ACCEPT
iptables --append INPUT --in-interface virbr1 --proto udp --match multiport --dports 67,53 --jump ACCEPT
	
### Ports we want Dropped Instantly
iptables --append INPUT --protocol udp --match multiport --dports 137,138,139,1900,5353 --jump DROP

### NFS traffic within a particular interface gets passed on to the NFS chain
iptables --append INPUT --proto udp --match conntrack --ctstate NEW --match multiport --dports 111,2049 --in-interface enp3s0 --jump NFS

### We accept ports specified in the UDP accept chain
iptables --append INPUT --proto udp --match conntrack --ctstate NEW --jump UDP_ACCEPT
	
### We reject new UDP connections
iptables --append INPUT --proto udp --match conntrack --ctstate NEW --jump REJECT --reject-with icmp-port-unreachable 
}

Next, a function for configuring ICMP

  • Allow new ping requests (echo-request) but rate limit
config_icmp(){
### Accept NEW echo-request packets but limit them to 1/sec
iptables --append INPUT --match conntrack --ctstate NEW --proto icmp --icmp-type echo-request --limit 1/s --jump ACCEPT
}

Now this function actually creates all the chains we've seen

create_user_chains(){
### Create and configure SSH hardening chain
iptables --table filter --new-chain SSH
config_ssh_chain

### Create and configure NFS access-restriction chain
iptables --table filter --new-chain NFS
config_nfs_chain

### Create and configure HTTP rate limiting chain
iptables --table filter --new-chain HTTP
config_http_chain

### Create and configure TCP accepting chain
iptables --table filter --new-chain TCP_ACCEPT
config_tcp_accept_chain

### Create and configure UDP accepting chain	
iptables --table filter --new-chain UDP_ACCEPT
config_udp_accept_chain
}

Now we set up the INPUT chain

  • Drop invalid and fragmented traffic; allow established/related
  • Allow loopback
  • Configure the TCP, UDP, and ICMP rules; call the respective functions seen above
  • Set the default policy to DROP all
setup_input_chain() {
### Drop invalid traffic
iptables --append INPUT --match conntrack --ctstate INVALID --jump DROP
### Drop fragmented traffic	
iptables --append INPUT --fragments --jump DROP
### Accept Established/related
iptables --append INPUT --match conntrack --ctstate ESTABLISHED,RELATED --jump ACCEPT
### Allow loopback
iptables --append INPUT --in-interface lo --jump ACCEPT
### Configure TCP+UDP rules
config_tcp
config_udp
### Configure ICMP rules
config_icmp
### Set input policy to drop
iptables --table filter --policy INPUT DROP
}

At last, we have reached the 'main' body of the script

  • Saves the old rules to the root's home directory
  • Create and configure the user chains
  • Flush the INPUT chain
  • Configure the INPUT chain
### Save the old iptables rules
iptables-save > /root/iptables-old-$(date +%d_%m_%y_%H%M%Z)
### Create the user chains
create_user_chains
### Flush the input rules
iptables --flush INPUT
### Set up the input rules
setup_input_chain