
Getting started with cfn-guard
- Stephen Jones
- Aws
- March 13, 2023
Table of Contents
Update
Link to official AWS blog post here
A fair few policy-as-code tools are popping up these days. This post looks at getting started with cfn-guard to parse AWS Config Resource JSON outputs.
Why Guard?
The reason this one has piqued my interest is due to the native AWS Config support for the Guard language which I discuss here.
With this support, there is an opportunity for delivering compliance checks for AWS Resources without having to write, deploy or maintain Lambdas. Any resource that is supported 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
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 likely 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](https://github.com/awslabs/aws-config-rdk/tree/master/rdk/template/example_ci).
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](https://github.com/aws-cloudformation/cloudformation-guard/blob/main/docs/UNIT_TESTING.md) 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.
```bash
# 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/
}
```text
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](https://github.com/awslabs/aws-config-resource-schema/blob/master/config/properties/resource-types/AWS::ACM::Certificate.properties.json).
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 let's 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 that's it, let's 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: FAILname: 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 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 examples. PRs always welcome so go check it out.
sjramblings/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!
For more articles on AWS Config click here!](https://sjramblings.io/tag/config)


