Enforcing Policy as Code with Cloudify, Terraform, and Open Policy Agent
In my previous article, I provided an example of using Cloudify’s native REST plugin to send a policy evaluation request to an Open Policy Agent (OPA) service. While dispatching requests to an upstream OPA endpoint is a great way to integrate policy enforcement throughout an environment blueprint, Cloudify has also been working toward native integration of OPA throughout our ecosystem.
Cloudify took the first step toward this ecosystem-level integration by adding initial OPA support directly to our Terraform plugin. You can now run OPA policy evaluation against a generated Terraform plan using new capabilities in the Cloudify Terraform plugin. In this article, I’ll show you an example of how this integration works and the new interface operations that Cloudify introduced to enable this functionality.
Scenario
This article discusses a simple cloud use case. A single EC2 instance is deployed into the default VPC, and a security group is attached to the instance. The security group controls the IP addresses that can connect to the EC2 instance via SSH. The infrastructure components are managed using a Terraform module, and the Cloudify blueprint allows the user to specify the permitted SSH IP addresses.
The policy goal is simple: prevent the user from adding an overly permissive IP range, such as “0.0.0.0/0”, to the security group. The policy is expressed using OPA’s Rego language. Cloudify’s official Terraform plugin generates a Terraform plan and evaluates the policy against the plan.
You can follow along with these examples by downloading the code from our community GitHub repository.
The Policy
The policy’s main.rego defines a terraform package with two rules: deny and allow. The deny rule further evaluates the deny_permissive_security_group rule, and the allow rule is a negation of the deny rule:
# main.rego
package terraform
import future.keywords.contains
deny contains msg {
msg := deny_permissive_security_group
}
allow {
not deny
}
The security_groups.rego file defines the deny_permissive_security_group rule within the terraform package. This rule searches across all ingress CIDR blocks in any security groups defined by the Terraform plan. If a security group contains the overly permissive “0.0.0.0/0” rule, then it will raise a message indicating that disallowed IP address ranges were found:
# security_groups.rego
package terraform
import future.keywords.in
import future.keywords.if
import future.keywords.contains
import input as tfplan
deny_permissive_security_group contains msg {
some task in tfplan["planned_values"]["root_module"]["resources"]
task["type"] == "aws_security_group"
task["values"]["ingress"][_]["cidr_blocks"][_] == "0.0.0.0/0"
msg := "You have disallowed IP addresses in the ingress CIDR blocks for your security group"
}
The policy is zipped up as a bundle in the resources/ directory within the blueprint package:
# cd policy/
# zip -r ../resources/policy.zip *
adding: main.rego (deflated 21%)
adding: security_groups.rego (deflated 48%)
The Blueprint
The blueprint enables self-service provisioning of the EC2 instance defined by the Terraform module. Cloudify’s integration with Terraform and OPA ensures that policy is enforced for the requested environment. Inputs to the blueprint allow the user to specify the SSH IP addresses passed to the Terraform module:
inputs:
ssh_ips:
description: List of IP addresses for SSH access
display_label: SSH IP Addresses
The node template definition for the Terraform module is a standard Cloudify Terraform module node template with additional configuration to support OPA. The opa_config property specifies the location of the OPA bundle within the blueprint package. The Cloudify lifecycle interfaces include the addition of the tf.cloudify_tf.tasks.evaluate_opa_policy task as part of the configure operation. The other Terraform-related tasks, such as tf.cloudify_tf.tasks.apply and tf.cloudify_tf.tasks.state_pull are moved “down” in the chain of interface operations that are executed during the built-in install workflow. The tf.cloudify_tf.tasks.evaluate_opa_policy operation accepts the decision to evaluate (“terraform/deny”) as an input. This allows you to evaluate different decisions inside of a policy based on the needs of the blueprint.
ec2-instance:
type: cloudify.nodes.terraform.Module
properties:
opa_config:
policy_bundles:
- name: opa_bundle
location: resources/policy.zip
resource_config:
source:
location: resources/instance.zip
variables:
access_key: { get_secret: aws_access_key_id }
secret_key: { get_secret: aws_secret_access_key }
ssh_ips: { get_input: ssh_ips }
relationships:
- target: terraform
type: cloudify.terraform.relationships.run_on_host
interfaces:
cloudify.interfaces.lifecycle:
configure:
implementation: tf.cloudify_tf.tasks.evaluate_opa_policy
inputs:
decision: terraform/deny
opa_config: { get_property: [SELF, opa_config ] }
start:
implementation: tf.cloudify_tf.tasks.apply
poststart:
implementation: tf.cloudify_tf.tasks.state_pull
Running the Example
This example can be executed from Cloudify’s UI, CLI, or API. First, upload the blueprint package from GitHub to a Cloudify 7.0 manager:
# cfy blueprint upload -b OPA-Example https://github.com/cloudify-community/tf-opa-example/archive/refs/heads/main.zip
Publishing blueprint archive https://github.com/cloudify-community/tf-opa-example/archive/refs/heads/main.zip...
main.zip |############################################################| 100.0%
Blueprint `OPA-Example` upload started.
2023-04-10 14:07:04.941 CFY <None> Starting 'upload_blueprint' workflow execution
2023-04-10 14:07:05.428 LOG <None> INFO: Blueprint archive uploaded. Extracting...
2023-04-10 14:07:05.474 LOG <None> INFO: Blueprint archive extracted. Parsing...
2023-04-10 14:07:06.044 LOG <None> INFO: Blueprint parsed. Updating DB with blueprint plan.
2023-04-10 14:07:06.126 CFY <None> 'upload_blueprint' workflow execution succeeded
Blueprint uploaded. The blueprint's id is OPA-Example
Next, create a deployment with a forbidden value of “0.0.0.0/0” for the SSH IP addresses:

Cloudify initiates the installation process and builds the Execution Task Graph. The installation fails during evaluation of the OPA policy:

The failure’s error message indicates that the policy evaluation failed, and it points the user toward the opa_evaluation_result_json runtime property for further information:

The opa_evaluation_result_json runtime property for the “ec2-instance” node instance provides the exact error message raised by the policy definition:

You can also create a deployment that passes the policy evaluation by specifying IP addresses that do not violate the policy constraints. For example, you can create a deployment with a value of “192.168.28.100/24” for the SSH IP address. The Execution Task Graph runs to completion, and the EC2 instance’s IP address is available as a Deployment Capability:


Wrapping Up
In this article, I introduced you to our initial work toward integrating OPA into Cloudify. Providing self-service environment capabilities for Terraform modules is a very common use case, and we believe that it is an excellent starting point for developing powerful policy-as-code guardrails. OPA has proven itself as a best-in-class tool for policy evaluation, and we are excited to continue working toward expanded OPA support across the Cloudify portfolio.
You can find the full list of Cloudify 7 features in the release notes.