How to write Sentinel policies

Hi,

So i’ve managed to write a simple policy using the example from Github that restricts the region in which the VM can be deployed. But now i’m stuck, i’ve been reading the documentation and it doesn’t make any sense to me. Where are the real-world examples that i can use to understand how to write the policy?

For example: https://docs.hashicorp.com/sentinel/writing/imports/
How can i apply this information to write a policy based on a real-world plan?

Hi @bruun963,

We have a number of policies that have been written based on the CIS benchmarks that you can either use out of the box or you can reference as you start the process of writing your own policies. These are available in the following repo:

If you would like to progress through a learning track instead you can do so via the Sentinel curriculum which is hosted on:

If you provide a bit more detail regarding the types of controls you would like to implement I can see if I can share some content that may be of relevance.

Regards,

Ryan.

Hi @bruun963,

You’ll find many examples of Sentinel policies for Terraform here: https://github.com/hashicorp/terraform-guides/tree/master/governance.

If you’re using Terraform 0.12, then I recommend looking at the third-generation policies which use the new Terraform Sentinel v2 imports such as tfplan/v2: https://www.terraform.io/docs/cloud/sentinel/import/tfplan-v2.html. These policies use a number of useful Sentinel functions that are in Sentinel modules and minimize the need of complex Sentinel language elements such as if/else conditionals and for loops in the actual policies.

If you’re using Terraform 0.11, then I recommend you look at the second-generation policies which use the older Terraform Sentinel imports and do not use Sentinel modules.

As far as restricting region of VMs, it is possible to do it with the tfconfig import that has information about the providers. But if you’re using AWS, it is actually easier to restrict the availability zones instead of the regions using the tfplan import. The following third-generation policy restricts AWS availability zones: https://github.com/hashicorp/terraform-guides/blob/master/governance/third-generation/aws/restrict-availability-zones.sentinel

Roger Berlind

Hi,

Thanks for the fast response.

I’ve been reading the documentation and it still doesn’t make much sense to me. For example this page: https://learn.hashicorp.com/terraform/sentinel/sentinel-policies. It doesn’t tell me how to write the queries or how to structure the “queries”.

But then i went to the Github, copied over a policy (see below). This policy makes sense, then i wanted to check if the username contains a specific value. So I’ve Googled and checked the Hashicorp website for: “filter_attribute_not_in_list”. I was hoping to find a website with descriptions of the functions. But there aren’t any?

I’ve watched your presentation Berlind and that helped me get started with testing policies. I’m hoping that Hashicorp will produce similar video’s that will help people get started.

import “tfplan-functions” as plan

import “strings”

allowed_locations = [“westeurope”, “westus”]

allAzureVMs = plan.find_resources(“azurerm_linux_virtual_machine”)

AzureVMsLocation = plan.filter_attribute_not_in_list(allAzureVMs,

                "location", allowed_locations, true)

main = rule {

length(AzureVMsLocation[“messages”]) is 0

}

Hi @bruun963,

Would you be able to provide the details of the policy that you are attempting to enforce? Once I understand this a bit more I can provide more online resources that can assist and I can help you along the way. You have mentioned username as a property you would like to evaluate but the policy example you have provided is for checking location so I am bit confused at this point :slight_smile:

Hi @hcrhall,

For example i would like to check if the VM is added to a availability set:

