Project

asbestos

0.02
No commit activity in last 3 years
No release in over 3 years
Asbestos is a declarative DSL for building firewall rules (iptables, at this point)
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

~> 1.3
>= 0
>= 0

Runtime

 Project Readme

Asbestos

Asbestos is a mostly-declarative high-level DSL for firewalls. Show Asbestos where your services run and how they connect and it'll generate your firewall rules for you.

Trying to maintain a set of interconnected firewall rules is pretty annoying, hopefully Asbestos can help.

At the moment, Asbestos only supports IPTables (the filter table, specifically), but it can be easily expanded for other firewall types.

Build Status

Installation

Simply:

$ gem install asbestos

The asbestos executable should now be in your PATH.

Usage

Asbestos is Ruby at heart, you'll need to put your Asbestos configuration in files that end with .rb.

Quick Start

Check out the examples in the examples directory, generate firewall rules for any of the hosts like so:

asbestos rules --host some_hostname_here asbestos_example.rb

(not so) Quick Start

Let's run through a basic example then expand it out as we go.

Here's a simple http server, let's call it "app_host":

host 'app_host' do
  runs :ssh
  runs :http
end

running asbestos, that gives us:

# Generated by Asbestos at 2013-06-16 20:51:13 UTC for app_host
# http://www.github.com/koudelka/asbestos
*filter
:INPUT ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
# Begin [ssh]
-A INPUT -j ACCEPT -p tcp -m state --state NEW --dport 22 -m comment --comment "allow ssh(tcp port 22) from anyone"
# End [ssh]

# Begin [http]
-A INPUT -j ACCEPT -p tcp -m state --state NEW --dport 80 -m comment --comment "allow http(tcp port 80) from anyone"
# End [http]

-A INPUT -j DROP -m comment --comment "drop packets that haven't been explicitly accepted"
COMMIT
# Asbestos completed at 2013-06-16 20:51:13 UTC

Asbestos has opened tcp port 80 and port 22 to everyone, on all of app_host's interfaces.

(The rest of the guide excludes the pre/postamble for brevity).

Let's add another host, our app server needs to talk to our database server, which runs MongoDB. The database server should only listen for traffic from app_host and nobody else. To do that, we'll need to tell Asbestos a little about our network interfaces, here's app_host again with an interface declared.

host 'app_host' do
  interface :internal, :eth0
  # 'internal' is an arbitrary name, you can make it anything you want

  runs :ssh
  runs :http
end

host 'db_host' do
  runs :ssh
  runs :mongodb, from: { Host['app_host'] => :internal }
end

Here's the rule that Asbestos generates for the mongodb service on db_host:

# Begin [mongodb]
-A INPUT -j ACCEPT -p tcp -s app_host_internal -m state --state NEW --dport 27017 -m comment --comment "allow mongodb(tcp port 27017) from app_host:eth0 (internal)"
# End [mongodb]

You'll notice that Asbestos makes an assumption about the address of app_host's "internal" interface, we'll cover that later (don't worry, you can change it).

Now mongodb is only accepting connections from app_host's internal interface, we can lock it down a little more by requiring that traffic to be through db_host's own internal interface (assuming packets are routable between the two).

host 'db_host' do
  interface :internal, :eth0

  runs :ssh
  runs :mongodb, on: :internal, from: { Host['app_host'] => :internal }
end

Asbestos will now give the following more stringent rule for mongodb on db_host:

# Begin [mongodb]
-A INPUT -j ACCEPT -i eth0 -p tcp -s app_host_internal -m state --state NEW --dport 27017 -m comment --comment "allow mongodb(tcp port 27017) from app_host:eth0 (internal) on eth0"
# End [mongodb]

Not only does our app host need to talk to the database, but we've also got a developer that needs to make db dumps, her machine is named dax, mongodb needs to listen for her connections.

host 'dax' do
  interface :internal, :eth0
end

host 'db_host' do
  interface :internal, :eth0

  runs :ssh
  runs :mongodb, on: :internal, from: { Host['app_host'] => :internal,
                                        Host['dax']      => :internal }
end

