Distributed Firewall Configuration Part 2: Ferm Configuration with Salt

← Blog Home

RSS

By

This article is a continuation of a previous article on managing a distributed firewall using salt and ferm, which you can read here.

First, a few notes about the ferm configuration files in this post:

  • SM stands for “Salt Managed” in file names, as a hint to administrators of the system.
  • Feel free to copy / paste for the formatting, but be sure to change all of the contents, or you’ll be giving random people on the internet access to your systems.

I suggest building a testing system specifically for the design of your firewall. On that system, install your base software and the packages you want to test first, then install the ferm and salt-minion packages.

Step 1: Establish your internal and trusted lists

You need to have a list of all the IP addresses that should have wide access to most things, like internal documentation servers, outbound SMTP relays, and communication servers. For our purposes, we call the list of all the IP address blocks that our systems use “internal” and the list of all the static blocks our home users use are “trusted”. You would probably also want VPN users and offices to be on the “trusted” list.

SM-trusted-all.conf:

#centrally managed by Salt.  Please be aware any changes to this could be overwritten at any time.

@def $NET_TRUSTED_ALL = (
    # Billy User
    123.45.67.88/29
    2654:184A:2C56:F329::/64

    # Bobby User
    45.76.34.96/28
    25FF:F932:1::/48

    # All Users VPN
    10.10.0.0/16 

    # Main Office
    124.133.98.83
);

domain (ip ip6) {
    chain TRUSTED_ALL {
        saddr @ipfilter($NET_TRUSTED_ALL) ACCEPT;
    }
    # THIS PART IS OPTIONAL, ONLY INCLUDE IT IF YOU HAVE SERVICES YOU WANT ALL TRUSTED USERS TO GET TO ON EVERY SYSTEM, example ICMP
    chain INPUT {
         proto icmp jump TRUSTED_ALL;
    }
}

SM-trusted-ops.conf:

#centrally managed by Salt.  Please be aware any changes to this could be overwritten at any time.

@def $NET_TRUSTED_OPS = (

    # Bobby User
    45.76.34.96/28
    25FF:F932:1::/48

    # Bobby User VPN
    10.10.3.243 

    # Operations Office
    10.9.8.0/24
);

domain (ip ip6) {
    chain TRUSTED_OPS {
        saddr @ipfilter($NET_TRUSTED_OPS) ACCEPT;
    }
    # THIS PART IS OPTIONAL, ONLY INCLUDE IT IF YOU HAVE SERVICES YOU WANT ALL TRUSTED OPS USERS TO GET TO ON EVERY SYSTEM, example ssh
    chain INPUT {
         proto tcp dport sshd jump TRUSTED_OPS;
    }
}

SM-internal-all.conf:

#centrally managed by Salt.  Please be aware any changes to this could be overwritten at any time.

@def $NET_INTERNAL_ALL = (
    # West Coast DC
    48.34.87.0/24
    5432:1234:6789::/48

    # East Coast DC
    34.46.78.0/24
    DD33:FF88:123::/48

);

domain (ip ip6) {
    chain INTERNAL_ALL {
        saddr @ipfilter($NET_INTERNAL_ALL) ACCEPT;
    }
    # THIS PART IS OPTIONAL, ONLY INCLUDE IT IF YOU HAVE SERVICES YOU WANT ALL INTERNAL SYSTEMS TO GET TO ON EVERY SYSTEM, example ICMP
    chain INPUT {
         proto icmp jump INTERNAL_ALL;
    }
}

SM-internal-web.conf:

#centrally managed by Salt.  Please be aware any changes to this could be overwritten at any time.

@def $NET_INTERNAL_WEB = (
    # West Coast DC
    48.34.87.48/29
    5432:1234:6789:48::/64

    # East Coast DC
    34.46.78.48/29
    DD33:FF88:123:48::/64

);

domain (ip ip6) {
    chain INTERNAL_WEB {
        saddr @ipfilter($NET_INTERNAL_WEB) ACCEPT;
    }
    # No optional section provided to let your web servers access everything, as I can’t do so in good conscience
}

You should build a separate “trusted” configuration for each group of users (e.g., operations staff, developers, and IT users) and “internal” configuration for each group of servers (e.g., web, database, authentication, and monitoring servers). These can also be broken down by project if you want to get even more secure, or merged if it makes sense. Tune it to your needs, this is a tool not a rulebook. We’ll set it up later so that if a config isn’t used it’s not placed on the system, and we’re not running through unneeded iptables rules.

Remember, if you include the optional portion in the above configurations, it will be on every system that config gets dropped on. The primary purpose of those configuration files is to establish the groups, so the optional portion should only be used for similarly universal reasons, such as operations access to ssh and monitoring access to nrpe. It’s important to use a different file for each of the groups to more easily determine what’s going on on each individual host. Note that breaking them into more files will not impact performance (unless you’re really low on inodes), since they are read and processed into iptables rules.

Step 2: Making the Rules

After you have the groups of servers and users built, then you’re ready to begin using them to allow access to services. We’re going to do this by building them into salt. We start with a base set of rules, as follows:

SM-base.conf:

#Centrally managed by Salt.  Please be aware any changes to this could be overwritten at any time.
domain (ip ip6) {
    chain INPUT {
        policy DROP;

        # connection tracking
        mod state state INVALID DROP;
        mod state state (ESTABLISHED RELATED) ACCEPT;

        # allow local packet
        interface lo ACCEPT;

        # respond to ping
        proto icmp ACCEPT; 
    }
    chain OUTPUT {
        policy ACCEPT;

        # connection tracking
        mod state state INVALID DROP;
        mod state state (ESTABLISHED RELATED) ACCEPT;
    }
    chain FORWARD {
        policy DROP;
    }
}

