Securing CoreOS with iptables

I've been keeping a close eye on CoreOS since it was originally announced, and in the last few months I've actually started using it for a few things. As a young project, CoreOS has lots of rough edges in terms of documentation and usability. One of the issues I ran into was how to secure a CoreOS machine's public network. By default, a fresh CoreOS installation has no firewall rules, allowing all inbound network traffic.

In order to secure a CoreOS machine, I had to learn how to configure the firewall. I use the common iptables utility for this purpose. While I was vaguely familiar with iptables, I'd never really had to learn it, so I delved in to get a more thorough understanding of it. There are plenty of resources to learn iptables on the web, so I won't go into that too much here. The issue specific to CoreOS is how to configure iptables when launching a new machine.

CoreOS is unusual in that it is extremely minimal. It's designed for all programs to be run inside Linux containers, so the OS itself contains only the subsystems and tools necessary to achieve that. iptables, however, is one of the programs that does run on the OS itself.

With a more traditional Linux distribution, it's common to launch a new instance and then provision it with a tool like Chef or Puppet. Your configuration lives in a Git repository somewhere and you run a program on the target machine after it's booted to converge it into the desired state. CoreOS is missing a lot of the infrastructure assumed to be present by tools like Chef and Puppet, so they are not supported. It is possible to run Ansible, a push-based configuration management tool, on a CoreOS host, but I'm not a fan of Ansible for reasons that are beyond the scope of this post, and plus, using a complex configuration management tool is sort of against the spirit of CoreOS, where almost everything should happen in containers.

For very minimal on-boot configuration, CoreOS supports a subset of cloud-config, the YAML-based configuration format from the cloud-init tool. CoreOS instances can be provided a cloud-config file and will perform certain actions on boot. cloud-config can be used to load iptables with a list of rules for a more secure network.

I'll provide the relevant portion of the cloud-config I use on DigitalOcean, then explain the relevant pieces:

#cloud-config

coreos:
  units:
    - name: iptables-restore.service
      enable: true
write_files:
  - path: /var/lib/iptables/rules-save
    permissions: 0644
    owner: root:root
    content: |
      *filter
      :INPUT DROP [0:0]
      :FORWARD DROP [0:0]
      :OUTPUT ACCEPT [0:0]
      -A INPUT -i lo -j ACCEPT
      -A INPUT -i eth1 -j ACCEPT
      -A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
      -A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
      -A INPUT -p tcp -m tcp --dport 80 -j ACCEPT
      -A INPUT -p tcp -m tcp --dport 443 -j ACCEPT
      -A INPUT -p icmp -m icmp --icmp-type 0 -j ACCEPT
      -A INPUT -p icmp -m icmp --icmp-type 3 -j ACCEPT
      -A INPUT -p icmp -m icmp --icmp-type 11 -j ACCEPT
      COMMIT

Every cloud-config file must start with #cloud-config exactly. I learned the hard way that this is not just a comment – it actually tells CoreOS to treat the file as a cloud-config. Otherwise it will assume it's a shell script and just run it as such.

The following lines are YAML syntax. The coreos section is a CoreOS-specific extension to cloud-init's cloud-config format. The units section within it will automatically perform the specified action(s) on the specified systemd units. systemd is the init system used by CoreOS, and many of the OS's core operations are tied closely to it. "Units" are essentially processes that are managed by systemd and represented on disk by unit files that define how the unit should behave.

The systemd unit iptables-restore.service ships with CoreOS but is not enabled by default. enable: true turns it on and will cause it to run on boot after every reboot. Here are the important contents of that unit file:

[Service]
Type=oneshot
ExecStart=/sbin/iptables-restore /var/lib/iptables/rules-save

The unit file defines a "oneshot" job, meaning it simply executes and exits and is not intended to stay running permanently. The command run is the iptables-restore utility, which accepts an iptables script file defining rules to be loaded into iptables. Whenever the system reboots, all iptables rules are flushed and must be reloaded from this script. That's exactly what iptables-restore does. The script it loads is expected to live at /var/lib/iptables/rules-save, which brings us to the second section of the cloud-config file.

cloud-config's write_files section will, unsurprisingly, write files with the given content to the file system. The content field is the most important part here. This defines the iptables rules to load. The details of this configuration can be fully explained by reading the iptables documentation, but to summarize, these rules:

  • Allow all input to localhost
  • Allow all input on the private network interface
  • Allow all connections that are currently established, which prevents existing SSH sessions from being suddenly terminated
  • Allow incoming TCP traffic on ports 22 (SSH), 80 (HTTP), and 443 (HTTPS)
  • Allow incoming ICMP traffic for echo replies, unreachable destination messages, and time exceeded messages
  • Drop all other incoming traffic
  • Drop all traffic attempting to forward through the network
  • Allow all outbound traffic

The three TCP ports allowed are pretty standard, but those are the rules you'd be most likely to change or augment depending on what services you'll be running on your CoreOS machine.

After CoreOS boots, SSH into it, and verify that iptables was configured properly by running sudo iptables -S (to see it listed in the same format as above) or with sudo iptables -nvL (for the more standard list format).

That's pretty much it! As you can see, there are a lot of related technologies to learn when venturing into CoreOS. Several of these were new for me, so there was a lot of learning involved in getting this working. For reference, the entire cloud-config I use for CoreOS on DigitalOcean can be found in this Gist.

