Jump to content

DevEx improvements in HashiCorp Sentinel


Recommended Posts

Improving the developer experience for writing policies and configuration has been the focus of recent releases of HashiCorp Sentinel. This blog covers the most notable features of these releases including static imports, named functions, defined checks, and per-policy parameter values. If you are new to Sentinel, be sure to read our Sentinel documentation and try out the Sentinel Playground.

Improved configuration syntax

Previously, the terms "import" and "plugin" were often used interchangeably, and "module" had a different meaning. However, the way you accessed these different import types (standard import, plugin, or module) within policy was the same.

Starting with Sentinel 0.19, we have improved the import configuration syntax, which makes it simpler to work with Sentinel. Our introduction of a standardized naming convention ensures consistent import configuration using the HCL syntax already employed by Terraform.

We’ve also added support to the import block to allow overriding the default configuration for the standard imports and plugins that are used within a policy. The improved configuration syntax for Sentinel makes it easier to define different types of imports in a consistent and repeatable way. The new import configuration syntax for plugins and modules looks like this:

import "plugin" "time" {
	config = {
		timezone = "Australia/Brisbane"
	}
}

import "module" "reporter" {
	source = "./reporter.sentinel"
}

Support for static JSON data

Making the policy evaluation process more dynamic has several benefits, such as reducing the number of policies that need to be written and simplifying policy logic for easier contribution to policy libraries by teams. Importing arbitrary structured data into policies is a commonly requested feature from customers looking to enhance their policy-evaluation process.

Starting with version 0.19 of Sentinel, a new static import feature has been added that allows structured data to be imported into policies. This feature currently supports JSON documents, which is a popular data format used in many programming languages. The Sentinel team plans to support more data formats in the future. The new import configuration syntax for static imports looks like this:

import "static" "animals" {
    source = "./animals.json"
    format = "json"
}

Named functions

The introduction of named functions in Sentinel 0.20 has significant impact to the policy authoring experience. Named functions provide a way for the author to define a function that cannot be reassigned or reused. For instance, anonymous functions can be re-assigned, causing policies to fail if an attempted call is made later. This provides some extra safety for policy authors to be certain that critical functions will not change after definition. Here is an example of a named function:

func sum(a, b) {
	a + b
}

Simplified expressions for unknown values

Sentinel allows values to be undefined, however there has historically been no way for policy authors to determine if a value is undefined. Additionally, policy authors must use the else expression to recover from undefined values and provide an alternative value. As part of the Sentinel 0.21 release, there are now two new helpers to determine if a value has been defined. This drastically improves readability of policies, as seen in this example:

foo = undefined

// using the else expression
foo else false is false // false
foo else true is true // true

// the new defined expressions
foo is defined // false
foo is not defined // true

Per-policy parameter values

Parameters help facilitate policy reuse and allow values to be removed from the policy itself. Previously, parameter values could be supplied only once within a configuration, with that value being shared across policies. With the introduction of per-policy parameter values in Sentinel 0.21, parameter values can be supplied once per-policy, with the policy value taking precedence over a globally supplied value. Providing a parameter value to a single policy within configuration is shown here:

policy "restrict-s3" {
	source = "./deny-resource.sentinel"
	params = {
		resource_kind = "aws_s3_bucket"
	}
}

Bringing it all together

The example below brings all of the above features together to showcase what they enable for policy authors. In this example, we are going to create a policy that utilizes exemptions to determine its result. Here are a few considerations:

  • Make the policy reusable to allow for different inputs
  • Use static data to manage exemptions

First, let's create a modular policy for finding violations:

// main.sentinel
import "helpers" 			// our helpers module
import "tfplan/v2" as tfplan	// tfplan import

param id			// id of the policy
param resource_type	// the type of resource
param valid_actions	// allowed actions
param attr			// the attribute to check
param allowed_value	// the allowed value for the attribute

// Filter resources by type
all_resources = filter tfplan.resource_changes as _, rc {
		rc.type is resource_type and
			rc.mode is "managed" and
			rc.change.actions in valid_actions
}
// Filter resources that violate a given condition
violations = filter all_resources as _, r {
		r.change.after[attr] != allowed_value
}

result = rule when not helpers.exempt(id) {
	violations is empty
}

main = rule {
	result
}

This policy is heavily parameterized, giving it greater reusability. It will filter all resources based on resource type and its action via the resource_type and valid_actions parameters. It will then find all violations through filtering the resources and asserting the provided attribute against the allowed value. The result rule is then evaluated based on the value returned from helpers.exempt(id), ensuring that no violations are present.

Now that we have a working policy, let's take a look at the helpers module for finding exemptions in static data:

// helpers.sentinel
import "exemptions"	// static import

func exempt(id) {
	if exemptions[id] is defined {
		return exemptions[id]
	} else {
		return false
	}
}

This simple module has a single named function, exempt, which returns the value of the id within the exemptions static import, or false if it isn't defined. Our exemption static data will look like this.

{
	"ec2_instance_size": false
}

Finally, our configuration will contain the following:

import "module" "helpers" {
	source = "./helpers.sentinel"
}

import "static" "exemptions" {
	source = "./exemptions.json"
	format = "json"
}

policy "ec2_instance_size" {
	source = "./main.sentinel"
	params = {
		id = "ec2_instance_size",
		resource_type = "aws_instance",
		attr = "instance_type",
		allowed_value = "t3.micro",
		valid_actions = [
["no-op"],
["create"],
["update"],
		]
	}
}

If we were to run this policy against valid HashiCorp Terraform plan data with no violations, we should expect an output similar to what’s shown here:

No module changes to install

No policy changes to install

Execution trace. The information below will show the values of all
the rules evaluated. Note that some rules may be missing if
short-circuit logic was taken.

Note that for collection types and long strings, output may be
truncated; re-run "sentinel apply" with the -json flag to see the
full contents of these values.

Pass - ec2_instance_size.sentinel

ec2_instance_size.sentinel:25:1 - Rule "main"
  Value:
	true

ec2_instance_size.sentinel:21:1 - Rule "result"
  Value:
	true

Get started

The latest release of HashiCorp Sentinel includes several new features that build on previous investments in the policy authoring workflow. You can start exploring these new capabilities now by downloading the latest version of the Sentinel CLI from the Sentinel download page.

For more information on the Sentinel language and specification, visit the Sentinel documentation page. If you would like to engage with the community to discuss information related to Sentinel use cases and best practices, visit the HashiCorp Community Forum.

If you would like to experiment with Sentinel in a safe development environment, you can do so by visiting the Sentinel Playground, which provides the ability to evaluate and share example Sentinel policies and mock data. You can also get hands-on with tutorials for Sentinel’s integrations with Terraform Cloud, Vault Enterprise, and Nomad Enterprise.

View the full article

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...