terraformvault

Using Rich Return Types and Map Expressions in Sentinel 0.17

Sentinel 0.17 provides the ability to return non-boolean data within a policy. See examples of how to use this new functionality to improve compliance reporting capabilities.

Sentinel is a policy as code language and framework that enables fine-grained, logic-based policy decisions. Sentinel is an enterprise-only feature and is embedded in HashiCorp Consul, Nomad, Terraform, and Vault.

Recently we announced the release of Sentinel 0.17, which is a fundamental change to the Sentinel runtime. It includes several new features:

  • Map Expressions: Provides the ability to create rich reporting data sets.
  • Rich Return Types: Adds supports for non-boolean values in rules.
  • Emptiness Comparison: Comparison expressions that provide a natural method of checking for empty collections.
  • Comparison Enhancements: Maps are now comparable for equality.
  • Machine Readable Tracing: Adding JSON output as a CLI option for both sentinel apply and sentinel test.
  • Base64 Import: Enabling Sentinel policies to work with base64-encoded data.

In this post, I will demonstrate how, by using a mixture of Map Expressions and Rich Return Types, you can greatly improve the process of returning policy compliance reporting data.

»Map Expressions

From Sentinel 0.17 onward, you can use a higher-order map expression to return a list based on an input collection.

In map expressions, a list is always returned regardless of the input collection type. Upon evaluation, each element within an input collection is identified according to the map expression body, and the resulting data is added to the map.

// Input Collection
input = [
  { 
    "name":"sentinel",
    "version":"0.17.1",
    "shasums":"sentinel_0.17.1_SHA256SUMS",
    "shasums_signature":"sentinel_0.17.1_SHA256SUMS.sig",
  },
]

// Map Expression
result = map input as _, i {
  {
    "name": i.name,
    "version": i.version,
  }
}

main = rule {
 result // [{"name": "sentinel", "version": "0.17.1"}]
}
// Input Collection
input = [
  { 
    "name":"sentinel",
    "version":"0.17.1",
    "shasums":"sentinel_0.17.1_SHA256SUMS",
    "shasums_signature":"sentinel_0.17.1_SHA256SUMS.sig",
  },
]
 
// Map Expression
result = map input as _, i {
  {
    "name": i.name,
    "version": i.version,
  }
}
 
main = rule {
 result // [{"name": "sentinel", "version": "0.17.1"}]
}

If you would like to view this policy in its entirety, visit the Sentinel Playground.

»Rich Return Types

Rules are a core feature in the Sentinel runtime and form the basis of a policy. Up until now, a rule has been a single boolean expression that returns either true or false to indicate a passing or failing rule.

Boolean expression will short-circuit when evaluated, which is critical in time-sensitive applications like HashiCorp Vault. For integrations like Terraform Cloud, which tend to have larger data sets, short-circuit behavior is less of a concern.

When working with large data sets, understanding why a rule evaluation failed, and identifying the offending resources is far more important and can require multiple evaluations before all policy violations are understood and remediated. Achieving the desired result can lead to the adoption of creative workarounds that tend to circumvent the standard behavior and favor round-about tricks to report meaningful data.

With the release of Sentinel 0.17, rules have been updated so that they can evaluate any expression and hold non-boolean values. By supporting all basic types, Sentinel rules can accept collections (lists and maps), strings, and numeric (floating-point and integer) values. Combined with the new higher order map expression, this allows practitioners to create rich sets of report data without having to resort to workarounds such as print messages and rules that would otherwise serve no real purpose.

»Bringing it All Together

So far we have covered what Map Expressions and Rich Return Types are and why we have introduced them to the Sentinel runtime. Let’s take a look at how we can apply our knowledge to a real-world policy based on the following requirements:

All provisioned server instances should have a type of:

  • t2.small
  • t2.medium
  • t2.large