We then integrate the salt pillars from the last article with a jinja formatted services file like the one below. Please note that if you haven’t done the salt configuration part this won’t work yet: we have to tell salt what to do with it. You’ll need to tweak it for your config. I’m going to make it match the four above.

SM-services.conf:

#Centrally managed by Salt.  Please be aware any changes to this could be overwritten at any time.

domain (ip ip6) {
    chain INPUT {
{%- if salt['pillar.get’](‘services:public') %}
{%-   for service in pillar['services']['public'] %}
        proto {{pillar['services']['public’][service]['protocol']}} dport {{pillar['services']['public'][service]['port']}} ACCEPT;
{%-   endfor %}
{%- endif %}
{%- if salt['pillar.get']('services:trusted-all’) %}
{%-   for service in pillar['services']['trusted-all’] %}
        proto {{pillar['services']['trusted-all’][service]['protocol']}} dport {{pillar['services']['trusted-all’][service]['port']}} jump TRUSTED-ALL;
{%-   endfor %}
{%- endif %}
{%- if salt['pillar.get']('services:trusted-ops’) %}
{%-   for service in pillar['services']['trusted-ops’] %}
        proto {{pillar['services']['trusted-ops’][service]['protocol']}} dport {{pillar['services']['trusted-ops’][service]['port']}} jump TRUSTED-OPS;
{%-   endfor %}
{%- endif %}
{%- if salt['pillar.get']('services:internal-all’) %}
{%-   for service in pillar['services']['internal-all’] %}
        proto {{pillar['services']['internal-all’][service]['protocol']}} dport {{pillar['services']['internal-all’][service]['port']}} jump INTERNAL-ALL;
{%-   endfor %}
{%- endif %}
{%- if salt['pillar.get']('services:internal-web’) %}
{%-   for service in pillar['services']['internal-web’] %}
        proto {{pillar['services']['internal-web’][service]['protocol']}} dport {{pillar['services']['internal-web’][service]['port']}} jump INTERNAL-WEB;
{%-   endfor %}
{%- endif %}
    }
}

Let’s break down what’s happening in the above config file. There are 5 possible sections of the file above, one for each of the groups of rules we want to check for: public, trusted-all, trusted-ops, internal-all, internal-web. If there are any rules in that section, then it loops through all of the entries in that section and inserts a rule for each of the ports for the appropriate group. You’ll notice that “public” wasn’t defined in one of the earlier groups… but that’s because it’s a simple ACCEPT rule, instead of a redirection to a group, meaning it’s allowed for everyone, everywhere. Public servers like your mail and web servers should be done that way, but just about everything else from your databases to your file servers should be defined in groups with access specialized.

Now we need to build the main ferm configuration, which is going to pull in all of these configuration files. For your production systems, you may want to include the specific files instead of the whole directory, since they should be fully automated. You’d use a very similar jinja template to the services file or the init.sls checks in the salt configuration section to accomplish that task.

ferm.conf:

#Centrally managed by Salt.  Please be aware any changes to this could be overwritten at any time.

@include 'conf.d/SM-base.conf’;
@include 'conf.d/SM-trusted-all.conf’;
@include 'conf.d/SM-internal-all.conf’;

{%- if salt['pillar.get']('services') %}
@include 'conf.d/SM-services.conf’;
{%- endif %}
{%- if salt['pillar.get']('services:trusted-ops) %}
@include 'conf.d/SM-trusted-ops.conf’;
{%- endif %}
{%- if salt['pillar.get']('services:internal-web’) %}
@include 'conf.d/SM-internal-web.conf’;
{%- endif %}

Making Exceptions

For development systems where you want to allow local configuration files:

ferm.conf:

#centrally managed by Salt.  Please be aware any changes to this could be overwritten at any time.
@include 'conf.d/';

I’ll provide an example of a local configuration here, especially helpful during the development cycle, or for a quick one-off which should be only on the one local server, and not deployed through salt:

local-web.conf:

# Local Configuration with thall’s home IP for testing the development website.  Expires June 12, 2015.
domain (ip ip6) {
    chain INPUT {
        proto tcp saddr 43.123.21.12 dport (80 443) ACCEPT;
        proto tcp saddr F954:11C:9832:1234:4321:7773:1298:1324 dport (80 443) ACCEPT;
    }

You should watch the file count, and (depending on your level of paranoia) also the md5sum of the files being pushed with your monitoring system to ensure the firewall stays secure. Every time salt is pushed to the box it will ensure that the files are returned to their expected state and including the entire directory allows for local configuration files to be added.

Conclusion

Now that we’ve done all the work, here’s the payoff: When your web server gets compromised, the attackers won’t be able to bounce from there to your mail server and publish all of your company’s email. If you’ve divided the frontend and backend in a security conscious manner where the frontend only talks to the backend over specific, limited APIs, the attackers can’t get from your frontend web servers to your databases to get your customer list. Even your users will only be able to access the systems that they are allowed to, limiting the effect of social engineering or malware to only the systems that they are allowed to access. And when you consider the overall security benefits to your organization, it’s really not all that much work.

Travis Hall is a Systems Administrator for Farsight Security, Inc.

← Blog Home

Protect against cybercriminal activity in real-time.

Request demo

Email: sales@farsightsecurity.com Phone: +1-650-489-7919