planned_values = {
"outputs": {},
"resources": {
	"azurerm_linux_virtual_machine.example": {
		"address":        "azurerm_linux_virtual_machine.example",
		"depends_on":     [],
		"deposed_key":    "",
		"index":          null,
		"mode":           "managed",
		"module_address": "",
		"name":           "example",
		"provider_name":  "azurerm",
		"tainted":        false,
		"type":           "azurerm_linux_virtual_machine",
		"values": {
			"additional_capabilities":         [],
			"admin_password":                  "<>",
			"admin_ssh_key":                   [],
			"admin_username":                  "adminuser",
			"allow_extension_operations":      true,
			"availability_set_id":             null,
			"boot_diagnostics":                [],
			"custom_data":                     null,
			"dedicated_host_id":               null,
			"disable_password_authentication": false,

Thanks, @bruun963 this is helpful.

What I am going to do is show you how to do it without the need for functions. The reason being, functions are an advanced topic and it sounds like you are just starting out so don’t want to abstract away a lot of the code because it can make it hard to follow. Hope that’s okay!

Regarding Availability Sets, these are kind of tricky to determine because they are usually computed resources and therefore cannot be determined at the time of the plan. More information on that here. Happy to unpack this later once we have covered a simpler use case. Anyway onto the example…

First, we need to tell Sentinel which import version we are using, you do so by specifying the following at the top of your policy file:

import "tfplan/v2" as tfplan

Next we create a collection of resources based on type and save the properties for all of the resources in a variable called allVirtualMachines. In this example, we only care about the azurerm_virtual_machine type:

allVirtualMachines = filter tfplan.resource_changes as _, resource_changes {
	resource_changes.type is "azurerm_virtual_machine" and
		resource_changes.mode is "managed" and
		resource_changes.change.actions is ["create"]
}

Then we define our rule. In this example, we will write a rule that will determine if boot diagnostics has been enabled:

boot_diagnostics_enabled_is_true = rule {
	all allVirtualMachines as _, vm {
		all vm.change.after.boot_diagnostics as bd {
			bd.enabled is true
		}
	}
}

Then to close off we evaluate our newly defined rule within main as follows:

main = rule {
	boot_diagnostics_enabled_is_true
}

When we are done our policy should appear as follows:

import "tfplan/v2" as tfplan

allVirtualMachines = filter tfplan.resource_changes as _, resource_changes {
	resource_changes.type is "azurerm_virtual_machine" and
		resource_changes.mode is "managed" and
		resource_changes.change.actions is ["create"]
}

boot_diagnostics_enabled_is_true = rule {
	all allVirtualMachines as _, vm {
		all vm.change.after.boot_diagnostics as bd {
			bd.enabled is true
		}
	}
}

main = rule {
	boot_diagnostics_enabled_is_true
}

So that is the standard flow for writing a policy. Essentially you are:

  1. Filtering out the resource type that you care about through a filter expression which is more often than not based on the resource_changes map which is touched on in more detail here and here.
  2. Then you author a rule that evaluates whether or not a particular property has been set accordingly.

We have detailed documentation for the Sentinel language as well as the specification. I would also encourage you to familiarise yourself with the list of functions that are builtin to Sentinel as well as the standard imports library. In the example above I have used the all expression quite a bit which you can read more about here.

Finally, debugging and logging is achieved using the print function but use these sparingly because things can get unruly pretty quickly. If you want to see a practical example of how to use print you can change the main rule to the following which should print out the contents of allVirtualMachines.

main = rule {
	boot_diagnostics_enabled_is_true and 
        print(allVirtualMachines)
}

I realise that this is a rather large braindump and I am probably oversimplifying things a great deal. If you have any further question or you would like me to dig deeper into certain things, please let me know. I’ve tried to provide the most relevant documentation in the form of links but if there is anything I have missed please call it out. I didn’t mention that I work in the Sentinel product team so if you have any feedback related to documentation etc. please send it my way.

I hope you find this helpful.

Regards,

Ryan

@hcrhall: thank you!

This is how documentation with examples should look like to be honest :slight_smile: Just a simple overview how it works with real world examples, how it can be achieved and how to troubleshoot it with a clear breakdown.

From here i can cross-reference the documentation with links mentioned in your post.

I hope that Hashicorp will release video tutorials in the near future where these things are explained in the same way :wink:

Been reading up on the documentation and created a few simple policies. I’m now trying to write a policy to check if the VM will be created in a specific Resource Group:

	"resource_changes": [
	{
		"address": "azurerm_linux_virtual_machine.example",
		"change": {
			"actions": [
				"create",
			],
			"after": {
				"additional_capabilities":         [],
				"resource_group_name":          "example-resources",

I’ve checked the documentation but, once again, no clue how to apply it it to real-world scenario’s. Also checked a few examples and tried a few things, but no luck.

Should the policy include IF statements and for example a validated = true or false?

edit: found a Sentinel workshop from Roger Berlind that contains some good examples. I wonder why it isn’t posted on the main website?

Hi @bruun963,

The simplest way to evaluate the resource_group_name property would be as follows:

resource_group_name_is_defined = rule {
	all azurerm_virtual_machines as _, virtual_machines {
		virtual_machines.change.after.resource_group_name is "example-resource-group-name"
	}
}

You’ll notice that I only have a single all expression because the property that I am evaluating is a string type value and not a map or list and therefore there is no need to .

This example can be improved upon by defining an input parameter just below our imports as follows:

import "tfplan/v2" as tfplan

param rg_name default "example-resource-group-name"

NOTE:
In this example if I have specified a default value just in case one is not provided as part of the Sentinel Policy Set configuration in Terraform Cloud which are defined at configuration time.

Now when I define my rule it will appear as follows:

resource_group_name_is_defined = rule {
	all azurerm_virtual_machines as _, virtual_machines {
		virtual_machines.change.after.resource_group_name is rg_name
	}
}

With my policy appearing as follows when all my changes are complete:

import "tfplan/v2" as tfplan

param rg_name default "example-resources"

azurerm_virtual_machines = filter tfplan.resource_changes as _, resource_changes {
	resource_changes.type is "azurerm_virtual_machine" and
		resource_changes.mode is "managed" and
		resource_changes.change.actions is ["create"]
}

boot_diagnostics_enabled_is_true = rule {
	all azurerm_virtual_machines as _, virtual_machines {
		all virtual_machines.change.after.boot_diagnostics as boot_diagnostics {
			boot_diagnostics.enabled is true
		}
	}
}

resource_group_name_is_defined = rule {
	all azurerm_virtual_machines as _, virtual_machines {
		virtual_machines.change.after.resource_group_name is rg_name
	}
}

main = rule {
	boot_diagnostics_enabled_is_true and
	resource_group_name_is_defined
}

I hope this improves your understanding. If not, let me know :slight_smile: