Getting started with cfn-guard

Getting started with cfn-guard
Photo by Mehmet Ali Peker / Unsplash

Update

Link to offical AWS blog post here

There are a fair few policy-as-code tools popping up these days. This post looks at getting start with cfn-guard to parse AWS Config Resource JSON outputs.

Why Guard?

The reason this one has peaked my interest is due to the native AWS Config support for the Guard language which I discuss here.

With this support there is an oppourtunity for delivering compliance checks for AWS Resources without having to write, deploy or maintain Lambdas. Any resouce that is support by AWS Config can have its configuration settings validated via Guard.

Cloudformation support is nearly there, but until then we are stuck with aws cli and the UI.

Installation

Installation is pretty easy across all platforms, however I'm on a Mac so brew does the job as per the GitHub README

$ brew install cloudformation-guard

Security people look away but there is also a shell script available.

Note:- Please check what you are executing before pulling the pin on this one!

$ curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/aws-cloudformation/cloudformation-guard/main/install-guard.sh | sh

With the installation complete, lets get into it!

$ cfn-guard --help

cfn-guard 2.0.4

  Guard is a general-purpose tool that provides a simple declarative syntax to define 
  policy-as-code as rules to validate against any structured hierarchical data (like JSON/YAML).
  Rules are composed of clauses expressed using Conjunctive Normal Form
  (fancy way of saying it is a logical AND of OR clauses). Guard has deep
  integration with CloudFormation templates for evaluation but is a general tool
  that equally works for any JSON- and YAML- data.

AWS Config Resource Schema

In order to write our Guard rule we need to know what AWS Config is going present to Guard.

Thankfully we don't have to go and create all these resources and see what JSON is presented to us, AWS has a GitHub Repo available for us to browse each resource-type schema.

Lets take a look at the AWS::EC2::SecurityGroup resource-type.

{
  "accountId": "string",
  "arn": "string",
  "availabilityZone": "string",
  "awsRegion": "string",
  "configuration.associations.networkAclAssociationId": "string",
  "configuration.associations.networkAclId": "string",
  "configuration.associations.subnetId": "string",
  "configuration.entries.cidrBlock": "cidr_block",
  "configuration.entries.egress": "boolean",
  "configuration.entries.icmpTypeCode.code": "integer",
  "configuration.entries.icmpTypeCode.type": "integer",
  "configuration.entries.ipv6CidrBlock": "cidr_block",
  "configuration.entries.portRange.from": "integer",
  "configuration.entries.portRange.to": "integer",
  "configuration.entries.protocol": "string",
  "configuration.entries.ruleAction": "string",
  "configuration.entries.ruleNumber": "integer",
  "configuration.isDefault": "boolean",
  "configuration.networkAclId": "string",
  "configuration.ownerId": "string",
  "configuration.vpcId": "string",
  "configurationItemCaptureTime": "string",
  "configurationItemStatus": "string",
  "configurationStateId": "string",
  "relationships.resourceId": "string",
  "relationships.resourceName": "string",
  "relationships.resourceType": "string",
  "resourceCreationTime": "string",
  "resourceId": "string",
  "resourceName": "string",
  "resourceType": "string",
  "tags.key": "string",
  "tags.tag": "string",
  "tags.value": "string",
  "version": "string"
}

From the schema we can pick out the items we are likley going to want to trigger compliance on.

configuration.entries is going to contain multiple items of interest

  • protocol
  • ruleAction
  • egress
  • cidrBlock
  • ipv6CidrBlock

The following is an extract from an AWS Config event.

{
  "ruleNumber": 100,
  "protocol": "-1",
  "ruleAction": "allow",
  "egress": true,
  "cidrBlock": "0.0.0.0/0"
}
💡
While not kept up to date with all resource examples, the AWS Config RDK Github repo has example events for many resources. Take a look here

Now we have the references lets walk through an example.

ACM Certificate Validation Example

Lets say we want to ensure that all certificates requested within our AWS Accounts are from the following authorized domain names.

  • .net
  • .gov.au

We will use the built-in guard unit test functionality detailed here to build our rule.

As per the docs we will initially create a rule that will match the resource type we are testing for - AWS::ACM::Certificate.

