Enforcing Compliance with OPA and Terraform: A Practical Guide


Bicycle

In the world of infrastructure as code (IaC), maintaining compliance and security can be challenging. This is where Open Policy Agent (OPA) combined with Terraform comes into play. By integrating OPA policies with Terraform, you can enforce compliance and security best practices automatically. In this article, we'll explore how to use OPA with Terraform, and we'll provide examples to demonstrate common use cases.

What is Open Policy Agent (OPA)?

Open Policy Agent (OPA) is an open-source policy engine that allows you to define and enforce policies across various systems. It uses a high-level declarative language called Rego to write policies. OPA can be integrated with various tools and services, including Kubernetes, microservices, CI/CD pipelines, and, as we'll see, Terraform.

What is Terraform?

Terraform is an open-source tool by HashiCorp that allows you to define and manage your infrastructure as code. With Terraform, you can describe your infrastructure using a high-level configuration language, and then use that configuration to create, update, and version your infrastructure.

Why Combine OPA with Terraform?

Integrating OPA with Terraform allows you to enforce policies on your infrastructure code before it gets deployed. This ensures that your infrastructure adheres to compliance, security, and operational best practices, reducing the risk of misconfigurations and policy violations.

Writing OPA policies for Terraform

OPA policies are written in Rego, a high-level declarative language. Rego policies are organized into packages, and each package can contain multiple rules. Here's an example of a simple OPA policy that enforces a minimum Terraform version:

 1package terraform.module
 2
 3import input as tfplan
 4import rego.v1
 5
 6minimum_terraform := "1.6.0"
 7
 8deny contains msg if {
 9    v := tfplan.terraform_version
10    semver.compare(v, minimum_terraform) < 0
11
12    msg := sprintf("terraform version must be at least %v - %v is not supported", [minimum_terraform, v])
13}

To verify our OPA policy, we can use the OPA CLI to check the policy file:

1$ opa check policies/minimal_terraform_version.rego

Integrating OPA with Terraform

To integrate OPA with Terraform, we need to generate a Terraform plan and convert it to JSON format. We can then evaluate the OPA policy against the JSON plan. Here's an example using Terraform to create an S3 bucket with bucket ownership controls and an ACL:

 1resource "aws_s3_bucket" "example" {
 2  bucket = "my-tf-example-bucket"
 3}
 4
 5resource "aws_s3_bucket_ownership_controls" "example" {
 6  bucket = aws_s3_bucket.example.id
 7  rule {
 8    object_ownership = "BucketOwnerPreferred"
 9  }
10}
11
12resource "aws_s3_bucket_acl" "example" {
13  depends_on = [aws_s3_bucket_ownership_controls.example]
14
15  bucket = aws_s3_bucket.example.id
16  acl    = "public-read"
17}
  1. Create a Terraform Plan
1$ terraform plan -out tfplan
  1. Convert the Plan to JSON
1$ terraform show -json tfplan > tfplan.json
  1. Run the OPA Evaluation
 1$ opa eval --data policies/minimal_terraform_version.rego --input tfplan.json "data.terraform.module.deny"
 2
 3{
 4  "result": [
 5    {
 6      "expressions": [
 7        {
 8          "value": [],
 9          "text": "data.terraform.module.deny",
10          "location": {
11            "row": 1,
12            "col": 1
13          }
14        }
15      ]
16    }
17  ]
18}

Example: Enforcing S3 Bucket Policies

You can also use OPA policies to verify S3 bucket policies. Here's an example policy that enforces that S3 buckets have specific ACLs:

 1package terraform.module
 2
 3import input as tfplan
 4import rego.v1
 5
 6allowed_s3_acls := ["private", "authenticated-read"]
 7
 8deny contains msg if {
 9    r := tfplan.resource_changes[_]
10    r.type == "aws_s3_bucket_acl"
11    not r.change.after.acl in allowed_s3_acls
12
13    msg := sprintf("%v - s3 buckets must have acls that are in %v", [r.address, allowed_s3_acls])
14}

