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.