Create a file called certificate.guard with the following content.

# Rule intent
# a) Match AWS::ACM::Certificate resourceType
#
# Expectations:
# 1) FAIL when there are no AWS::ACM::Certificate resources
# 2) FAIL otherwise                                                                          
#

rule check_resource_type_and_status {
    resourceType == /AWS::ACM::Certificate/
}

The syntax is fairly intuitive

  • define a rule - rule
  • give it a name - check_resource_type_and_status
  • match the following value in our AWS Config event - resourceType == /AWS::ACM::Certificate/
💡
Note that resourceType is a name-value pair in our AWS Config Event. See the full event here

Pretty simple and logical. Now lets add our required logic for matching our authorized domain names.

#
# Rule intent
# a) Certificate should have authorised domain name
#
# Expectations:
# 1) SKIP when there are no AWS::ACM::Certificate resources
# 2) PASS when the domain name is on allowed list     
# 3) FAIL otherwise                                                                                   
#

rule check_approved_domain_name when check_resource_type_and_status {

    # 
    # select subjectAlternativeNames entries
    #
    let domain_names = configuration.subjectAlternativeNames[*]

    when %domain_names !empty
    {
        %domain_names {
            this  == /(?i).net/ or
            this  == /(?i).gov.au/
        }
    }
}

Little more complicated, but lets step through it

  • define a rule & give it a name
  • add a where clause that applies the rule on our desired resource type when check_resource_type_and_status
  • select all the values in the following array configuration.subjectAlternativeNames[*] and assign them to domain_names
  • when domain_names isn't empty
  • check if either of the values /(?i).net/ or /(?i).gov.au/ exist

And thats it, lets test it!

Create a file called certificate_tests.yaml with the following content. This file contains our unit tests with the intent we defined above in the certificate.guard rule file.

---
- name: NoMatchedResource  
  input: {}
  expectations:
    rules:
      check_resource_type_and_status: FAIL

- name: ApprovedDomainName
  input: {
    "version": "1.3",
    "accountId": "1236456789012",
    "configurationItemCaptureTime": "datetime.datetime(2020, 4, 22, 13, 13, 17, 718000, tzinfo=tzlocal())",
    "configurationItemStatus": "OK",
    "configurationStateId": "1587582797718",
    "configurationItemMD5Hash": "",
    "arn": "arn:aws:acm:us-east-2:1236456789012:certificate/aaa111a1-1aaa-1aa1-1aaa-aaa1a11aa111",
    "resourceType": "AWS::ACM::Certificate",
    "resourceId": "arn:aws:acm:us-east-2:1236456789012:certificate/aaa111a1-1aaa-1aa1-1aaa-aaa1a11aa111",
    "awsRegion": "us-east-2",
    "availabilityZone": "Regional",
    "resourceCreationTime": "datetime.datetime(2020, 4, 22, 13, 13, 17, 718000, tzinfo=tzlocal())",
    "tags": {},
    "relatedEvents": [],
    "relationships": [],
    "configuration": {
      "certificateArn": "arn:aws:acm:us-east-2:1236456789012:certificate/aaa111a1-1aaa-1aa1-1aaa-aaa1a11aa111",
      "domainName": "www.test.com",
      "subjectAlternativeNames": [
      "www.test.com"
      ],
      "domainValidationOptions": [
      {
        "domainName": "www.test.com",
        "validationDomain": "www.test.com",
        "validationStatus": "FAILED",
        "validationMethod": "DNS"
      }
      ],
      "subject": "CN=www.test.com",
      "issuer": "Amazon",
      "createdAt": "datetime.datetime(2020, 4, 22, 13, 13, 17, 718000, tzinfo=tzlocal())",
      "status": "FAILED",
      "keyAlgorithm": "RSA-2048",
      "signatureAlgorithm": "SHA256WITHRSA",
      "inUseBy": [],
      "failureReason": "INVALID_PUBLIC_DOMAIN",
      "type": "AMAZON_ISSUED",
      "keyUsages": [],
      "extendedKeyUsages": [],
      "renewalEligibility": "INELIGIBLE",
      "options": {
      "certificateTransparencyLoggingPreference": "ENABLED"
      }
    },
    "supplementaryConfiguration": {
      "Tags": []
    },
    "resourceTransitionStatus": "None"
  }
  expectations:
    rules:
      check_resource_type_and_status: PASS
      check_approved_domain_name: PASS

