Home OPA Rego + tfsec: Custom security policies for your infrastructure
Post
Cancel
OPA + tfsec logos

OPA Rego + tfsec: Custom security policies for your infrastructure

Recently, tfsec added support for applying Rego policies to your Terraform code. Clear rules can be written against simple data structures, whilst providing the developer with a wealth of information in the event of a failure, such as path, line numbers, and highlighted code snippets.

Example tfsec output Example output of a tfsec rego scan

What is tfsec?

tfsec is a tool designed to run either on a developer’s machine or as part of a build pipeline. It examines Terraform files for misconfigurations and clearly reports them to the user.

Hundreds of rules come prepackaged in tfsec, and generally reflect industry best-practice.

You can install and get started with tfsec by following these instructions

What is OPA/Rego?

Quoting the official OPA Documentation:

The Open Policy Agent (OPA, pronounced “oh-pa”) is an open source, general-purpose policy engine that unifies policy enforcement across the stack. OPA provides a high-level declarative language that lets you specify policy as code and simple APIs to offload policy decision-making from your software. You can use OPA to enforce policies in microservices, Kubernetes, CI/CD pipelines, API gateways, and more.

This sounds quite complicated, but it’s actually much simpler than it sounds. Here’s some simpler definitions:

Rego
…is a language for defining policies, such as every S3 bucket must have encryption enabled.
OPA
…is the engine that knows how to apply these policies to a given input. OPA can be queried to check the results of these policies for certain inputs i.e. does this particular S3 bucket comply with the bucket encryption policy?.

Most organisations need to define some of their own unique security policies, and therefore need a way to enforce them as well as applying industry-standard rules.

For example, an organisation may want to ensure all of it’s AWS EC2 instances are tagged with a Department in order to track ownership of resources, track spend between departments, and improve audit capabilities.

OPA Rego is an excellent solution to this problem, especially when combined with other tools…

You can get started with Rego by reading the docs or looking at this OPA Policy Authoring course

Writing Rego policies for tfsec

You must be using tfsec v1.12.0 or later for the features discussed here.

Below is a very minimal example of a Rego policy that tfsec can run. It checks there are no S3 buckets named “insecure-bucket”. Not very useful in the real world, but it’s helpful to explain how this works.

1
2
3
4
5
6
package custom.aws.s3.no_insecure_buckets

deny {
    bucket := input.aws.s3.buckets[_]
    bucket.name.value == "insecure-bucket"
}

Let’s break this down.