Resulting in the additional of a single rule for dax.

# Begin [mongodb]
-A INPUT -j ACCEPT -i eth0 -p tcp -s app_host_internal -m state --state NEW --dport 27017 -m comment --comment "allow mongodb(tcp port 27017) from app_host:eth0 (internal) on eth0"
-A INPUT -j ACCEPT -i eth0 -p tcp -s dax_internal -m state --state NEW --dport 27017 -m comment --comment "allow mongodb(tcp port 27017) from dax:eth0 (internal) on eth0"
# End [mongodb]

Groups

It's pretty unlikely that we'll only have two hosts in our topology, let's add some more hosts and explore how Asbestos' groups work. We'll add a couple app hosts and start using the group DSL call.

host 'app_host_0' do
  group :app_hosts

  interface :internal, :eth0

  runs :ssh
  runs :http
end

host 'app_host_1' do
  group :app_hosts

  interface :internal, :eth0

  runs :ssh
  runs :http
end

host 'app_host_2' do
  group :app_hosts

  interface :internal, :eth0

  runs :ssh
  runs :http
end

host 'dax' do
  interface :internal, :eth0
end

host 'db_host' do
  interface :internal, :eth0

  runs :ssh
  runs :mongodb, on: :internal, from: { :app_hosts  => :internal,
                                        Host['dax'] => :internal }
end

Our three app hosts belong to the group called app_hosts. Now mongodb listens for connections from any host in that group, Asbestos gives the following rules for the mongodb:

# Begin [mongodb]
-A INPUT -j ACCEPT -i eth0 -p tcp -s app_host_0_internal -m state --state NEW --dport 27017 -m comment --comment "allow mongodb(tcp port 27017) from app_host_0:eth0 (internal) on eth0"
-A INPUT -j ACCEPT -i eth0 -p tcp -s app_host_1_internal -m state --state NEW --dport 27017 -m comment --comment "allow mongodb(tcp port 27017) from app_host_1:eth0 (internal) on eth0"
-A INPUT -j ACCEPT -i eth0 -p tcp -s app_host_2_internal -m state --state NEW --dport 27017 -m comment --comment "allow mongodb(tcp port 27017) from app_host_2:eth0 (internal) on eth0"
-A INPUT -j ACCEPT -i eth0 -p tcp -s dax_internal -m state --state NEW --dport 27017 -m comment --comment "allow mongodb(tcp port 27017) from dax:eth0 (internal) on eth0"
# End [mongodb]

Host Templates

So we've got groups now, but wrangling more than a couple machines with common attributes is pretty ungainly, let's DRY up the app hosts with a host_template and a little Ruby.

host_template 'app_host' do
  group :app_hosts

  interface :internal, :eth0

  runs :ssh
  runs :http
end

0.upto(2) do |i|
  app_host "app_host_#{i}"
end


host 'dax' do
  interface :internal, :eth0
end

host 'db_host' do
  interface :internal, :eth0

  runs :ssh
  runs :mongodb, on: :internal, from: { :app_hosts  => :internal,
                                        Host['dax'] => :internal }
end

The host_template DSL call has created a new app_host DSL call for us, it's just like the host DSL call. You can pass these custom hosts blocks to extend them further than their template, too:

app_host 'app_host_3' do
  runs :nfs
end

Arbitrary Addresses

Sometimes you need to allow traffic from external addresses, you could do that with a host entity, but that's a lot of overhead to get a simple address into the mix. To make it dead simple, Asbestos provides the address DSL call to maintain a list of named addresses.

address :load_balancers, ['lb0.myprovider.com', 'lb1.myprovider.com']
address :monitoring, 'pinger.monitoringservice.com'

host 'app_host' do
  runs :http, from: [:load_balancers, :monitoring]
end

Results in:

