An introduction to Policy as Code (Sentinel)

Sentinel is HashiCorp’s framework for implementation of Policy as Code (PaC). It integrates with Infrastructure as Code (IaC), and allows teams/organizations to be proactive from a compliance/risk standpoint. Sentinel allows for granular, logic-based policy decisions that reads information from external sources to derive a decision. In plain English, based on logic written (policies), Sentinel can act as a decision maker based on information provided. This is pretty handy when you want to prevent users from executing specific actions, or ensure that certain steps/actions are conducted. Example, an employee attempting to deploy a bad practice network rule that allows everyone in the internet inbound access! It’s important to call out that Sentinel is a dynamic programming language, with types and the ability to work with rule constructs based on boolean logic.

This article was originally published on Medium. Link to the Medium article can be found here.

First things first, implementation of Sentinel is only available to HashiCorp enterprise customers, hence a reason for why the only documentation available is from HashiCorp. It can be used with the following HashiCorp products; Terraform, Vault, Consul, and Nomad. However, non-enterprise users can still get their hands on Sentinel by leveraging the Sentinel Simulator.

To better understand Sentinel, let’s observe a traditional workflow, let’s use Terraform as the example workflow. In a traditional Terraform workflow the operator/user would draft up a configuration template that specifies which infrastructure resources to be deployed. The next step would be to issue the command terraform plan to ensure the desired resources and infrastructure changes aligns with our desired intention. If everything checks out in the output provided by terraform plan then the final step is to execute terraform apply. The terraform apply command is what would actually deploy our infrastructure. This example workflow applies to both Terraform Enterprise and Terraform Cloud (free tier). To learn more about the difference between the two checkout this link.

Now, let’s take the same workflow and add Sentinel to the mix. Sentinel comes into play after terraform plan and before terraform apply (see image below).

Assume we have a Sentinel policy (more about policies later) that prevents users to deploy infrastructure resources to a public cloud environment without at least one tag. The terraform plan output would be evaluated against that Sentinel policy. If the resources contains at a minimum a tag, as defined in the policy, then the user is allowed to execute terraform apply . Otherwise the plan is rejected and the user is forced to make the specified changes so that the plan passes the policy check. In simple terms, Sentinel prevents users from conducting actions deemed “unapproved” by the policy authors. Traditionally, policy authors are platform administrators and/or information security.

Again, Sentinel is only available for Terraform Enterprise and Terraform Cloud Paid Tier!

Enforcement Levels

Enforcement levels come in three different flavors; Advisory, Soft Mandatory, and Mandatory. The various enforcement levels allow administrators/policy authors to decouple policy behavior from the policy logic. This means that we can set a policy to act as a warning or notification, in this scenario failing still allows for a run to be applied.

A soft mandatory requires the policy to pass, otherwise an administrator/elevated user is required to manually override the failed policy evaluation. If an override is provided then the apply is allowed to execute.

The strictest level, hard mandatory, requires a policy to pass, no exception. The only way around this policy is to remove it from the targeted workspace(s) or global scope.

Policies can be applied to selected workspaces and/or they can be applied globally to all workspaces. Note: Vault Enterprise has a different behavior

Sentinel Policies

So what is a Sentinel policy and what does it look like? Ah, glad you asked, and great question my friend! Let’s take a peek at a conceptual example (not real).

1
2
3
4
5
6
7
8
import "time"
import "temperature"
# This is the first rule.
time_rule = rule { time is after 6pm and before 9pm }
# This is the second rule.
temperature_check = rule { temperature is >= 71 degrees }
# the main rule that combines both of them.
main = rule { time_rule and temperature_check}

We have two rules and each rule evaluates to either true or false . Although the example is not realistic, the goal here is to explain the concept. In order for this policy to pass the following things must be true.

Assume that the values for time and temperature are read in by Sentinel and available.

  1. It is later than 6pm and before 9pm.
  2. The inside temperature has to be greater than or equal to 71 degrees. If both of these are true, then the policy will pass. If either of them were to be false then the policy would fail and the user would not be permitted to proceed.

Okay, let’s get more realistic, observe the example below.

1
2
3
4
day = "wednesday"  # This is a variable
two_greater  = rule { 2 > 1 } # This is a rule
is_wednesday = rule { day is "wednesday" } # This is also a rule
main = rule { two_greater and is_wednesday } # This is the main rule