How Lita.io uses the RubyGems and rubygems.org APIs

Today I released a brand new website for Lita at lita.io. While the site consists primarily of static pages for documentation, it also has a cool feature that takes advantage of a few relatively unknown things in the Ruby ecosystem. That feature is the plugins page, an automatically updating list of all Lita plugins that have been published to RubyGems.

Previously, the only directory of Lita plugins was Lita's wiki on GitHub. When someone released a plugin, they'd have to edit the list manually. This was not ideal. It was easy to forget, and required that people knew that the wiki had such a list in the first place. To make an automatically updating list, I had to think about how I could detect Lita plugins out there on the Internet.

I spent some time digging through the rubygems.org source code to see how I might get the information I wanted out of the API. After experimenting with a few things, I discovered an undocumented API: reverse dependencies. You can hit the endpoint /api/v1/gems/GEM_NAME/reverse_dependencies.json and you will get back a list of all gems that depend on the given gem. This was great! Now I had a list of names of all the gems that depend on Lita. It's pretty safe to assume those are all Lita plugins.

This API only returns the names of the gems, however. I also wanted to display a description and the authors' names. This data could be gathered from an additional API request, but there was another piece of information I wanted that couldn't be extracted from the API.

Lita has two types of plugins: adapters and handlers. Adapters allow Lita to connect to a particular chat service, and handlers add new functionality to the robot at runtime; they're the equivalent of Hubot's scripts. I wanted the plugins page to list the plugin type along with the name, author, and link to its page on rubygems.org. To do this, I used another lesser-known feature: RubyGems metadata.

In RubyGems 2.0 or greater, a gem specification can include arbitrary metadata. The metadata consists of a hash assigned to the metadata attribute of a Gem::Specification. The keys and values must all be strings. In Lita 2.7.1, I updated the templates used to generate new Lita plugins so that they automatically included metadata in their gemspecs indicating which type of plugin it is. For example:

Gem::Specification.new do |spec|
  spec.metadata = { "lita_plugin_type" => "handler" }
end

Because Lita requires Ruby 2.0 or greater, which comes with RubyGems 2.0, any Lita plugin can use the metadata attribute. Any plugins created before the generator update in Lita 2.7.1 can still be detected and listed on the plugins page, it just won't list their type.

Now all I had to do was read this information from each plugin gem. Unfortunately, rubygems.org currently has no API that exposes gem metadata, so things got a little tricky. I wrote a script which called gem fetch to download the actual gem files for all the Lita plugins. Once downloaded, running gem spec on the gem file outputs a YAML representation of the gem's specification. In Ruby, loading that YAML with YAML.load returns a Gem::Specification. From there I could simply access the fields I wanted to display, including the type of plugin via spec.metadata["lita_plugin_type"]. This data is then persisted in Postgres. The script runs once a day to get the latest data from RubyGems.

This process could be made much easier and less error-prone if rubygems.org added metadata to the information it exposes over its API. Nevertheless, creating the plugins page for Lita.io was a good challenge and gave me a chance to explore some of the pieces of the RubyGems ecosystem I didn't know existed.

Getting started with Lita

Lita is an extendable chat bot for Ruby programmers that can work with any chat service. If you've used Hubot before, Lita is similar, but written in Ruby instead of JavaScript. It's easy to get started, and you can have your own bot running in minutes.

Lita uses regular RubyGems as plugins. You'll need at least one "adapter" gem to connect to the chat service of your choice. Add as many "handler" gems as you want to add functionality to your bot.

  1. Install the lita gem.

    $ gem install lita
    
  2. Create a new Lita project.

    $ lita new
    

    This will create a new directory called lita with a Gemfile and Lita configuration file.

  3. Edit the Gemfile, uncommenting or adding the plugins you want to use. There should be an adapter gem (such as lita-hipchat or lita-irc) and as many handler gems as you'd like. For example:

    source "https://rubygems.org"
    
    gem "lita"
    gem "lita-hipchat"
    gem "lita-karma"
    gem "lita-google-images"
    
  4. Install all the gems you specified in the Gemfile:

    $ bundle install
    
  5. Install Redis. On OS X, you can use Homebrew with brew install redis.

  6. Test Lita out right in your terminal with the built-in shell adapter.

    $ bundle exec lita
    

    Type "Lita: help" to get a list of commands available to you.

  7. Edit the Lita configuration file to add connection information for the chat service you're using. For example, if you're using the HipChat adapter, it might look something like this:

    Lita.configure do |config|
      config.robot.name = "Lita Bot"
      config.robot.adapter = :hipchat
      config.adapter.jid = "12345_123456@chat.hipchat.com"
      config.adapter.password = "secret"
      config.adapter.rooms = :all
    end
    

    You'll want to consult the documentation for whichever adapter you're using for all the configuration options. If you're going to deploy Lita to Heroku, you'll want to add the Redis To Go add on and set config.redis.url = ENV["REDISTOGO_URL"].

  8. Deploy your Lita project anywhere you like. If you're deploying to Heroku, you can use a Procfile like this:

    web: bundle exec lita
    

    Lita also has built-in support for daemonization if you want to deploy it to your own server.

Be sure to visit the Lita home page for lots more information on usage, configuration, and adding your own behavior to your robot!

Page 3