# Begin [http]
-A INPUT -j ACCEPT -p tcp -s lb0.myprovider.com -m state --state NEW --dport 80 -m comment --comment "allow http(tcp port 80) from lb0.myprovider.com"
-A INPUT -j ACCEPT -p tcp -s lb1.myprovider.com -m state --state NEW --dport 80 -m comment --comment "allow http(tcp port 80) from lb1.myprovider.com"
-A INPUT -j ACCEPT -p tcp -s pinger.monitoringservice.com -m state --state NEW --dport 80 -m comment --comment "allow http(tcp port 80) from pinger.monitoringservice.com"
# End [http]

Interface Addresses

By default, Asbestos sets interface addresses to sensible defaults, like so:

host 'kira' do
  interface :external, :eth0  #=> address is "kira_external"

  interface :dmz, [:eth1, :eth2]  #=> addresses are "kira_dmz_eth1" and "kira_dmz_eth2"
end

You're more than welcome to override Asbestos' defaults:

host 'kira' do
  group :developers

  interface :external, :eth3 do |host|
    [host.group, host.name, 'foo'].join('_')
  end
  #=> address is "developers_kira_foo"

  interface :internal, :eth4, 'bar' #=> address is "bar"
end

Of course, these addresses will need to be resolvable by the hosts executng the rules (via DNS or /etc/hosts).

Services

For the most part, Services are simply defined as a set of port numbers (or names, via /etc/services), like so:

service :nfs do
  ports :nfs, :sunrpc
  protocols :tcp, :udp
end

host 'myhost' do
  runs :nfs
end

Results in:

# Begin [nfs]
-A INPUT -j ACCEPT -p udp -m state --state NEW --dport nfs -m comment --comment "allow nfs(udp port nfs) from anyone"
-A INPUT -j ACCEPT -p udp -m state --state NEW --dport sunrpc -m comment --comment "allow nfs(udp port sunrpc) from anyone"
-A INPUT -j ACCEPT -p tcp -m state --state NEW --dport nfs -m comment --comment "allow nfs(tcp port nfs) from anyone"
-A INPUT -j ACCEPT -p tcp -m state --state NEW --dport sunrpc -m comment --comment "allow nfs(tcp port sunrpc) from anyone"
# End [nfs]

Asbestos comes with a small set of service definitions, located in lib/asbestos/services.

RuleSets

Simply opening ports between hosts is usually not sufficient for most needs, that's where RuleSets come in. RuleSets are named chunks of Asbestos/Ruby that are executed in the context of the host. They can easily expose lower level firewall functionality in a clean way. For example, the included icmp_protection ruleset:

rule_set :icmp_protection do

  accept :chain     => :output,
         :protocol  => :icmp,
         :icmp_type => 'echo-request',
         :comment   => "allow us to ping others"

  accept :protocol  => :icmp,
         :icmp_type => 'echo-reply',
         :comment   => "allow us to receive ping responses"


  interfaces[:external].each do |interface|
    from_each_address(allowed_from) do |address|
      accept :protocol  => :icmp,
             :icmp_type => 'echo-request',
             :interface => interface,
             :remote_address => address,
             :limit   => '1/s',
             :comment => "allow icmp from #{address}"
    end

    drop :protocol  => :icmp,
         :interface => interface,
         :comment   => "drop any icmp packets that haven't been explicitly allowed"
  end
end

address :monitoring, 'pinger.monitoringservice.com'

host 'kira' do
  interface :external, ['eth1', 'eth1:0']

  icmp_protection allowed_from: :monitoring
end

Results in:

# Begin [icmp_protection]
-A OUTPUT -j ACCEPT -p icmp --icmp-type echo-request -m comment --comment "allow us to ping others"
-A INPUT -j ACCEPT -p icmp --icmp-type echo-reply -m comment --comment "allow us to receive ping responses"
-A INPUT -j ACCEPT -i eth1 -p icmp -s pinger.monitoringservice.com -m limit --limit 1/s --icmp-type echo-request -m comment --comment "allow icmp from pinger.monitoringservice.com on eth1"
-A INPUT -j DROP -i eth1 -p icmp -m comment --comment "drop any icmp packets that haven't been explicitly allowed on eth1"
-A INPUT -j ACCEPT -i eth1:0 -p icmp -s pinger.monitoringservice.com -m limit --limit 1/s --icmp-type echo-request -m comment --comment "allow icmp from pinger.monitoringservice.com on eth1:0"
-A INPUT -j DROP -i eth1:0 -p icmp -m comment --comment "drop any icmp packets that haven't been explicitly allowed on eth1:0"
# End [icmp_protection]