Sentinel is all about rules! Rules can either be true or false , at the end of day it all comes down to a boolean value. In the example above we have two rules; two_greater , and is_wednesday .

The two_greater rule evaluates if the number 2 is greater than 1. Because this is a true statement we can safely assume that this statement will evaluate to true . The next rule is_wednesday , checks to see if the variable day , which is assigned the value wednesday , is equal to the string wednesday . This is also true so we can again assume the statement will evaluate to true .

Every Sentinel policy requires a main rule, and depending on what the main rule evaluates to will determine the outcome of the policy when it is evaluated against a workflow output, example terraform plan.

So if we look at our main rule, main = rule { two_greater and is_wednesday } , we can see that it checks to see if the two rules defined two_greater and is_wednesday are both true . Because both rules result intrue and we used the boolean comparison operator and , thus the main rule will result in a true .

Essentially, we are breaking out our policy logic into two separate rules and comparing those two rules inside the main rule statement. We don’t have to break out our logic in this manner, we could had accomplished the same logic in one rule. However, it is considered best practice to breakout logic into their own rules versus writing a monolithic rule - also it’s easier on the eyes. The later is more evident with complex policies.

1
2
day = "wednesday"
main = rule { 2 > 1 and day is "wednesday" }  

Real World Policy

Remember that example policy earlier of requiring resources to have at least a tag? Well, let’s take a peek at what that policy could look like. The following is a Sentinel policy for AWS that requires all EC2 instances to have a least a tag.

1
2
3
4
5
6
7
8
9
import "tfplan"

main = rule {
  all tfplan.resources.aws_instance as _, instances {
    all instances as _, r {
      (length(r.applied.tags) else 0) > 0
    }
  }
}

Let’s break the policy down. The first line is what allows us to read in the output of a terraform plan The import statement specifies what external data to read in or libraries to include at runtime. Then we move into the main rule, and a couple of things are happening here:

  1. We want to read all (the list) of aws_instances from the tfplan with the following key labeled as _ , and the value represented as instances.
  2. All instances of the list variable instances, as the key labeled _, and the value as r.
  3. We use the length function, calling the element’s attribute applied.tags and make sure it is greater than zero.

This makes sense if we look at a JSON object very similar to the one that’s actually imported (tfplan file) and what the Sentinel logic is applied on. I added comments to help understand the logic above.

If you are familiar with terraform statefiles, it’s pretty much the same pattern.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import "strings"
_modules = {
 "root": {
  "data": {},
  "path": [],
  "resources": {
   "aws_instance": {  // This is where the first all starts
    "demo_instance": {   // This where the second all starts
     0: {                // This is where the r object begins
      "applied": {
       "ami": "ami-123456789",  
       "associate_public_ip_address":  true,   
       "availability_zone":   "us-east-1a",
        ......
        "tags": {
        "Name": "aws-sentinel",
        "env":  "dev",
       },
........

TIP: If you are a Terraform Enterprise customer use the “Download Sentinel Mocks” from a run to get the actual mock data! This is super useful when writing test cases for Sentinel policies.

So what’s next? Well in order to validate Sentinel policies are written correctly and for testing purposes we can leverage the Sentinel CLI tool. We won’t go into policy testing in this article, we’ll table that for another time. But in preparation for that next article let’s get the Sentinel Simulator installed.

Installing the Sentinel Simulator

Getting the simulator up and running is pretty straight forward. Download the correct binary for your operating system and unzip the folder. Inside the zipped folder you will find a single binary. As all other HashiCorp products, a single binary is all that is needed to get going, simply place the binary in your user/system path and you will be able to start using the Sentinel simulator

If you are struggling with this step then simply place the binary in your working folder and continue from there. Lookup YouTube videos on how to change the PATH environment variable on your Operating System (Linux/Windows).

Verify the Sentinel Simulator is setup correctly by issuing sentinel version , if done correctly you will receive output indicating the version of the simulator.

1
2
3
sentinel version
C:\Users\Karl>sentinel version
Sentinel Simulator v0.12.0

Conclusion

We are barely scratching the surface of what Sentinel is and how it can be used. Hopefully this article helped fill some of the knowledge gaps pertaining to Policy as Code through Sentinel. There is still much to learn, such as language syntax, writing functions, complex policies, testing, integrating Sentinel with a VCS, uploading Sentinel policies and sets through Terraform, and so on. Stay tuned for more articles where we continue to dive into the world of Sentinel.

0%