AWS Cloud Operations & Migrations Blog

Policy-as-Code for Securing AWS and Third-Party Resource Types

This post was written by Scott Alexander and Kevin Formsma from Mphasis Stelligent.


Every day, more developers are having lightbulb moments as they realize they can design and manage their infrastructure. It’s our responsibility, as practitioners of the DevOps mindset, to build systems that allow developers to move quickly and speed up the feedback loop by adopting practices and tools for detecting issues early in the Software Development Life Cycle (SDLC). This includes validating — in developers’ workstations and in delivery pipelines — infrastructure as code configurations for compliance with company standards and best practices. For example, limiting choices for Amazon Elastic Compute Cloud (Amazon EC2) instance sizes or Availability Zones, blocking public Access Control Lists (ACLs) on Amazon Simple Storage Service (Amazon S3) buckets, as well as compliance with regulations such as SOX, HIPAA, and GDPR.

Key Tools

AWS CloudFormation helps developers model, provision, and manage AWS and third-party resources in a safe, repeatable, and predictable manner. Stelligent introduced cfn_nag in 2016 as a static analysis tool for CloudFormation. cfn_nag today includes over 150 rules to check your templates for common errors and security issues. In October 2020, AWS released AWS CloudFormation Guard (Guard), which had a major overhaul with its second release in May 2021. In this blog post, Introducing AWS CloudFormation Guard 2.0, Matteo describes numerous features of Guard 2.0, including providing developers with the ability to write policy rules for any JSON and YAML formatted data file: for example, configurations for Kubernetes and Terraform JSON configurations in addition to CloudFormation templates.

Both cfn_nag and Guard are open source tools. Developers can choose to utilize both tools and write rules for company policy compliance and security best practices. Developers validate infrastructure described with code for policy compliance against rules in order to prevent the provisioning of insecure or non-compliant resource configurations.

Both tools differ primarily in how developers write and manage rules. Developers write cfn_nag rules in Ruby, save rules in files, and package rules as a Ruby Gem or store rules in an S3 bucket. Developers write Guard rules in a domain-specific language (DSL) instead. They save rules in files and choose a location for storing rules, such as an S3 bucket or code repository. The table below summarizes the differences between the two tools:

Feature cfn_nag AWS CloudFormation Guard 2.0
Rules out of the box 150+ 10 example rules available in the source code repository as of September 2021
Rule definition Ruby code Guard DSL
Rule testing RSpec/Ruby Built-in rule testing feature
Rule validation targets CloudFormation templates (JSON, YAML) Any JSON- and YAML-formatted configuration data, including CloudFormation templates

Developers looking to quickly leverage policy-as-code validation in their delivery pipelines find immediate value with cfn_nag’s built-in ruleset. Choose to leverage these rules in every pipeline for your CloudFormation-based infrastructure to perform a baseline security review of your templates before deployment. Developers needing to build their own policy-as-code rules find Guard DSL easy to learn and implement. Furthermore, developers with prior Ruby experience can chose to leverage cfn_nag for rule definitions.

Let’s go over examples of how to leverage both tools in your workflow, with a focus on third-party extensions. The AWS CloudFormation Public Registry allows developers to create and manage third-party services as resources in your CloudFormation templates. By writing rules for Guard or cfn_nag, developers can automate the validation of third-party service configurations in their deployment pipelines. Conducting static analysis on third-party integrations at this stage increases security by preventing non-compliant configurations releases.

Example: OpsGenie users have company email IDs

Let’s write a Guard rule to validate that users, as created for Atlassian OpsGenie, only have company email IDs specified in their configuration:

Template snippet

Resources:
  UserOne:
    Type: Atlassian::Opsgenie::User
    Properties:
      ApiKey:
        Ref: OpsgenieApiKey
      Endpoint:
        Ref: OpsgenieApiEndpoint
      Username: opsgenie-user2@example.com
      FullName: John Smith
      Role: User

Guard rule

Resources.*[ Type == 'Atlassian::Opsgenie::User'].Properties.Username == /.*@stelligent.com/

Guard rules are based on clauses that provide assertions, such as Username ==/.*@stelligent.com/. Rules can then be extended by utilizing various filters and queries to restrict the resources being applied to clauses. In this example rule, the Username property of any Atlassian::Opsgenie::User resource is required to match the regular expression showing the company domain.

Example: Preventing hardcoded access keys

Evaluating templates for hardcoded access keys is an important use case for any analysis tool. Developers can implement this rule with Guard. Let’s use the New Relic resource provider shown next as an example:

Template snippet

Resources:
  LamdaAlert:
    Type: NewRelic::Alerts::NrqlAlert
    Properties:
      ApiKey: 0123456789ABCDEF0123456789ABCDEF
      PolicyId: 100
      NrqlCondition:
        Name: Alert Condition Test
        RunbookUrl: http://example.com/runbook
        Enabled: true
        ExpectedGroups: 0
        IgnoreOverlap: true
        ValueFunction: single_value
        Terms:
        - Duration: "1"
          Operator: "equal"
          Priority: "critical"
          Threshold: "1"
          TimeFunction: "all"
        Nrql:
          Query: "SELECT count(*) FROM AwsLambdaInvocation WHERE provider.functionName = 'LambdaTestFunction'"
          SinceValue: "1"

