CloudFormation Linting with cfn-nag

Over the last 3 years I’ve done a lot of CloudFormation work and while it’s an easy enough technology to get to grips with the mass of JSON can become a bit of a blur when you’re doing code reviews. It’s always nice to get a second pair of eyes, especially an unflagging, automated set, that has insight in to some of the easily overlooked security issues you can accidentally add to your templates. cfn-nag is a ruby gem that attempts to sift through your code and present guidelines on a number of frequently misused, and omitted, resource properties.

gem install cfn-nag

Once the gem and its dependencies finish installing you can list all the rules it currently validates against.

$ cfn_nag_rules
...
IAM policy should not apply directly to users.  Should be on group
...

I found reading through the rules to be quite a nice context refresher. While there are a few I don’t agree with there are also some I wouldn’t have thought to single out in code review so it’s well worth having a read through the possible anti-patterns. Let’s check our code with cfn-nag.

cfn_nag --input-json-path . # all .json files in the directory
cfn_nag --input-json-path templates/buckets.json # single file check

The default output from these runs looks like:

./templates/buckets.json
------------------------------------------------------------
| WARN
|
| Resources: ["AssetsBucketPolicy"]
|
| It appears that the S3 Bucket Policy allows s3:PutObject without server-side encryption

Failures count: 0
Warnings count: 1

./templates/elb.json
-------------
| WARN
|
| Resources: ["ELB"]
|
| Elastic Load Balancer should have access logging configured

Failures count: 0
Warnings count: 1

If you’d like to reprocess the issues in another part of your tooling / pipelining then the json output formatter might be more helpful.

cfn_nag --input-json-path . --output-format json
    {
        "type": "WARN",
        "message": "Elastic Load Balancer should have access logging configured",
        "logical_resource_ids": [
            "ELB"
        ],
        "violating_code": null
    }

While the provided rules are useful it’s always a good idea to have an understanding of how easy a linting tool makes adding your own checks. In the case of cfn-nag there are two typed of rules. Some use JSON and jq and the others are pure ruby code. Let’s add a simple pure ruby rule to ensure all our security groups have descriptions. At the moment this requires you to drop code directly in to the gems contents but I imagine this will be fixed in the future.

First we’ll create our own rule:

# first we find where the gem installs its custom rules
$ gem contents cfn-nag | grep custom_rules

./.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/cfn-nag-0.0.19/lib/custom_rules

Then we’ll add a new rule to that directory

touch $full_path/lib/custom_rules/security_group_missing_description.rb

Our custom check looks like this -

class SecurityGroupMissingDescription

  def rule_text
    'Security group does not have a description'
  end

  def audit(cfn_model)
    logical_resource_ids = []

    cfn_model.security_groups.each do |security_group|
      unless security_group.group_description
        logical_resource_ids << security_group.logical_resource_id
      end
    end

    if logical_resource_ids.size > 0
      Violation.new(type: Violation::FAILING_VIOLATION,
                    message: rule_text,
                    logical_resource_ids: logical_resource_ids)
    else
      nil
    end
  end
end

The code above was heavily ‘borrowed’ from an existing check and a little bit of object exploration was done using pry. Once we have our new rule we need to plumb it in to the current rule loading code. This is currently a little unwieldy but it’s worth keeping an eye on the docs for when this is fixed. We need to edit two locations in the $full_path/lib/cfn_nag.rb file. Add a require to the top of the file along side the other custom_rules and add our new classes name to the custom_rule_registry at the bottom.

--- ./.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/cfn-nag-0.0.19/lib/cfn_nag.rb  2016-05-01 18:00:14.123226626 +0100
+++ ./.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/cfn-nag-0.0.19/lib/cfn_nag.rb  2016-05-02 09:55:16.842675430 +0100
@@ -1,4 +1,5 @@
 require_relative 'rule'
+require_relative 'custom_rules/security_group_missing_description'
 require_relative 'custom_rules/security_group_missing_egress'
 require_relative 'custom_rules/user_missing_group'
 require_relative 'model/cfn_model'
@@ -175,6 +176,7 @@

   def custom_rule_registry
     [
+      SecurityGroupMissingDescription,
       SecurityGroupMissingEgressRule,
       UserMissingGroupRule,
       UnencryptedS3PutObjectAllowedRule

We can then add a simple CloudFormation security group resource and test our code when it does, and does not include a “description” property.

cat single-sg.json
{
  "Resources": {
    "my_sg": {
      "Type": "AWS::EC2::SecurityGroup",
      "Properties": {
        "GroupDescription": "some_group_desc",
        "SecurityGroupIngress": {
          "CidrIp": "10.1.2.3/32",
          "FromPort": 34,
          "ToPort": 34,
          "IpProtocol": "tcp"
        },
        "VpcId": "vpc-12345678"
      }
    }
  }
}

If you run cfn_nag over that template then you shouldn’t see our new rule mentioned. Now go back and remove the GroupDescription line and run it again.

| FAIL
|
| Resources: ["my_sg"]
|
| Security group does not have a description

It’s quite early days for the project and there are a few gaps in functionality, controlling which rule sets to apply and easier addition of custom rules are the two I’d like to see, but considering how easy it is to install and run cfn-nag over your templates I think it’s well worth giving your code an occasional once over with a second pair of (automated) eyes. I don’t think I’d add it to my build/deploy pipelines until it addresses that missing functionality but as a small automated code review helper I can see it being quite handy.