The package (line #1) must always start with the custom namespace in order for tfsec to recognise it. The rest of the package name can be whatever you like, but it’s generally a good idea to break things down by cloud provider, service, environment etc.

The name of the deny rule is important. Rule names must either be deny, or begin with deny_ in order to highlight an issue when tfsec runs.

The input variable contains cloud resources organised by provider (e.g aws), and then service (e.g. s3). You can see what this looks like by running tfsec on your project with the --print-rego-input flag. Combining this with the jq tool is very helpful:

1
2
3
4
5
6
7
8
9
tfsec --print-rego-input | jq '.aws.s3.buckets[0].name'
{
  "endline": 3,
  "explicit": true,
  "filepath": "/home/liamg/rego-playground/terraform/bucket.tf",
  "managed": true,
  "startline": 3,
  "value": "secure-bucket"
}

For more information about the input structure, you can review the entire schema in code form by studying the state.State Go struct defined in the defsec source code. All property names are converted to lower-case for consistency, to make writing policies easier.

You may have noticed that the policy checks bucket.name.value, instead of just bucket.name. This is because the bucket.name property contains more than just the value of the property, it also contains various metadata about where this property value was defined, including the filename and line number of the source Terraform file. You can see an example of this metadata in the jq output above.

Trying it out

Create a new directory and change into it, then create a directory named policies for Rego, and a directory named terraform for Terraform files.

Add the following content to terraform/bucket.tf:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
resource "aws_s3_bucket" "example" {
  bucket   = "secure-bucket"

  website {
    index_document = "index.html"
  }

  logging {
    target_bucket = "my-logging-bucket"
  }

  server_side_encryption_configuration {
    rule {
      apply_server_side_encryption_by_default {
        kms_master_key_id = "this-is-a-real-key-id-honest"
        sse_algorithm     = "aws:kms"
      }
    }
  }

  versioning {
    enabled = true
  }

  tags = {
    Environment = "production"
    Repository  = "core-infra"
  }
}

resource "aws_s3_bucket_public_access_block" "example" {
  bucket = aws_s3_bucket.example.id
  block_public_acls   = true
  block_public_policy = true
  restrict_public_buckets = true
  ignore_public_acls = true
}

The above may look a bit overwhelming, but the details aren’t important for now, just know that it defines an S3 bucket with some core security considerations implemented.

Add the following content to policies/bucket.rego:

1
2
3
4
5
6
package custom.aws.s3.no_insecure_buckets

deny {
    bucket := input.aws.s3.buckets[_]
    bucket.name.value == "insecure-bucket"
}

You should now have the following setup:

1
2
3
4
5
6
tree
.
├── policies
│   └── bucket.rego
└── terraform
    └── bucket.tf

You can ask tfsec to apply your custom Rego policies by using the --rego-policy-dir flag to specify the directory containing your policies. Policies will be loaded recursively in this directory, and so can be organised by nested subdirectories if desired.

1
2
3
tfsec --rego-policy-dir ./policies ./terraform

No problems detected!

If we change our Terraform bucket definition so that the name is insecure-bucket, as specified in the rule, running _tfsec_again should yield a failure:

1
2
3
4
5
6
7
8
9
10
 tfsec --rego-policy-dir ./policies ./terraform

Result #1  Rego policy resulted in DENY
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
  Rego Package custom.aws.s3.no_insecure_buckets
     Rego Rule deny
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────


  10 passed, 1 potential problem(s) detected.

Awesome! This is already quite useful, but you may notice the message above is not particularly useful…

Customising messages

You can add custom messages to tfsec rego rules but returning them from the rule, for example:

1
2
3
4
5
6
7
package custom.aws.s3.no_insecure_buckets

deny[msg] {
    bucket := input.aws.s3.buckets[_]
    bucket.name.value == "insecure-bucket"
    msg := "Bucket name should not be 'insecure-bucket'"
}

The above now gives us a clearer explanation of what is failing:

1
2
3
4
5
6
7
8
9
10
tfsec --rego-policy-dir ./policies ./terraform

Result #1  Bucket name should not be 'insecure-bucket'
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
  Rego Package custom.aws.s3.no_insecure_buckets
     Rego Rule deny
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────


  10 passed, 1 potential problem(s) detected.

This is better, but in a large project it’s going to be very difficult to locate the cause of a failure with the above output…

Adding paths, line numbers, and more…

It is very easy to add line numbers, file paths and highlighted, annotated code to the tfsec output for a rego policy.

You can import the data.lib.result package and call the result.new() function to create a result which contains all of this information. The first argument is the message to display, and the second is the property or object which caused a failure (the bucket name in this case).

1
2
3
4
5
6
7
8
9
10
package custom.aws.s3.no_insecure_buckets

import data.lib.result

deny[res] {
    bucket := input.aws.s3.buckets[_]
    bucket.name.value == "insecure-bucket"
    msg := "Bucket name should not be 'insecure-bucket'"
    res := result.new(msg, bucket.name)
}

This gives us much richer output, making it much easier to act on a failure surfaced by this policy:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
tfsec --rego-policy-dir ./policies ./terraform

Result #1  Bucket name should not be 'insecure-bucket'
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 bucket.tf Line 3
───────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
    3  │   bucket   = "insecure-bucket"
───────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
  Rego Package custom.aws.s3.no_insecure_buckets
     Rego Rule deny
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────


  10 passed, 1 potential problem(s) detected.

At this point, you should have all tools you need to start writing policies for use in tfsec!


I hope this provides a good overview of the new Rego capabilities of tfsec. This functionality will also soon be available for both Terraform and CloudFormation using Trivy.

More documentation for this feature will be available over the coming weeks. In the meantime, please get in touch on Slack if you have questions, comments or suggestions. Please raise any issues on GitHub.

Thanks for reading!

This post is licensed under CC BY 4.0 by the author.

Escalating Privileges with Dirty Pipe (CVE-2022-0847)

Malicious Rego: OPA Supply Chain Attacks

Comments powered by Disqus.