diff --git a/lib/ansible/roles/firewall/files/init.d/iptables-persistent b/lib/ansible/roles/firewall/files/init.d/iptables-persistent new file mode 100644 index 00000000..b8bbbc30 --- /dev/null +++ b/lib/ansible/roles/firewall/files/init.d/iptables-persistent @@ -0,0 +1,199 @@ +#!/bin/sh + +### BEGIN INIT INFO +# Provides: iptables-persistent +# Required-Start: mountkernfs $local_fs +# Required-Stop: $local_fs +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# X-Start-Before: $network +# X-Stop-After: $network +# Short-Description: Set up iptables rules +### END INIT INFO + +# Based on https://github.com/zertrin/iptables-persistent + +PATH="/sbin:/bin:/usr/sbin:/usr/bin" + +# Include config file for iptables-persistent +. /etc/default/iptables-persistent.conf + +. /lib/lsb/init-functions + +rc=0 + +case "$1" in +start) + log_action_begin_msg "Starting iptables" + + if [ -e /var/run/iptables ]; then + log_warning_msg "iptables is already started" + exit 1 + else + touch /var/run/iptables + fi + + # if fail2ban is already running, stop it the time needed to load the new rules + if [ -x /etc/init.d/fail2ban ]; then + /etc/init.d/fail2ban stop + fi + + if [ $ENABLE_ROUTING -ne 0 ]; then + # Enable Routing + echo 1 > /proc/sys/net/ipv4/ip_forward + log_action_cont_msg "v4 Routing" + if [ $IPV6 -ne 0 ]; then + echo 1 >/proc/sys/net/ipv6/conf/all/forwarding + log_action_cont_msg "v6 Routing" + fi + fi + + if [ $MODULES ]; then + # Load Modules + modprobe -a $MODULES + log_action_cont_msg "Modules $MODULES" + fi + + # Load saved rules + if [ -f /etc/iptables/rules.v4 ]; then + iptables-restore /etc/iptables/rules.v4 + if [ $? -ne 0 ]; then + rc=1 + fi + log_action_cont_msg "IPv4" + + if [ $IPV6 -ne 0 ]; then + # Backup old rules + cp /etc/iptables/rules.v6 /etc/iptables/rules.v6.bak + # Save new rules + ip6tables-save >/etc/iptables/rules.v6 + if [ $? -ne 0 ]; then + rc=1 + fi + log_action_cont_msg "IPv6" + fi + fi + + # stop fail2ban before flushing iptables chains + if [ -x /etc/init.d/fail2ban ]; then + /etc/init.d/fail2ban stop + fi + + # Restore Default Policies + iptables -P INPUT ACCEPT + iptables -P FORWARD ACCEPT + iptables -P OUTPUT ACCEPT + + # Flush rules on default tables + iptables -F + iptables -t nat -F + iptables -t mangle -F + + if [ $IPV6 -ne 0 ]; then + # Restore Default Policies + ip6tables -P INPUT ACCEPT + ip6tables -P FORWARD ACCEPT + ip6tables -P OUTPUT ACCEPT + + # Flush rules on default tables + ip6tables -F + ip6tables -t mangle -F + fi + + if [ $MODULES ]; then + # Unload previously loaded modules + modprobe -r $MODULES + fi + + # Disable Routing if enabled + if [ $ENABLE_ROUTING -ne 0 ]; then + # Disable Routing + echo 0 > /proc/sys/net/ipv4/ip_forward + if [ $IPV6 -ne 0 ]; then + echo 0 >/proc/sys/net/ipv6/conf/all/forwarding + fi + fi + + # start of fail2ban + if [ -x /etc/init.d/fail2ban ]; then + /etc/init.d/fail2ban start + fi + + log_action_end_msg $rc + ;; + +restart|force-reload) + $0 stop + $0 start + ;; + +status) + echo "Filter Rules:" + echo "--------------" + iptables -L -v + echo "" + echo "NAT Rules:" + echo "-------------" + iptables -t nat -L -v + echo "" + echo "Mangle Rules:" + echo "----------------" + iptables -t mangle -L -v + + if [ $IPV6 -ne 0 ]; then + echo "**********" + echo "** IPV6 **" + echo "**********" + echo "Filter Rules:" + echo "--------------" + ip6tables -L -v + echo "" + echo "Mangle Rules:" + echo "----------------" + ip6tables -t mangle -L -v + fi + ;; + +*) + echo "Usage: $0 {start|stop|force-stop|restart|force-reload|status}" >&2 + exit 1 + ;; +esac + +exit 0 diff --git a/lib/ansible/roles/firewall/files/iptables/rules.v4 b/lib/ansible/roles/firewall/files/iptables/rules.v4 new file mode 100644 index 00000000..397ea11a --- /dev/null +++ b/lib/ansible/roles/firewall/files/iptables/rules.v4 @@ -0,0 +1,70 @@ +*filter +# Set default chain policies +:INPUT ACCEPT [0:0] +:FORWARD ACCEPT [0:0] +:OUTPUT ACCEPT [0:0] + +# Allows all loopback (lo0) traffic and drop all traffic to 127/8 that doesn't use lo0 +-A INPUT -i lo -j ACCEPT +-A INPUT ! -i lo -d 127.0.0.0/8 -j REJECT + +# Drop any tcp packet that does not start a connection with a syn flag. +-A INPUT -p tcp ! --syn -m state --state NEW -j DROP + +# Accepts all established inbound connections +-A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT + +# Drop any invalid packet that could not be identified. +-A INPUT -m state --state INVALID -j DROP + +# Blacklist (examples) +#-A INPUT -i eth+ -s 198.51.100.1 -p tcp --dport 22 -j DROP # Example IP from RFC 5737 +#-A INPUT -i eth+ -s 203.0.113.34 -j DROP # Example IP from RFC 5737 + +# Allow incoming FTP +#-A INPUT -i eth+ -p tcp --dport 21 -j ACCEPT + +# Allow incoming SSH +-A INPUT -i eth+ -p tcp --dport 22 -j ACCEPT + +# Allow incoming HTTP +-A INPUT -i eth+ -p tcp --dport 80 -j ACCEPT + +# Allow incoming HTTPS +-A INPUT -i eth+ -p tcp --dport 443 -j ACCEPT + +# Allow ping from inside to outside +-A OUTPUT -o eth+ -p icmp --icmp-type echo-request -j ACCEPT +-A INPUT -i eth+ -p icmp --icmp-type echo-reply -m limit --limit 2/s -j ACCEPT + +# Allow ping from outside to inside +-A INPUT -i eth+ -p icmp --icmp-type echo-request -m limit --limit 2/s -j ACCEPT +-A OUTPUT -o eth+ -p icmp --icmp-type echo-reply -j ACCEPT + +# Allow NTP +#-A INPUT -i eth+ -p udp --dport 123 -j ACCEPT +#-A OUTPUT -o eth+ -p udp --sport 123 -j ACCEPT + +# Allow DNS +#-A OUTPUT -o eth+ -p tcp --dport 53 -j ACCEPT +#-A OUTPUT -o eth+ -p udp --dport 53 -j ACCEPT +#-A INPUT -i eth+ -p tcp --sport 53 -j ACCEPT +#-A INPUT -i eth+ -p udp --sport 53 -j ACCEPT + +# Log dropped packets +-N LOGGING +-A INPUT -j LOGGING +-A LOGGING -p tcp -m limit --limit 5/min --limit-burst 10 -j LOG --log-prefix "iptables: [denied TCP] " --log-level 7 +-A LOGGING -p udp -m limit --limit 5/min --limit-burst 10 -j LOG --log-prefix "iptables: [denied UDP] " --log-level 7 +-A LOGGING -p icmp -m limit --limit 5/min --limit-burst 10 -j LOG --log-prefix "iptables: [denied ICMP] " --log-level 7 + +# Reject all other inbound - default deny unless explicitly allowed policy: +-A INPUT -j REJECT + +# Allow all other outbound traffic +-A OUTPUT -j ACCEPT + +# Reject forwarded traffic, explicitely (despite chain policy) +-A FORWARD -j REJECT + +COMMIT diff --git a/lib/ansible/roles/firewall/files/iptables/rules.v6 b/lib/ansible/roles/firewall/files/iptables/rules.v6 new file mode 100644 index 00000000..f8d3c3c3 --- /dev/null +++ b/lib/ansible/roles/firewall/files/iptables/rules.v6 @@ -0,0 +1,51 @@ +*filter +:INPUT ACCEPT [0:0] +:FORWARD ACCEPT [0:0] +:OUTPUT ACCEPT [0:0] + +# allow all loopback traffic +-A INPUT -i lo -j ACCEPT + +# allow all ICMP traffic +-A INPUT -p icmpv6 -j ACCEPT + +# Drop any tcp packet that does not start a connection with a syn flag. +-A INPUT -p tcp ! --syn -m state --state NEW -j DROP +# packets belonging to an established connection or related to one can pass +-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT +# packets that are out-of-sequence are silently dropped +-A INPUT -m state --state INVALID -j DROP + +# Allow incoming FTP +#-A INPUT -i eth+ -p tcp --dport 21 -j ACCEPT + +# Allow incoming SSH +-A INPUT -i eth+ -p tcp -m tcp --dport 22 -j ACCEPT + +# Allow incoming HTTP +-A INPUT -i eth+ -p tcp -m tcp --dport 80 -j ACCEPT + +# Allow incoming HTTPS +-A INPUT -i eth+ -p tcp -m tcp --dport 443 -j ACCEPT + +# Allow DNS +#-A OUTPUT -o eth+ -p tcp --dport 53 -j ACCEPT +#-A OUTPUT -o eth+ -p udp --dport 53 -j ACCEPT +#-A INPUT -i eth+ -p tcp --sport 53 -j ACCEPT +#-A INPUT -i eth+ -p udp --sport 53 -j ACCEPT + +# Log dropped packets +-N LOGGING +-A INPUT -j LOGGING +-A LOGGING -p tcp -m limit --limit 5/min --limit-burst 10 -j LOG --log-prefix "iptables: [INPUT6 ][denied TCP] " --log-level 7 +-A LOGGING -p udp -m limit --limit 5/min --limit-burst 10 -j LOG --log-prefix "iptables: [INPUT6 ][denied UDP] " --log-level 7 +-A LOGGING -p icmp -m limit --limit 5/min --limit-burst 10 -j LOG --log-prefix "iptables: [INPUT6 ][denied ICMP] " --log-level 7 +-A INPUT -j REJECT + +# allow outgoing traffic, explicitly (despite chain policy) +-A OUTPUT -j ACCEPT + +# disallow forwarded traffic, explicitly (despite chain policy) +-A FORWARD -j REJECT + +COMMIT diff --git a/lib/ansible/roles/firewall/handlers/main.yml b/lib/ansible/roles/firewall/handlers/main.yml new file mode 100644 index 00000000..7c39f4a3 --- /dev/null +++ b/lib/ansible/roles/firewall/handlers/main.yml @@ -0,0 +1,4 @@ +--- +- name: restart firewall + command: /etc/init.d/iptables-persistent restart + sudo: yes diff --git a/lib/ansible/roles/firewall/tasks/main.yml b/lib/ansible/roles/firewall/tasks/main.yml new file mode 100644 index 00000000..8a7a5cb9 --- /dev/null +++ b/lib/ansible/roles/firewall/tasks/main.yml @@ -0,0 +1,37 @@ +--- +- name: Attempt to install vanilla iptables-persistent + apt: pkg=iptables-persistent state=latest update_cache=yes + register: iptables_attempted + ignore_errors: yes + sudo: yes + +# See https://forum.linode.com/viewtopic.php?t=9070&p=58732 +- name: Postinst workaround for iptables-persistent + replace: dest=/var/lib/dpkg/info/iptables-persistent.postinst regexp='^(\s*?modprobe -q ip6?table_filter\s*?)$' replace='\1 || true' + when: iptables_attempted | failed + sudo: yes + +- name: Install firewall packages + apt: pkg={{ item }} state=present + with_items: firewall_packages + sudo: yes + +- name: Install iptables init script + rulesets + copy: src={{ item }} dest=/etc/{{ item }} mode=0644 backup=yes + sudo: yes + with_items: + - init.d/iptables-persistent + - iptables/rules.v4 + - iptables/rules.v6 + +- name: Create iptables + fail2ban configs + template: src={{ item }} dest=/etc/{{ item }} mode=0644 + sudo: yes + with_items: + - default/iptables-persistent.conf + - fail2ban/jail.local + +- name: Ensure iptables init script is executable + file: path=/etc/init.d/iptables-persistent mode=0755 + notify: restart firewall + sudo: yes diff --git a/lib/ansible/roles/firewall/templates/default/iptables-persistent.conf b/lib/ansible/roles/firewall/templates/default/iptables-persistent.conf new file mode 100644 index 00000000..2e9a7971 --- /dev/null +++ b/lib/ansible/roles/firewall/templates/default/iptables-persistent.conf @@ -0,0 +1,15 @@ +# A basic config file for the /etc/init.d/iptable-persistent script +# + +# Should new manually added rules from command line be saved on reboot? Assign to a value different that 0 if you want this enabled. +SAVE_NEW_RULES=0 + +# Modules to load: +#modules="ip_conntrack_ftp" #example for ftp conntracking +modules="" + +# Enable Routing? Assign to a value different that 0 if you want this enabled. +ENABLE_ROUTING=0 + +# Enable IPv6? Assign to a value different that 0 if you want this enabled. +IPV6={% if iptables.ipv6 is defined and iptables.ipv6 %}1{% else %}0{% endif %} diff --git a/lib/ansible/roles/firewall/templates/fail2ban/jail.local b/lib/ansible/roles/firewall/templates/fail2ban/jail.local new file mode 100644 index 00000000..3b21c28e --- /dev/null +++ b/lib/ansible/roles/firewall/templates/fail2ban/jail.local @@ -0,0 +1,62 @@ +## Overrides of distribution-provided fail2ban jail.conf + +# Global definition of options, can be overridden in each jail afterwards. +[DEFAULT] + +{% if fail2ban.whitelist is defined %} +# Can be space separated IP addresses, CIDR masks or DNS hosts +ignoreip = 127.0.0.1/8 {{ fail2ban.whitelist | join(' ') }} + +{% endif %} +{% if fail2ban.ban_time is defined %} +# Global default ban time (in seconds) +bantime = {{ fail2ban.ban_time }} + +{% endif %} +{% if fail2ban.notification_email is defined %} +# Destination email for interpolations in jail.{conf,local} +destemail = {{ fail2ban.notification_email }} + +{% endif %} +# Default banning action +banaction = iptables-multiport + +# Default action (e.g. action_ for ban only, action_mw for ban + email, action_mwl for ban + email w relevant log entries) +{% if fail2ban.notify_on_ban is defined and fail2ban.notify_on_ban %} +action = %(action_mwl)s +{% else %} +action = %(action_)s +{% endif %} + + +[ssh] + +enabled = true +port = ssh +filter = sshd +logpath = /var/log/auth.log +maxretry = 6 + +[ssh-ddos] + +enabled = true +port = ssh +filter = sshd-ddos +logpath = /var/log/auth.log +maxretry = 6 + +[apache] + +enabled = true +port = http,https +filter = apache-auth +logpath = /var/log/apache*/*error.log +maxretry = 6 + +[apache-overflows] + +enabled = true +port = http,https +filter = apache-overflows +logpath = /var/log/apache*/*error.log +maxretry = 2 diff --git a/lib/ansible/roles/firewall/vars/main.yml b/lib/ansible/roles/firewall/vars/main.yml new file mode 100644 index 00000000..13715f9d --- /dev/null +++ b/lib/ansible/roles/firewall/vars/main.yml @@ -0,0 +1,6 @@ +--- +role_firewall: true + +firewall_packages: + - iptables-persistent + - fail2ban diff --git a/lib/yeoman/templates/lib/ansible/group_vars/all b/lib/yeoman/templates/lib/ansible/group_vars/all index 4339955b..f58f47f1 100644 --- a/lib/yeoman/templates/lib/ansible/group_vars/all +++ b/lib/yeoman/templates/lib/ansible/group_vars/all @@ -22,3 +22,13 @@ apache_settings: mail: postmaster: no-reply@<%= props.domain %> + +iptables: + # ipv6: 0 + +fail2ban: + # whitelist: + # - 127.0.0.1/8 + # ban_time: 600 + # notification_email: root@localhost + # notify_on_ban: 0 diff --git a/lib/yeoman/templates/lib/ansible/provision.yml b/lib/yeoman/templates/lib/ansible/provision.yml index 7f76915e..6b12c3eb 100644 --- a/lib/yeoman/templates/lib/ansible/provision.yml +++ b/lib/yeoman/templates/lib/ansible/provision.yml @@ -19,6 +19,7 @@ - pound # (Optional) SSL support & decryption - wp-cli # (Optional) CLI for managing WordPress - mail # (Optional) Ability to send e-mails from the server (i.e. PHP) + - firewall # (Optional) Simple intrusion protection via Fail2ban + iptables - debug # (Optional) Tools to monitor processes & debug when SSH'd in post_tasks: diff --git a/lib/yeoman/templates/lib/ansible/templates/evolution-init-d b/lib/yeoman/templates/lib/ansible/templates/evolution-init-d index 951a33d0..ab29c239 100644 --- a/lib/yeoman/templates/lib/ansible/templates/evolution-init-d +++ b/lib/yeoman/templates/lib/ansible/templates/evolution-init-d @@ -16,6 +16,9 @@ start_services() { {% if role_pound is defined %} /etc/init.d/pound start {% endif %} +{% if role_firewall is defined %} + /etc/init.d/iptables-persistent start +{% endif %} } stop_services() { @@ -31,6 +34,9 @@ stop_services() { {% if role_pound is defined %} /etc/init.d/pound stop {% endif %} +{% if role_firewall is defined %} + /etc/init.d/iptables-persistent stop +{% endif %} } restart_services() { @@ -46,6 +52,9 @@ restart_services() { {% if role_pound is defined %} /etc/init.d/pound restart {% endif %} +{% if role_firewall is defined %} + /etc/init.d/iptables-persistent restart +{% endif %} } status_services() { @@ -61,6 +70,9 @@ status_services() { {% if role_pound is defined %} /etc/init.d/pound status {% endif %} +{% if role_firewall is defined %} + /etc/init.d/iptables-persistent status +{% endif %} } case "$1" in diff --git a/test/functional/00-firewall.js b/test/functional/00-firewall.js new file mode 100644 index 00000000..a57a38f5 --- /dev/null +++ b/test/functional/00-firewall.js @@ -0,0 +1,73 @@ +'use strict'; + +var assert = require('assert'); +var exec = require('child_process').exec; +var util = require('util'); + +var port_cmd; + +describe('firewall ports', function(done) { + it('should resolve host', function(done) { + if (process.env.TRAVIS) { + port_cmd = 'sudo iptables -L -n | grep :%d'; + done(); + } else { + exec('php -r "echo gethostbyname(\'local.example.com.\');"', { + cwd: process.cwd() + '/temp' + }, function(err, stdout, stderr) { + assert.ifError(err); + port_cmd = 'nc -z -w 1 ' + stdout.trim() + ' %d'; + done(); + }); + } + }); + + describe('should be closed', function(done) { + it('mysql', function(done) { + exec(util.format(port_cmd, '3306'), { + cwd: process.cwd() + '/temp' + }, function(err, stdout, stderr) { + assert.ok(err); + done(); + }); + }); + + it('apache backend', function(done) { + exec(util.format(port_cmd, '8080'), { + cwd: process.cwd() + '/temp' + }, function(err, stdout, stderr) { + assert.ok(err); + done(); + }); + }); + }); + + describe('should be open', function(done) { + it('ssh', function(done) { + exec(util.format(port_cmd, '22'), { + cwd: process.cwd() + '/temp' + }, function(err, stdout, stderr) { + assert.ifError(err); + done(); + }); + }); + + it('http', function(done) { + exec(util.format(port_cmd, '80'), { + cwd: process.cwd() + '/temp' + }, function(err, stdout, stderr) { + assert.ifError(err); + done(); + }); + }); + + it('https', function(done) { + exec(util.format(port_cmd, '443'), { + cwd: process.cwd() + '/temp' + }, function(err, stdout, stderr) { + assert.ifError(err); + done(); + }); + }); + }); +});