All server instance configuration that violates the policy requirements should be reported and should include:

  • Clear messaging that explains why the violation has occurred
  • The address of the offending resource
  • The value of the configuration attribute
  • The list of allowed configuration values

»Filtering Violations

In order to fulfill the first requirement, we need to declare a list of allowedServerTypes that we can cross-reference to ensure that all provisioned server instances are compliant.

import "tfplan/v2" as tfplan

// Allowed Server Instance Types
allowedServerTypes = ["t2.small", "t2.medium", "t2.large"]
import "tfplan/v2" as tfplan
 
// Allowed Server Instance Types
allowedServerTypes = ["t2.small", "t2.medium", "t2.large"]

We then use a filter expression to return a subset of all server instances that have a type value that violates the values in the allowedServerTypes list.

// Filter all instances that will be created that has a type value that is not listed in allowedServerTypes
allServerInstanceTypeViolations = filter tfplan.resource_changes as _, rc {
  rc.change.actions is ["create"] and
    rc.type is "fakewebservices_server" and
    rc.change.after.type not in allowedServerTypes
}
// Filter all instances that will be created that has a type value that is not listed in allowedServerTypes
allServerInstanceTypeViolations = filter tfplan.resource_changes as _, rc {
  rc.change.actions is ["create"] and
    rc.type is "fakewebservices_server" and
    rc.change.after.type not in allowedServerTypes
}

We can now take the contents of the allServerInstanceTypeViolations data set and use it to report all compliance violations.

»Violation Reporting

We can use the new map expression to build a dynamic map that contains a message that describes why a violation has occurred, the address of the violating resource as well as the type value and list of allowed_types.

// This Sentinel policy ensures that server instance type configuration does not violate a list of allowed types.
main = rule {
  // Sentinel map expression providing contextual violation data
  map allServerInstanceTypeViolations as _, violation {
    {
      "message":          violation.change.after.name + " has an unsupported instance type",
      "address": violation.address,
      "type":             violation.change.after.type,
      "allowed_types":    allowedServerTypes,
    }
  }
}
// This Sentinel policy ensures that server instance type configuration does not violate a list of allowed types.
main = rule {
  // Sentinel map expression providing contextual violation data
  map allServerInstanceTypeViolations as _, violation {
    {
      "message":          violation.change.after.name + " has an unsupported instance type",
      "address": violation.address,
      "type":             violation.change.after.type,
      "allowed_types":    allowedServerTypes,
    }
  }
}

By encapsulating the expression in main, we can take advantage of Rich Return Types as well as the enhancements that we have made to Sentinel tracing. With the redesign we have moved from a fully verbose stack-like trace, to a human-readable output that is easier to understand.

1 policies evaluated.

## Policy 1: policy.sentinel (hard-mandatory)

Result: false

policy.sentinel:14:1 - Rule "main"
  Description:
    This Sentinel policy ensures that server instance type
    configuration does not violate a list of allowed types.

  Value:
    [
      {
        "message": "BESRV0 has an unsupported instance type"
        "address": "fakewebservices_server.backend[0]"
        "type": "t2.macro"
        "allowed_types": [
          "t2.small"
          "t2.medium"
          "t2.large"
        ]
      }
      {...}
    ]
1 policies evaluated.
 
## Policy 1: policy.sentinel (hard-mandatory)
 
Result: false
 
policy.sentinel:14:1 - Rule "main"
  Description:
    This Sentinel policy ensures that server instance type
    configuration does not violate a list of allowed types.
 
  Value:
    [
      {
        "message": "BESRV0 has an unsupported instance type"
        "address": "fakewebservices_server.backend[0]"
        "type": "t2.macro"
        "allowed_types": [
          "t2.small"
          "t2.medium"
          "t2.large"
        ]
      }
      {...}
    ]

If you would like to view this policy in its entirety, visit the Sentinel Playground.

»Get Started

The latest release of 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 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 play 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.

Sign up for the latest HashiCorp news