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.
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 theAsbestos::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!
- Fork it
- Fix my bugs or add new features with tests (please). :)
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request