使用 iptables 管理防火墙

Programming can be fun, so can cryptography; however they should not be combined.

— Kreitzberg and Shneiderman

C 编程语言被形容为 “只写” 的语言;它是如此的简洁、高效, 甚至你自己读自己写过的代码都可能很难理解。 同样地,Linux 内核内置的包过滤防火墙的 iptables 的配置也是如此。 一条原始的 iptables 命令规则看上去像这样:

iptables -A INPUT -d 10.0.2.15/32 -p tcp -m tcp --dport 80 -j ACCEPT

除非你会因为掌握了命令行的这些似乎毫无意义的字符串而获得男子气概 (诚然这是 UNIX 系统管理员的职业病), 否则,能够以更加象征性和可读性的方式来表达防火墙规则是更好的选择。 Puppet 在这方面可以为我们提供帮助,因为我们可以用它对 iptables 的实现细节进行抽象, 并通过参考管理员所控制的服务角色来定义防火墙规则,例如:

iptables::role { "web-server": }

准备工作

你需要我们在第 5 章 为配置文件添加配置行 一节中创建的 append_if_no_such_line 函数。

操作步骤

  1. 创建一个 iptables 模块:

    # mkdir /etc/puppet/modules/iptables
    # mkdir /etc/puppet/modules/iptables/manifests
    # mkdir /etc/puppet/modules/iptables/files
    
  2. 使用如下内容创建 /etc/puppet/modules/iptables/manifests/init.pp 文件:

    class iptables {
        file { [ "/root/iptables",
                 "/root/iptables/hosts",
                 "/root/iptables/roles" ]:
            ensure => directory,
        }
    
        file { "/root/iptables/roles/common":
            source => "puppet:///modules/iptables/common.role",
            notify => Exec["run-iptables"],
        }
    
        file { "/root/iptables/names":
            source => "puppet:///modules/iptables/names",
            notify => Exec["run-iptables"],
        }
    
        file { "/root/iptables/iptables.sh":
            source => "puppet:///modules/iptables/iptables.sh",
            mode   => "755",
            notify => Exec["run-iptables"],
        }
    
        file { "/root/iptables/hosts/${hostname}":
            content => "export MAIN_IP=${ipaddress}\n",
            replace => false,
            require => File["/root/iptables/hosts"],
            notify  => Exec["run-iptables"],
        }
    
        exec { "run-iptables":
            cwd         => "/root/iptables",
            command     => "/usr/bin/test -f hosts/${hostname} &&
     /root/iptables/iptables.sh && /sbin/iptables-save >
     /etc/iptables.rules",
            refreshonly => true,
        }
    
        append_if_no_such_line { "restore iptables rules":
            file => "/etc/network/interfaces",
            line => "pre-up iptables-restore < /etc/iptables.rules",
        }
    
        define role() {
            include iptables
    
            file { "/root/iptables/roles/${name}":
                source  => "puppet:///modules/iptables/${name}.role",
                replace => false,
                require => File["/root/iptables/roles"],
                notify  => Exec["run-iptables"],
            }
    
            append_if_no_such_line { "${name} role":
                file    => "/root/iptables/hosts/${hostname}",
                line    => ". `dirname \$0`/roles/${name}",
                require => File["/root/iptables/hosts/${hostname}"],
                notify => Exec["run-iptables"],
            }
        }
    }
    
  3. 使用如下内容创建 /etc/puppet/modules/iptables/files/iptables.sh 文件:

    # Server names and ports
    . `dirname $0`/names
    
    # Interfaces (override in host-specific file if necessary)
    export EXT_INTERFACE=eth0
    
    # Flush and remove all chains
    iptables -P INPUT ACCEPT
    iptables -P OUTPUT ACCEPT
    iptables -F
    iptables -X
    
    # Allow all traffic on loopback interface
    iptables -I INPUT 1 -i lo -j ACCEPT
    iptables -I OUTPUT 1 -o lo -j ACCEPT
    
    # Allow established and related connections
    iptables -I INPUT 2 -m state --state ESTABLISHED,RELATED -j ACCEPT
    iptables -I OUTPUT 2 -m state --state ESTABLISHED,RELATED -j ACCEPT
    
    # Include machine specific settings
    HOST_RULES=`dirname $0`/hosts/`hostname -s`
    [ -f ${HOST_RULES} ] && . ${HOST_RULES}
    [ "${MAIN_IP}" == "" ] && ( echo No MAIN_IP was set, \
     please set the primary IP address in ${HOST_RULES}. ; exit 1 )
    
    # Include common settings
    . `dirname $0`/roles/common
    
    # Drop all non-matching packets
    iptables -A INPUT -j LOG --log-prefix "INPUT: "
    iptables -A INPUT -j DROP
    iptables -A OUTPUT -j LOG --log-prefix "OUTPUT: "
    iptables -A OUTPUT -j DROP
    
    echo -e "Test remote login and then:\n iptables-save \
     >/etc/iptables.rules"
    
  4. 使用如下内容创建 /etc/puppet/modules/iptables/files/names 文件:

    # Servers
    export PUPPETMASTER=10.0.2.15
    
    # Well-known ports
    export DNS=53
    export FTP=21
    export GIT=9418
    export HEARTBEAT=694
    export IMAPS=993
    export IRC=6667
    export MONIT=2828
    export MYSQL=3306
    export MYSQL_MASTER=3307
    export NRPE=5666
    export NTP=123
    export POSTGRES=5432
    export PUPPET=8140
    export RSYNCD=873
    export SMTP=25
    export SPHINX=3312
    export SSH=22
    export STARLING=3307
    export SYSLOG=514
    export WEB=80
    export WEB_SSL=443
    export ZABBIX=10051
    
  5. 使用如下内容创建 /etc/puppet/modules/iptables/files/common.role 文件:

    # Common rules for all hosts
    iptables -A INPUT -p tcp -m tcp -d ${MAIN_IP} --dport ${SSH} -j ACCEPT
    
    iptables -A INPUT -p ICMP --icmp-type echo-request -j ACCEPT
    iptables -A OUTPUT -p ICMP --icmp-type echo-request -j ACCEPT
    
    iptables -A OUTPUT -p tcp --dport ${SSH} -j ACCEPT
    iptables -A OUTPUT -p tcp --dport ${SMTP} -j ACCEPT
    iptables -A OUTPUT -p udp --dport ${NTP} -j ACCEPT
    iptables -A OUTPUT -p tcp --dport ${NTP} -j ACCEPT
    iptables -A OUTPUT -p udp --dport ${DNS} -j ACCEPT
    iptables -A OUTPUT -p tcp --dport ${WEB} -j ACCEPT
    iptables -A OUTPUT -p tcp --dport ${WEB_SSL} -j ACCEPT
    iptables -A OUTPUT -p tcp -d ${PUPPETMASTER} --dport ${PUPPET} -j ACCEPT
    iptables -A OUTPUT -p tcp --dport ${MYSQL} -j ACCEPT
    
    # Drop some commonly probed ports
    iptables -A INPUT -p tcp --dport 23 -j DROP # telnet
    iptables -A INPUT -p tcp --dport 135 -j DROP # epmap
    iptables -A INPUT -p tcp --dport 139 -j DROP # netbios
    iptables -A INPUT -p tcp --dport 445 -j DROP # Microsoft DS
    iptables -A INPUT -p udp --dport 1433 -j DROP # SQL server
    iptables -A INPUT -p tcp --dport 1433 -j DROP # SQL server
    iptables -A INPUT -p udp --dport 1434 -j DROP # SQL server
    iptables -A INPUT -p tcp --dport 1434 -j DROP # SQL server
    iptables -A INPUT -p tcp --dport 2967 -j DROP # SSC-agent
    
  6. 使用如下内容创建 /etc/puppet/modules/iptables/files/web-server.role 文件:

    # Access to web
    iptables -A INPUT -p tcp -d ${MAIN_IP} --dport ${WEB} -j ACCEPT
    
    # Send mail from web applications
    iptables -A OUTPUT -p tcp --dport ${SMTP} -j ACCEPT
    
  7. 使用如下内容创建 /etc/puppet/modules/iptables/files/puppet-server.role 文件:

    # Access to puppet
    iptables -A INPUT -p tcp -d ${MAIN_IP} --dport ${PUPPET} -j ACCEPT
    
  8. 在你的 Puppetmaster 节点上包含如下内容:

    iptables::role { "web-server": }
    iptables::role { "puppet-server": }
    
  9. 运行 Puppet:

    # puppet agent --test
    info: Retrieving plugin
    info: Caching catalog for cookbook.bitfieldconsulting.com
    info: Applying configuration version '1311682880'
    
    notice: /Stage[main]/Iptables/File[/root/iptables]/ensure: created
    notice: /Stage[main]/Iptables/File[/root/iptables/names]/ensure:
    defined content as '{md5}9bb004a7d2c6d70616b149d044c22669'
    
    info: /Stage[main]/Iptables/File[/root/iptables/names]: Scheduling
    refresh of Exec[run-iptables]
    
    notice: /Stage[main]/Iptables/File[/root/iptables/hosts]/ensure:
    created
    
    notice: /Stage[main]/Iptables/File[/root/iptables/hosts/cookbook]/
    ensure: defined content as '{md5}d00bc730514bbb74cdef3dad70058a81'
    
    info: /Stage[main]/Iptables/File[/root/iptables/hosts/cookbook]:
    Scheduling refresh of Exec[run-iptables]
    
    notice: /Stage[main]//Node[cookbook]/Iptables::Role[web-server]/
    Append_if_no_such_line[web-server role]/Exec[/bin/echo '. `dirname
    $0`/roles/web-server' >> '/root/iptables/hosts/cookbook']/returns:
    executed successfully
    
    info: /Stage[main]//Node[cookbook]/Iptables::Role[web-server]/
    Append_if_no_such_line[web-server role]/Exec[/bin/echo '. `dirname
    $0`/roles/web-server' >> '/root/iptables/hosts/cookbook']:
    Scheduling refresh of Exec[run-iptables]
    
    notice: /Stage[main]//Node[cookbook]/Iptables::Role[puppetserver]/
    Append_if_no_such_line[puppet-server role]/Exec[/bin/echo
    '. `dirname $0`/roles/puppet-server' >> '/root/iptables/hosts/
    cookbook']/returns: executed successfully
    
    info: /Stage[main]//Node[cookbook]/Iptables::Role[puppet-server]/
    Append_if_no_such_line[puppet-server role]/Exec[/bin/echo '.
    `dirname $0`/roles/puppet-server' >> '/root/iptables/hosts/
    cookbook']: Scheduling refresh of Exec[run-iptables]
    
    notice: /Stage[main]/Iptables/File[/root/iptables/roles]/ensure:
    created
    
    notice: /Stage[main]//Node[cookbook]/Iptables::Role[puppetserver]/
    File[/root/iptables/roles/puppet-server]/ensure: defined
    content as '{md5}c30a13f7792525c181e14e78c9a510cd'
    
    info: /Stage[main]//Node[cookbook]/Iptables::Role[puppet-server]/
    File[/root/iptables/roles/puppet-server]: Scheduling refresh of
    Exec[run-iptables]
    
    notice: /Stage[main]//Node[cookbook]/Iptables::Role[web-server]/
    File[/root/iptables/roles/web-server]/ensure: defined content as
    '{md5}11e5747cb2737903ffc34133f5fe2452'
    
    info: /Stage[main]//Node[cookbook]/Iptables::Role[web-server]/
    File[/root/iptables/roles/web-server]: Scheduling refresh of
    Exec[run-iptables]
    
    notice: /Stage[main]/Iptables/File[/root/iptables/roles/common]/
    ensure: defined content as '{md5}116f57d4e31f3e0b351da6679dca15e3'
    
    info: /Stage[main]/Iptables/File[/root/iptables/roles/common]:
    Scheduling refresh of Exec[run-iptables]
    
    notice: /Stage[main]/Iptables/File[/root/iptables/iptables.sh]/
    ensure: defined content as '{md5}340ff9fb5945e9fc7dd78b21f45dd823'
    
    info: /Stage[main]/Iptables/File[/root/iptables/iptables.sh]:
    Scheduling refresh of Exec[run-iptables]
    
    notice: /Stage[main]/Iptables/Exec[run-iptables]: Triggered
    'refresh' from 8 events
    
    notice: /Stage[main]/Iptables/Append_if_no_such_line[restore
    iptables rules]/Exec[/bin/echo 'pre-up iptables-restore < /etc/
    iptables.rules' >> '/etc/network/interfaces']/returns: executed
    successfully
    
    notice: Finished catalog run in 4.86 seconds
    
  10. 检查要求的规则是否已被安装:

    # iptables -nL
    Chain INPUT (policy ACCEPT)
    target prot opt source     destination
    ACCEPT all  --  0.0.0.0/0  0.0.0.0/0
    ACCEPT all  --  0.0.0.0/0  0.0.0.0/0    state RELATED,ESTABLISHED
    ACCEPT tcp  --  0.0.0.0/0  10.0.2.15    tcp dpt:80
    ACCEPT tcp  --  0.0.0.0/0  10.0.2.15    tcp dpt:8140
    ACCEPT tcp  --  0.0.0.0/0  10.0.2.15    tcp dpt:22
    ACCEPT icmp --  0.0.0.0/0  0.0.0.0/0    icmp type 8
    DROP   tcp  --  0.0.0.0/0  0.0.0.0/0    tcp dpt:23
    DROP   tcp  --  0.0.0.0/0  0.0.0.0/0    tcp dpt:135
    DROP   tcp  --  0.0.0.0/0  0.0.0.0/0    tcp dpt:139
    DROP   tcp  --  0.0.0.0/0  0.0.0.0/0    tcp dpt:445
    DROP   udp  --  0.0.0.0/0  0.0.0.0/0    udp dpt:1433
    DROP   tcp  --  0.0.0.0/0  0.0.0.0/0    tcp dpt:1433
    DROP   udp  --  0.0.0.0/0  0.0.0.0/0    udp dpt:1434
    DROP   tcp  --  0.0.0.0/0  0.0.0.0/0    tcp dpt:1434
    DROP   tcp  --  0.0.0.0/0  0.0.0.0/0    tcp dpt:2967
    LOG    all  --  0.0.0.0/0  0.0.0.0/0    LOG
     flags 0 level 4 prefix `INPUT: '
    DROP   all  --  0.0.0.0/0  0.0.0.0/0
    
    Chain FORWARD (policy ACCEPT)
    target prot opt source destination
    Chain OUTPUT (policy ACCEPT)
    target prot opt source     destination
    ACCEPT all  --  0.0.0.0/0  0.0.0.0/0
    ACCEPT all  --  0.0.0.0/0  0.0.0.0/0    state RELATED,ESTABLISHED
    ACCEPT tcp  --  0.0.0.0/0  0.0.0.0/0    tcp dpt:25
    ACCEPT icmp --  0.0.0.0/0  0.0.0.0/0    icmp type 8
    ACCEPT tcp  --  0.0.0.0/0  0.0.0.0/0    tcp dpt:22
    ACCEPT tcp  --  0.0.0.0/0  0.0.0.0/0    tcp dpt:25
    ACCEPT udp  --  0.0.0.0/0  0.0.0.0/0    udp dpt:123
    ACCEPT tcp  --  0.0.0.0/0  0.0.0.0/0    tcp dpt:123
    ACCEPT udp  --  0.0.0.0/0  0.0.0.0/0    udp dpt:53
    ACCEPT tcp  --  0.0.0.0/0  0.0.0.0/0    tcp dpt:80
    ACCEPT tcp  --  0.0.0.0/0  0.0.0.0/0    tcp dpt:443
    ACCEPT tcp  --  0.0.0.0/0  10.0.2.15    tcp dpt:8140
    ACCEPT tcp  --  0.0.0.0/0  0.0.0.0/0    tcp dpt:3306
    LOG    all  --  0.0.0.0/0  0.0.0.0/0    LOG
    flags 0 level 4 prefix `OUTPUT: '
    DROP   all  --  0.0.0.0/0  0.0.0.0/0
    

工作原理

为了创建一套合适的防火墙规则,我们需要知道节点的主 IP 地址以及其运行了哪些服务。 我们还需要添加一些所有的机器都要设置的共同规则(例如,允许 SSH),并运行一系列的 iptables 命令以激活我们已经生成的规则。 之后我们还要保存这些规则,以便使这些规则可以在开机时恢复执行。 下面介绍所有的这一切是如何完成的。

首先,我们创建一个 names 文件为常用的端口定义 shell 变量。 这意味着,当我们定义防火墙规则时,可以引用变量,例如对于 MySQL 服务可以使用变量 ${MYSQL} 取代数值端口号 3306。

common.role 文件包含了一些对所有机器都有用的规则。编辑这个文件以适应你自己的所有机器 (例如,你可能仅允许从指定的 IP 范围访问 SSH)。

web-server.role 和 puppet-server.role 文件包含了两个特定角色的规则。 你可以添加更多的文件用于定义你的网络中所需的众多角色:例如,数据库服务器、应用服务器、 DNS 服务器,等等。文件中所有的规则都具有如下的格式:

iptables -A INPUT -p tcp -d ${MAIN_IP} --dport ${WEB} -j ACCEPT

通常,你只需要修改 ${WEB} 这部分:将其替换为另一个端口名(定义在 names 文件中) 的变量引用(例如 ${POSTGRES})。如果你需要更多的端口名,在 names 文件中添加相应的定义。

iptables.sh 脚本读取其他的所有文件并执行规则要求的 iptables 命令。 每当相关文件有任何改变,Puppet 就执行这个脚本,因此要想刷新防火墙, 你需要做的工作就是改变相关的配置并运行 Puppet。

Puppet 还会将当前的规则集保存在 /etc/iptables.rules 文件中。 为了让机器重启后加载规则集文件,Puppet 在 /etc/network/interfaces 文件中添加了如下一行:

pre-up iptables-restore < /etc/iptables.rules

所有这一切意味着,在相关的模块中(例如 apache 模块), 你只要简单地包含如下一行即可创建相应的防火墙:

iptables::role { "web-server": }

一旦防火墙被激活,任何不符合规则的数据包将被阻止并记录到 /var/log/messages 日志文件。 检查这个文件,以帮助解决防火墙的任何问题。

更多用法

如果在你的规则中引用了某些特定的机器(例如,你的监控服务器), 可以在 names 文件中添加如下的定义:

MONITOR=10.0.2.15

然后在适当的位置(例如 common.role 文件中),你可以允许来自这台机器的访问, 例如,允许来自监控服务器对指定主机 NRPE 端口的访问规则如下:

iptables -A INPUT -p tcp -m tcp -d ${MAIN_IP} -s ${MONITOR} --dport ${NRPE} -j ACCEPT

你也可以用这种方法指定数据库服务器,以及在 .role 文件中需要引用特定的 IP 地址、 网络地址和地址范围等情况。

像这样动态生成防火墙规则集对于云基础设施是非常有用的, 云中的服务器列表会因为节点的创建和销毁而不断地变化。 对于需要触发防火墙重建的任何资源,你只要在此资源中添加如下代码即可:

notify => Exec["run-iptables"],

你可能有一个由版本控制系统维护的或通过 cloud API (例如,Rackspace 或 Amazon EC2) 自动更新的 “主服务器列表(master server list)”。 可以在 Puppet 中将这个列表定义成一个 file 资源,通过在此资源中使用 notify 参数即可触发防火墙的重建,所以每次当你检入(check in)主服务器列表的变化, 每台机器上运行的 Puppet 将相应地更新其防火墙。

当然,这种高度的自动化意味着你需要对你检入的内容格外小心, 因为任何错误都可能会导致整个基础设施离线。

测试变更的一种好方法是对用于测试的 Puppet 配置清单使用一个单独的 Git 分支, 在分支中仅将变更应用到一到两台服务器。一旦你验证了变更的正确性, 就可以将其合并到主分支并回滚到主分支。