To evaluate the policy to the same plan we had created before, you just need to run the following command:

 1$ opa eval --data policies/s3_bucket_acl.rego --input tfplan.json "data.terraform.module.deny"
 2{
 3  "result": [
 4    {
 5      "expressions": [
 6        {
 7          "value": [
 8            "aws_s3_bucket_acl.example - s3 buckets must have acls that are in [\"private\", \"authenticated-read\"]"
 9          ],
10          "text": "data.terraform.module.deny",
11          "location": {
12            "row": 1,
13            "col": 1
14          }
15        }
16      ]
17    }
18  ]
19}

Example: Enforcing Security Group Policies

Let's enforce a policy to ensure that no security group allows SSH access from anywhere.

 1package terraform.module
 2
 3import rego.v1
 4import input as tfplan
 5
 6disallowed_cidrs := ["0.0.0.0/0", "::/0"]
 7
 8deny contains msg if {
 9    r := tfplan.resource_changes[_]
10    r.type in ["aws_security_group_rule", "aws_security_group"]
11    valid_port(r.change.after.ingress[_])
12    invalid_cidr(r.change.after.ingress[_])
13
14    msg := sprintf("%v has 0.0.0.0/0 as allowed ingress for SSH / Port 22", [r.address])
15}
16
17valid_port(ingress) if ingress.from_port == 22
18valid_port(ingress) if ingress.to_port == 22
19
20invalid_cidr(ingress) if ingress.cidr_blocks[_] in disallowed_cidrs
21invalid_cidr(ingress) if ingress.ipv6_cidr_blocks[_] in disallowed_cidrs

Here’s a sample Terraform configuration for an AWS security group:

 1resource "aws_security_group" "example" {
 2  name        = "example-sg"
 3  description = "Beispiel Sicherheitsgruppe"
 4
 5  ingress {
 6    from_port   = 22
 7    to_port     = 22
 8    protocol    = "tcp"
 9    cidr_blocks = ["0.0.0.0/0"]
10  }
11}
12
13resource "aws_security_group_rule" "example" {
14  type              = "ingress"
15  from_port         = 22
16  to_port           = 22
17  protocol          = "tcp"
18  ipv6_cidr_blocks = [ "::/0" ]
19  security_group_id = aws_security_group.example.id
20}
  1. Create and Show Plan:
1$ terraform plan -out=tfplan
2$ terraform show -json tfplan > tfplan.json
  1. Scan with OPA:
 1$ opa eval --data policies/security_group_ssh.rego --input tfplan.json "data.terraform.module.deny"
 2
 3{
 4  "result": [
 5    {
 6      "expressions": [
 7        {
 8          "value": [
 9            "aws_security_group.example has 0.0.0.0/0 as allowed ingress for SSH / Port 22"
10          ],
11          "text": "data.terraform.module.deny",
12          "location": {
13            "row": 1,
14            "col": 1
15          }
16        }
17      ]
18    }
19  ]
20}

Running all OPA Policies

In the previous examples, we evaluated individual policies against the Terraform plan. However, you can also bundle multiple policies together and evaluate them all at once. Here's how you can do that:

 1$ terraform plan -out=tfplan
 2$ terraform show -json tfplan > tfplan.json
 3$ opa exec --decision terraform/module/deny --bundle policies/ tfplan.json
 4
 5{
 6  "result": [
 7    {
 8      "path": "tfplan.json",
 9      "result": [
10        "aws_s3_bucket_acl.example - s3 buckets must have acls that are in [\"private\", \"authenticated-read\"]",
11        "aws_security_group.example has 0.0.0.0/0 as allowed ingress for SSH / Port 22"
12      ]
13    }
14  ]
15}

Conclusion

Integrating OPA with Terraform provides a powerful mechanism to enforce compliance, security, and operational best practices in your infrastructure as code. By defining policies in OPA and validating Terraform configurations against these policies, you can ensure that your infrastructure remains secure and compliant.

With the examples provided, you can start creating your own policies to meet your organization's requirements. Happy coding!

Go Back explore our courses

We are here for you

You are interested in our courses or you simply have a question that needs answering? You can contact us at anytime! We will do our best to answer all your questions.

Contact us