Black Hat Preview 2: Software Defined Security with AWS, Ruby, and Chef
I recently wrote a series on automating cloud security configuration management by taking advantage of DevOps principles and properties of the cloud. Today I will build on that to show you how the management plane can make security easier than traditional infrastructure with a little ruby code. This is another example of material covered in our Black Hat cloud security training class. Abstraction enhances management People tend to focus on multitenancy, but the cloud’s most interesting characteristics are abstraction and automation. Separating our infrastructure from the physical boxes and wires it runs on, and adding a management plane, gives us a degree of control that is difficult or impossible to obtain by physically tracing all those wires and walking around to the boxes. Dev and ops guys really get this, but we in security haven’t all been keeping up – not that we are stupid, but we have different priorities. That management plane enables us to do things such as instantly survey our environment and get details on every single server. This is an inherent feature of the cloud, because if you can’t find a server the cloud doesn’t know where it is – which would mean a) it effectively doesn’t exist, and b) you cannot be billed for it. There ain’t no Neo hiding away in AWS or OpenStack. For security this is very useful. It makes it nearly impossible for an unmanaged system to hide in your official cloud (although someone can always hook something in somewhere else). It also enables near-instant control. For example, quarantining a system is a snap. With a few clicks or command lines you can isolate something on the network, lock down management plane access, and lock out logical access. We can do all this on physical servers, but not as quickly or easily. (I know I am skipping over various risks, but we have covered them before and they are fodder for future posts). In today’s example I will show you how 40 lines of commented Ruby (just 23 lines without comments!) can scan your cloud and identify any unmanaged systems. Finding unmanaged cloud servers with AWS, Chef, and Ruby This examples is actually super simple. It is a short Ruby program that uses the Amazon Web Services API to list all running instances. Then it uses the Chef API to get a list of managed clients from your Chef server (or Hosted Chef). Compare the list, find any discrepancies, and profit. This is only a basic proof of concept – I found seen far more complex and interesting management programs using the same principles, but none of them written by security professionals. So consider this a primer. (And keep in mind that I am no longer a programmer, but this only took a day to put together). There are a couple constraints. I designed this for EC2, which limits the number of instances you can run. Nearly the same code would work for VPC, but while I run everything live in memory, there you would probably need a database to run this at scale. This was also built for quick testing, and in a real deployment you would want to enhance the security with SSL and better credential management. For example, you could designate a specific security account with IAM credentials for Amazon Web Services that only allows it to pull instance attributes but not initiate other actions. You could even install this on an instance inside EC2 using IAM roles, as we discussed previously. Lastly, I believe I discovered two different bugs in the Ridley gem, which is why I have to correlate on names instead of IP addresses – which would be more canonical. That cost me a couple hours of frustration. Here is the code. To use it you need a few things: An access key and secret key for AWS with rights to list instances. A Chef server, and a client and private key file with rights to make API calls. The aws-sdk and ridley Ruby gems. Network access to your Chef server. Remember, all this can be adapted for other cloud platforms, depending on their API support. # Securitysquirrel proof of concept by rmogull@securosis.com # This is a simple demonstration that evaluates your EC2 environment and identifies instances not managed with Chef. # It demonstrates rudimentary security automation by gluing AWS and Chef together using APIs. # You must install the aws-sdk and ridley gems. ridley is a Ruby gem for direct Chef API access. require “rubygems” require “aws-sdk” require “ridley” # This is a PoC, so I hard-coded the credentials. Fill in your own, or adjust the program to use a configuration file or environment variables. Don’t forget to specify the region… AWS.config(access_key_id: ‘your-access-key’, secret_access_key: ‘your-secret-key’, region: ‘us-west-2’) # Fill in the ec2 class ec2 = AWS.ec2 #=> AWS::EC2 ec2.client #=> AWS::EC2::Client # Memoize is an AWS function to speed up collecting data by keeping the hash in local cache. This line creates a list of EC2 private DNS names, which we will use to identify nodes in Chef. instancelist = AWS.memoize { ec2.instances.map(&:private_dns_name) } # Start a ridley connection to our Chef server. You will need to fill in your own credentials or pull them from a configuration file or environment variables. ridley = Ridley.new( server_url: “http://your.chef.server”, client_name: “your-client-name”, client_key: “./client.pem”, ssl: { verify: false } ) # Ridley has a bug, so we need to work on the node name, which in our case is the same as the EC2 private DNS name. For some reason node.all doesn’t pull IP addresses (it should) which we would prefer to use. nodes = ridley.node.all nodenames = nodes.map { |node| node.name } # For every EC2 instance, see if there is a corresponding Chef node. puts “” puts “” puts “Instance => managed?” puts “” instancelist.each do |thisinstance| managed = nodenames.include?(thisinstance) puts ” #{thisinstance} #{managed} ” end Where to go next If you run the code above you should see output like this: Instance => managed? ip-172-xx-37-xxx.us-west-2.compute.internal true ip-172-xx-37-xx.us-west-2.compute.internal true ip-172-xx-35-xxx.us-west-2.compute.internal true ip-172-3xx1-40-xxx.us-west-2.compute.internal false That