- name: UnApprovedDomainName
  input: {
    "version": "1.3",
    "accountId": "1236456789012",
    "configurationItemCaptureTime": "datetime.datetime(2020, 4, 22, 13, 13, 17, 718000, tzinfo=tzlocal())",
    "configurationItemStatus": "OK",
    "configurationStateId": "1587582797718",
    "configurationItemMD5Hash": "",
    "arn": "arn:aws:acm:us-east-2:1236456789012:certificate/aaa111a1-1aaa-1aa1-1aaa-aaa1a11aa111",
    "resourceType": "AWS::ACM::Certificate",
    "resourceId": "arn:aws:acm:us-east-2:1236456789012:certificate/aaa111a1-1aaa-1aa1-1aaa-aaa1a11aa111",
    "awsRegion": "us-east-2",
    "availabilityZone": "Regional",
    "resourceCreationTime": "datetime.datetime(2020, 4, 22, 13, 13, 17, 718000, tzinfo=tzlocal())",
    "tags": {},
    "relatedEvents": [],
    "relationships": [],
    "configuration": {
      "certificateArn": "arn:aws:acm:us-east-2:1236456789012:certificate/aaa111a1-1aaa-1aa1-1aaa-aaa1a11aa111",
      "domainName": "www.test.com",
      "subjectAlternativeNames": [
      "www.test.com"
      ],
      "domainValidationOptions": [
      {
        "domainName": "www.test.com",
        "validationDomain": "www.test.com",
        "validationStatus": "FAILED",
        "validationMethod": "DNS"
      }
      ],
      "subject": "CN=www.test.com",
      "issuer": "Amazon",
      "createdAt": "datetime.datetime(2020, 4, 22, 13, 13, 17, 718000, tzinfo=tzlocal())",
      "status": "FAILED",
      "keyAlgorithm": "RSA-2048",
      "signatureAlgorithm": "SHA256WITHRSA",
      "inUseBy": [],
      "failureReason": "INVALID_PUBLIC_DOMAIN",
      "type": "AMAZON_ISSUED",
      "keyUsages": [],
      "extendedKeyUsages": [],
      "renewalEligibility": "INELIGIBLE",
      "options": {
      "certificateTransparencyLoggingPreference": "ENABLED"
      }
    },
    "supplementaryConfiguration": {
      "Tags": []
    },
    "resourceTransitionStatus": "None"
  }
  expectations:
    rules:
      check_resource_type_and_status: PASS
      check_approved_domain_name: FAIL

With these two files populated we run our unit test using the following command.

cfn-guard test --rules-file certificate.guard --test-data certificate_tests.yaml

I've wrapped these up in a simple Makefile.

$ make test

cfn-guard test --rules-file `ls *.guard` --test-data `ls *_tests.yaml`

Test Case #1
Name: "NoMatchedResource"
  No Test expectation was set for Rule check_approved_domain_name
  PASS Rules:
    check_resource_type_and_status: Expected = FAIL, Evaluated = FAIL

Test Case #2
Name: "ApprovedDomainName"
  PASS Rules:
    check_resource_type_and_status: Expected = PASS, Evaluated = PASS
    check_approved_domain_name: Expected = PASS, Evaluated = PASS

Test Case #3
Name: "UnApprovedDomainName"
  PASS Rules:
    check_resource_type_and_status: Expected = PASS, Evaluated = PASS
    check_approved_domain_name: Expected = FAIL, Evaluated = FAIL

Wrapping up

As you can see from above cfn-guard is simple yet powerful and has many use cases for defining policy as code.

The examples and more are all available in the following GitHub.com repo and I'll be continuing to populate with more example. PRs always welcome so go check it out.

jonesy1234/cfn-guard-examples

Now we know how to create guard rules, check out this post on configuring them in AWS Security Hub.

Hope this helps someone else.

Cheers!