Using Open Policy Agent (OPA) to Develop Policy as Code for Cloud Infrastructure
Published 02/21/2020
By Becki Lee, Senior Technical writer at Fugue, Inc
Originally published as: Interactively Debugging the Rego Policy Language with Frego
Policy as code is an effective way to uniformly define, maintain, and enforce security and compliance standards in the cloud. Treating policy like code means taking a programmatic approach to applying a set of rules or best practices to an organization's cloud resources. Similar to its sister infrastructure as code, policy as code introduces programming practices such as version control and modular design and applies them to governance of cloud resources. This has the benefit of promoting consistency and enabling automation in policy, which in turn reduces time and money spent manually remediating compliance violations.
Open Policy Agent (OPA) offers a powerful way to implement this strategy. OPA is an open source policy engine that allows you to write and validate policy as code. OPA can be integrated into software services to decouple software from policy, avoiding the pitfalls of hard-coding policy into software -- handling system-wide policy changes, for example, can otherwise be a manual process prone to error. With policy separated from code, stakeholders can more easily maintain it, and automated enforcement becomes much more viable.
With OPA, policy as code validations are written in Rego, a declarative query language. These validations evaluate data such as infrastructure as code in the context of your organization's security and compliance policies. For example, this means you could write a Rego policy to check pre-deployment whether resources would violate industry compliance standards -- say, if a Terraform configuration declares an unencrypted Amazon Web Services EBS volume and violates a control in the Center for Internet Security's AWS Foundations Benchmark. (Fugue's open source tool Regula checks for this very scenario!)
To enhance the process of writing and debugging Rego policies, Fugue recently open-sourced Fregot, the Fugue Rego Toolkit. You can think of Fregot as a lightweight alternative to OPA's built-in interpreter, designed to validate cloud infrastructure from providers like AWS and Azure. Fregot's REPL allows you to interactively debug Rego code with easy-to-understand error messages, and you can evaluate expressions and test policies.
In this blog post, we'll demonstrate how to use Fregot to debug a Rego policy that checks whether AWS EC2 instances in a Terraform plan use AMIs from an approved list. This is an abbreviated version of the full walkthrough in Fregot's GitHub repo.
Prerequisites
1. Clone the repo:
git clone https://github.com/fugue/Fregot.git
2. Move to the demo directory:
cd Fregot/examples/demo
Optional Steps
If you'd like to generate the Terraform plan JSON yourself:
- Install Terraform v0.12 or later
- Optional: Install jq
Steps
Generate Terraform Plan as JSON
Let's say your organization requires Amazon Web Services EC2 instances to only use hardened Linux Amazon Machine Images (AMIs) that are on a whitelist. Your boss wants to prevent any Terraform with a non-blessed AMI ID from being deployed, so you've installed Fregot and have written a Rego policy to validate the Terraform plan before it is applied. You'll be working with these files:
demo.rego is a Rego policy that checks AWS AMI IDs in a Terraform plan against a whitelist. The policy contains an error that we'll debug in this tutorial.
demo.tf is a Terraform file that will deploy two EC2 instances. If you take a look, you'll see that ami-0b69ea66ff7391e80 is listed in approved_amis in the policy demo.rego, and ami-atotallyfakeamiid is (unsurprisingly) not.
repl_demo_input.json is the Terraform plan formatted as JSON so Fregot can evaluate it. We've done this for you, but if you've installed Terraform v0.12 or later and you'd like to generate the output yourself, you can do so with the following commands:
1. Initialize Terraform directory:
terraform init
terraform plan -out=tfplan
3. Generate JSON representation of plan and pretty-print it with jq:
terraform show -json tfplan | jq . > repl_demo_input.json
Evaluate Terraform Plan with Fregot
Let's start by validating the Terraform plan JSON against the Rego policy. We'll use Fregot eval to specify the input file (repl_demo_input.json), the function we want to evaluate (data.Fregot.examples.demo.deny), and the Rego file it is in (demo.rego):
Fregot eval --input \
repl_demo_input.json \
'data.Fregot.examples.demo.deny' demo.rego
But wait, what's this...
Oh No, an Error!
Uh oh! There's an error in the Rego file. Evaluating deny produces this message:
Fregot (eval error):
"demo.rego" (line 10, column 11):
index type error:
10| ami = input.resource_changes.change.after.ami
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
evalRefArg: cannot index array with a string
Stack trace:
rule Fregot.examples.demo.amis at demo.rego:21:11
rule Fregot.examples.demo.deny at cli:1:1
Well, it's a good thing we just installed Fregot! Let's use the REPL to interactively debug the code.
Launch REPL
We'll start by launching the REPL:
Fregot repl demo.rego --watch
The --watch flag tells Fregot to automatically reload the loaded files (including input) when it detects a change.
(Handy, right? When used in conjunction with the :watch command, Fregot monitors an expression and prints an updated evaluation whenever the policy and/or input files change! See the full walkthrough on GitHub for details.)
Load Policy and Input
First, load the policy:
:load demo.rego
Next, set the input:
:input repl_demo_input.json
We're going to take a closer look at data.Fregot.examples.demo.deny. Let's set a breakpoint so we can investigate.
Set and Activate Breakpoint
To set the breakpoint at deny, you can use the :break command with the rule name. Since the policy has already been loaded in the REPL, rather than using the full name data.Fregot.examples.demo.deny, you can simplify it like so:
:break deny
(We set the breakpoint with the rule name here, but we could also have used the line number: :break demo.rego:20)
Now, evaluate the rule to activate the breakpoint:
deny
You'll see this output:
21| ami = amis[ami]
^^^^^^^^^^^^^^^
Great! We've entered debugging mode and Fregot is showing us the first line of the rule body. Notice how the prompt shows the word debug now:
Fregot.examples.demo(debug)%
Step Forward
Nothing seems out of order yet, so step forward into the next query with the :step command:
:step
You'll see this output:
10| ami = input.resource_changes.change.after.ami
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
So far, so good. Step forward again:
:step
Look, there's the error message we saw earlier!
(debug) error
Fregot (eval error):
"demo.rego" (line 10, column 11):
index type error:
10| ami = input.resource_changes.change.after.ami
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
evalRefArg: cannot index array with a string
Stack trace:
rule Fregot.examples.demo.amis at demo.rego:21:11
rule Fregot.examples.demo.deny at deny:1:1
Error Mode
Fregot automatically puts you into error mode, indicated by the REPL prompt:
Fregot.examples.demo(error)%
Let's look at the error message closely. Something is wrong with the input, since line 10 is where we assign the AMI ID in the input to the variable ami:
10| ami = input.resource_changes.change.after.ami
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Consider the error reason:
evalRefArg: cannot index array with a string
In this case, that means the policy is referencing an array incorrectly. There's something wrong with this syntax:
input.resource_changes.change.after.ami
We're dealing with nested documents here, so let's start with the entire input document and narrow down level by level until we get just the part we want, ami, which is the AMI ID for each AMI in the input.
Evaluate Expressions
We can see the entire input document by evaluating input:
input
We get the expected result, which is the information in repl_demo_input.json. No problems yet, so evaluate the next nested level:
input.resource_changes
There's a lot less JSON now. Everything seems OK so far -- no errors. Narrow it down again:
input.resource_changes.change
Looks like we found the problem! You should see this error message:
Fregot (eval error):
"input.resource_changes.change" (line 1, column 1):
index type error:
1| input.resource_changes.change
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
evalRefArg: cannot index array with a string
Stack trace:
rule Fregot.examples.demo.amis at demo.rego:21:11
rule Fregot.examples.demo.deny at deny:1:1
Diagnose Error
If you look at repl_demo_input.json, you'll see the JSON follows this basic structure:
{
"resource_changes": [
{
"change": {
"after": {
"ami": "ami-0b69ea66ff7391e80"
},
}
},
{
"change": {
"after": {
"ami": "ami-atotallyfakeamiid"
}
}
}
]
}
As you can see, resource_changes is an array with two items, and we should account for this when we assign a value to the ami variable. We need to iterate through resource_changes so we can assign each change.after.ami value to the ami variable. We can do this with [_], so let's add that to the previous expression and evaluate it again:
input.resource_changes[_].change
Success! You should see the two items in the array. Let's narrow it all the way down and confirm we get just the two AMI IDs:
input.resource_changes[_].change.after.ami
The output looks great:
= "ami-0b69ea66ff7391e80"
= "ami-atotallyfakeamiid"
Fix Error in Policy
Now that we know how to fix the policy, let's add [_] to line 10 of demo.rego. It should look like this now:
ami = input.resource_changes[_].change.after.ami
Save your changes and go back to Fregot -- you'll see that the updated policy has automatically been reloaded:
Reloaded demo.rego
(Note: If you didn't launch the REPL with --watch, you can manually reload all modified files with :reload.)
You're still in error mode, so quit error mode to return to normal REPL mode:
:quit
Then quit the REPL so we can test out the policy:
:quit
Now we can evaluate the policy as we did before -- this time, it should work properly:
Fregot eval --input \
repl_demo_input.json \
'data.Fregot.examples.demo.deny' demo.rego
And it does!
[true]
deny returns true, which means we've successfully tested the Terraform plan against the Rego policy and determined that the plan fails validation.
Now that you've solved the code error, you may use the same policy to evaluate your own JSON Terraform plans. Output the plan as JSON and evaluate it with Fregot and the demo.rego policy, then execute the same Fregot eval command from earlier, substituting your_input_file_here for your own input:
Fregot eval --input \
your_input_file_here \
'data.Fregot.examples.demo.deny' demo.rego
If the AMIs in the Terraform plan are whitelisted and the plan passes
validation, you'll see output like this because deny returns no results -- concretely meaning that the request will not be denied:
[]
If the AMI IDs in the plan aren't whitelisted, the output looks like this
because deny returns true, as you saw earlier:
[true]
What's Next?
This is an abbreviated version of the full walkthrough in Fregot's GitHub repo. To learn how to use the :watch command to automatically print the updated value of deny whenever the policy or input changes, and to see a demo of the :next command, check out the walkthrough.
And if you want to try your hand at debugging on your own, you'll find an alternative version of this policy at Fregot/examples/ami_id/, where you can introduce an error and debug with Fregot to your heart's content.
Fregot is still in active development, so if you encounter any issues, please file a report. For more information about Fregot, see the README on GitHub.
Read more by Becki Lee
For more blog posts by Becki Lee please go to https://www.fugue.co/blog
Related Articles:
How Cloud-Native Architectures Reshape Security: SOC2 and Secrets Management
Published: 11/22/2024
It’s Time to Split the CISO Role if We Are to Save It
Published: 11/22/2024
Establishing an Always-Ready State with Continuous Controls Monitoring
Published: 11/21/2024
5 Big Cybersecurity Laws You Need to Know About Ahead of 2025
Published: 11/20/2024