Subscribe to the Blog

Get articles sent directly to your inbox.

As we all know by now, Infrastructure as Code (IaC) is creating an incredible opportunity for those who care about security: enforcing security and compliance requirements at the code level, thereby avoiding security issues from manifesting themselves in the cloud. This is often referred to as policy-as-code. The concept behind it is that you can define a security, compliance or operational policy in code (rules), and then enforce it on code (which is a structured representation of your infrastructure).

There are many tools to achieve this, each with their own capabilities. Most frameworks that exist today focus on what’s called Static Analysis – analyzing just code, using code. Examples:

  • Open Policy Agent (OPA) – with the Rego language, this framework allows you to write rules to analyze any structured input, with the most common use case being JSON outputs. It’s easy to get basic rules going, but as rules get complicated, the Rego language can get very challenging to read.
  • Checkov and TFsec – the former using Python, the latter uses Go. Both tools built a strong framework for analyzing code (such as Terraform and CloudFormation code), but with no ability to incorporate input from other sources (like the live cloud environment).

After reviewing these frameworks, we came to the conclusion we needed to build a new one. We needed one that was easy to use (like Checkov’s Python code), but also incorporated the live environment. We call this Dynamic Analysis, because it looks at both your code and how it performs in the “wild”. This allows for a far more precise calculation of the policy, 3x less noise, and far more valuable conclusions. In order to achieve this, Cloudrail defines a “context model” – which is an object representation of the cloud environment. For AWS, for example, it has classes for VPC, subnet, RDS, EC2, Lambda Function and many, many other resources (over 100!).

One really cool aspect about this is that the “context model” can be created from any source – Terraform, CloudFormation, live AWS environment, etc. This means that a rule written using Cloudrail “context model” can run on:

  • Any resource defined in code (like Terraform)
  • Any resource existing in the live environment (even if it wasn’t built using IaC!)
  • Any resource that is a mix of the two

The ability to write a rule once and use it everywhere is extremely valuable. Organizations today use a mix of IaC languages. We see different teams within a single company choosing different languages to be used (one team chooses language A, another chooses B). We also see single teams using Terraform or CloudFormation together with Helm, treating the former languages as infrastructure-focused ones, whereas Helm is more workload focused. With so many languages and input sources, writing a rule for each source can be daunting and time consuming. Cloudrail solves that with its context model.

Sounds crazy? Let’s start with a couple of examples, then explain how this works.

NOTE: The code for this blog post is in a GitHub repo.

Example #1: Rule for enforcing the use of specific regions in AWS

Here’s the code of a custom rule I wrote using Cloudrail’s context model, running on Cloudrail’s engine:

class EnsureOnlyApprovedRegionsAreUsed(BaseRule):
    def __init__(self):
        self.approved_list_of_regions: List[str] = [
            "us-east-1",
            "eu-central-1",
            "GLOBAL_REGION" # For IAM
        ]

    def get_id(self) -> str:
        return 'ensure_only_approved_regions_are_used'

    def execute(self, env_context: BaseEnvironmentContext, parameters: Dict[ParameterType, any]) -> List[Issue]:
        issues: List[Issue] = []

        for resource in env_context.get_all_mergeable_resources():
            if isinstance(resource, AwsResource) and resource.region:
                if resource.region not in self.approved_list_of_regions:
                    issues.append(Issue(f'Resource is in region `{resource.region}` which is not approved for usage', resource, resource))
        return issues

This is in Python and the logic is simple: if a resource is being created, or already exists, in a region we don’t allow, Cloudrail alerts about it. If this rule is in “mandate mode” (enforcement mode essentially), we can block deployment of such resources.

The beauty is that this rule is written once and can then be enforced everywhere Cloudrail is running in your organization.

Example #2: Allow only approved thirty party accounts to assume a role

In this case, the rule looks at the IAM statements of IAM roles’ policies:

class EnsureOnlyAssumesThirdPartiesCanAssumeRoles(BaseRule):
    def __init__(self):
        self.approved_list_of_third_parties: List[str] = [
            "645376637575",  # Indeni Cloudrail
            "464622532012",  # DataDog
        ]

    def get_id(self) -> str:
        return 'ensure_only_approved_third_parties_can_assume_roles'

    def execute(self, env_context: BaseEnvironmentContext, parameters: Dict[ParameterType, any]) -> List[Issue]:
        issues: List[Issue] = []

        for role in env_context.roles:
            for statement in role.assume_role_policy.statements:
                if statement.effect == StatementEffect.ALLOW:
                    for principal in statement.principal.principal_values:
                        if arn_utils.is_valid_arn(principal) and arn_utils.get_arn_account_id(principal) not in self.approved_list_of_third_parties:
                            issues.append(Issue(f'The IAM role `{role.get_friendly_name()}` has a trust policy that allows account `{ arn_utils.get_arn_account_id(principal)}` '
                                        f'to assume it but that is not in the list of pre-approved third-party accounts', role, role))
        return issues

Simple, eh?

How does this work?

So, how can this concept of “write rule once, run everywhere” be possible?

On the backend, Cloudrail has the concept of “builders”. These are essentially converters from a variety of data sources, into our context. So, for example, if our context defines the concept of IAM policy as a class, these builders convert data from various sources into the class:

  • There’s a builder that converts Terraform aws_iam_policy resources into that class.
  • There’s also a builder that converts the output of AWS’s GetPolicy API call into the same class.
  • And soon we’ll be releasing a similar builder for CloudFormation AWS::IAM::Policy resource and shortly after one for Pulumi’s aws.iam.Policy’s class.

This way, the heavy lifting of understanding the various languages is taken on by Cloudrail’s engine, keeping the rules themselves clean, simple, and highly reusable.
Want to dig in deeper? Jump over to our sample rules repository.