The functions available to RuleSets are translated to firewall rules by the appropriate firewall module, just lib/asbestos/firewalls/iptables.rb for now.

Asbestos comes with a small number of rule sets , located in lib/asbestos/rule_sets.

Overriding Defaults

Services and RuleSets are akin to classes, when a host includes them, a new instance is created. The arguments of any method sent to them (that isn't a firewall module method) get stored as an attribute of the instance and become accessible as a DSL method, this makes RulesSets and Services easy to write in a DSL-y manner. For example, the allowed_from attribute in the icmp_protection example above, or the port attribute below:

host 'kira' do
  runs :ssh, port: 22022
end

becomes:

# Begin [ssh]
-A INPUT -j ACCEPT -p tcp -m state --state NEW --dport 22022 -m comment --comment "allow ssh(tcp port 22022) from anyone"
# End [ssh]

Literal Commands

If you just want to add a literal firewall command, you can use the command DSL call inside of a rule_set:

rule_set :creates_chaos do
  command "-A INPUT -m statistic --mode random --probability 0.01 -j REJECT --reject-with host-unreach"
end

host 'kira' do
  creates_chaos
end

as expected, will give you:

# Begin [creates_chaos]
-A INPUT -m statistic --mode random --probability 0.01 -j REJECT --reject-with host-unreach
# End [creates_chaos]

If you find yourself needing to use command, please consider expanding the functionality of Asbestos::Firewall::IPTables#rule instead.

Generating Rules

Development

While you're developing your rules, you can generate the rules for an arbitrary host like so:

asbestos rules --host some_hostname_here asbestos_example.rb

Production

When you're ready to run your rules against your hosts, you'll need to send the rules from asbestos's stdout somewhere. You can pipe it directly to iptables-restore:

asbestos rules asbestos_example.rb | iptables-restore

(Calling the asbestos executable without --host will generate the corresponding rules for the current host)

However, I suggest you send the rules to a file somewhere in /etc and have your if-up process automatically apply them when the interface is brought up. Your flavor of linux may already have this mechanism built-in, try looking in /etc/network/if-up.d.

The asbestos executable can output your firewall rules on both stdout and stderr, with a slight difference, the rules that are printed on stderr contain line numbers. If there's a problem with your rules, iptables-restore will give you the line number of the error. To turn on the stderr output, use the --debug-stderr flag. You use this flag to pipe stdout directly to iptables-restore and view the rules in your terminal at the same time:

asbestos rules --debug-stderr asbestos_example.rb | iptables-restore

Keeping Things Organized

It's probably a good idea to keep various parts of your Asbestos configuration in different files, it's up to you how you do it.

You can use Ruby's require functionality to assemble your files, or you can provide all the file names to the asbestos executable.

Caveats

  • As with any framework, you may be sacrificing some degree of low-level control. Asbestos was designed to make firewalls for large networks a little saner, which means a degree of standardization. If you find yourself wishing for more control, try expanding the #rule method of the Asbestos::Firewall::IPTables module.

  • Of course, you are a fully qualified operator, conversant in your firewall of choice. You'll read all the rules that Asbestos generates before running them on your machines. Shit happens, it's best to give things a once over before making a mistake. :)

  • This is just a tool I built to make my life easier, I hope it helps you too. If you take issue with how Asbestos does things, please drop me a line, I'd love hear your thoughts so we can make a better tool for everyone. The infighting in OSS that comes from hiding behind a computer is not cool, let's be excellent to eachother and do kickass work. :)

Contributing

Hug and a beer for anyone submitting pull requests!

  1. Fork it
  2. Fix my bugs or add new features with tests (please). :)
  3. Create your feature branch (git checkout -b my-new-feature)
  4. Commit your changes (git commit -am 'Add some feature')
  5. Push to the branch (git push origin my-new-feature)
  6. Create new Pull Request