Guard rule

rule new_relic_keys {
     NewRelic::Alerts::NrqlAlert {
          Properties {
               ApiKey != /[0-9A-F]{32}/
               PolicyId != /[0-9]+/
          }
     }
}

In this example rule, you created two clauses that work on the NewRelic::Alerts::NrqlAlert resource type. The first clause is for the ApiKey property, and the second is for the PolicyId property. Both clauses leverage regular expressions in order to validate that property values don’t match literal keys or IDs.

Example: Ensure team tags are present

Let’s write another Guard rule to validate that Datadog integration resources always have a team tag specified. The Datadog resource type utilizes a property called HostTags for this purpose. Guard lets you create simple rules to represent this condition:

Template snippet

Resources:
  DatadogAWSIntegrationResource:
    Type: 'Datadog::Integrations::AWS'
    Properties:
      AccountID: <AWS_ACCOUNT_ID>
      RoleName: DatadogAWSIntegrationRole
      HostTags: ["env:staging", "group:devops"]
      AccountSpecificNamespaceRules: {"ec2": true, api_gateway": false}
      DatadogCredentials:
        ApiKey: <DD_API_KEY>
        ApplicationKey: <DD_APP_KEY>

Guard rule

rule team_tags {
     let datadog_integrations = Resources.*[ Type == 'Datadog::Integrations::AWS' ]
     %datadog_integrations.Properties.HostTags {
          some this == /team:*/
     }
}

In this rule, HostTags is a list of items with the following format: HostTags == ["tagNameOne:value","tagNameTwo:value"...]. You use a filter to select every Datadog-related resource type, and then you create a clause to validate that at least some of them match a regular expression for a team tag.

Example: Ensure database communication uses SSL

The Aqua::Enterprise::Server resource type from Aqua lets users configure the resource by providing values as a YAML string. In this example, we show how to use cfn_nag in order to write a rule to parse the provided YAML string and evaluate the configuration. For example, you want to ensure that communication to the database utilizes an SSL connection:

Template snippet

Resources:
  serverExplicitFalse:
    Type: Aqua::Enterprise::Server
    Properties:
      ClusterID: 'Demo'
      Name: 'Demo'
      ValueYaml: |
        db:
          enabled: false
          ssl: false
      TimeOut: 60

cfn_nag rule

require 'cfn-nag/violation'
require 'yaml'
require 'cfn-nag/custom_rules/base'

class AquaServerDbSslRule < BaseRule
  def rule_text
    'Aqua Server should be configured to use SSL encryption on db connections'
  end

  def rule_type
    Violation::FAILING_VIOLATION
  end

  def rule_id
    'F10000'
  end

  def audit_impl(cfn_model)
    aqua_server_resources = cfn_model.resources_by_type('Aqua::Enterprise::Server')

    violating_servers = aqua_server_resources.select do |resource|
      next true if resource.valueYaml.nil? || resource.valueYaml.empty?
      config_values = YAML.load(resource.valueYaml)
      !config_values.dig('db', 'ssl')
    end

    violating_servers.map(&:logical_resource_id)
  end
end

With this cfn_nag rule, you validate that Aqua::Enterprise::Server resources have db.ssl set to true in the valueYaml property content. The rule parses the YAML-formatted document, and then filters the results. You could extend this rule to add additional checks for the configuration properties of the Aqua Security Server.

Learn more about writing Guard rules in Introducing AWS CloudFormation Guard 2.0 blog post. For more information on custom rule development for cfn_nag, check Extending cfn_nag with custom rules.

Integrating with your Pipelines

Once you have developed your rules, it’s time to integrate them into your Continuous Integration/Continuous Delivery (CI/CD) pipelines and to validate your AWS and third-party resources against your rules. For an example pipeline integration, check Integrating AWS CloudFormation Guard into CI/CD pipelines.

Conclusion

This post shows you examples of how to leverage both cfn_nag and AWS CloudFormation Guard for policy compliance of AWS and third-party CloudFormation registry resources. Start creating your rules today! Your developers will appreciate the additional ability to manage their own tools, and you will be freed up to focus on higher-value work.

Unless otherwise noted, the code snippets in this blog post are licensed under SPDX-License-Identifier: MIT-0.

About the authors

Scott Alexander

Scott Alexander brings together the systems that get new code to end users faster and with fewer bugs. His work as a DevOps Automation Engineer has resulted in increased deployments, reduced bugs, and an improved developer experience. He currently works for Mphasis Stelligent, an AWS Premier Partner. He holds the AWS DevOps Professional, SysOps Administrator Associate, and Security Specialty certifications.

Kevin Formsma

Kevin Formsma has 10+ years of experience as an engineer with a focus on DevSecOps and automation. He has extensive experience in building CD pipelines delivering applications to production everyday on AWS. He currently works for Mphasis Stelligent, an AWS Premier Partner. He holds the AWS DevOps Professional and Developer Associate certifications.

Matteo Rinaudo

Matteo Rinaudo is a Senior Developer Advocate for AWS CloudFormation. He is passionate about the DevOps mindset, infrastructure as code and configuration management. In his spare time, Matteo enjoys spending time with his wife, reading and listening to classical music. You can find Matteo on Twitter at @mrinaudo.