» Managing Sentinel Policies with Version Control

Terraform Enterprise (TFE)'s UI for managing Sentinel policies is designed primarily for viewing your organization's policies and policy sets. It also works well for demos and other simple use cases.

For complex day-to-day use, we recommend keeping Sentinel code in version control and using Terraform to automatically deploy your policies. This approach is a better fit with Sentinel's policy-as-code design, and scales better for organizations with multiple owners and administrators.

This page describes an end-to-end process for managing TFE's Sentinel policies with version control and Terraform. Use this outline and the example repository as a starting point for managing your own organization's policies.

» Summary

Managing policies with version control and Terraform requires the following steps:

  • Create a VCS repository for policies.
  • Write Sentinel policies and add them to the policy repo.
  • Write a Terraform configuration for managing policies in TFE, and add it to the policy repo. This configuration must:
    • Configure the tfe provider.
    • Manage individual policies with tfe_sentinel_policy resources.
    • Manage policy sets with tfe_policy_set resources.
  • Create a TFE workspace linked to the policy repo.
  • Write tests for your Sentinel policies.
  • Use CI to run Sentinel tests automatically.

» Repository Structure

Create a single VCS repository for managing your organization's Sentinel policies. We recommend a short and descriptive name like "tfe-policies". Later, you will create a TFE workspace based on this repo.

Once the policy management process is fully implemented, the repo will contain the following:

  • Sentinel policies stored as .sentinel files in the root of the repo.
  • A Terraform configuration stored in the root of the repo. This can be a single main.tf file, or several smaller configuration files.
  • Sentinel tests stored in a test/ directory.
  • Optionally: other information or metadata, which might include a README file, editor configurations, and CI configurations.

» Sentinel Policies

Write some Sentinel policies for TFE, and commit them to your repo as .sentinel files. If you're already enforcing Sentinel policies in TFE, copy them into the new repo.

» Terraform Configuration

Next, write a Terraform configuration to manage your policies and policy sets in TFE using the tfe provider.

» Define Variables for Accessing TFE

The tfe provider needs a highly privileged Terraform Enterprise API token in order to manage policies. The best way to handle this type of secret is with a sensitive variable in a TFE workspace. To enable this, define a variable in the configuration.

variable "tfe_token" {}

If you use a private install of TFE or multiple TFE organizations (or if you might do so in the future), you can set your TFE hostname and TFE organization as variables:

variable "tfe_hostname" {
  description = "The domain where your TFE is hosted."
  default     = "app.terraform.io"
}

variable "tfe_organization" {
  description = "The TFE organization to apply your changes to."
  default     = "example_corp"
}

» Optional: Define a Workspace IDs Variable

The tfe_policy_set resource uses workspace IDs, which can be found on a workspace's settings page. You can use these IDs directly, but the configuration will be more readable if you provide a map of names to IDs and refer to workspaces by name throughout the configuration. Use a Terraform variable for this map, so you can update it in TFE without changing the configuration:

variable "tfe_workspace_ids" {
  description = "Mapping of workspace names to IDs, for easier use in policy sets."
  type        = "map"

  default = {
    "app-prod"                = "ws-LbK9gZEL4beEw9A2"
    "app-dev"                 = "ws-uMM93B6XrmCwh3Bj"
  }
}

To quickly get a list of workspace names and IDs, you can make an API call to the List Workspaces endpoint and pipe the result to a jq command:

$ curl \
  --header "Authorization: Bearer $TOKEN" \
  --header "Content-Type: application/vnd.api+json" \
  https://app.terraform.io/api/v2/organizations/my-organization/workspaces?page%5Bnumber%5D=1&page%5Bsize%5D=100 \
  | jq --raw-output '.data[] | "\"\(.attributes.name)\" = \"\(.id)\""'

» Configure the tfe Provider

Configure the tfe provider (version 0.3 or higher) with your API token and hostname variables.

provider "tfe" {
  hostname = "${var.tfe_hostname}"
  token    = "${var.tfe_token}"
  version  = "~> 0.4"
}

» Create Policy Resources

Create a tfe_sentinel_policy resource for each Sentinel policy in the repo.

  • Set the resource name and the name attribute to the policy's filename, minus the .sentinel extension.
  • Use the file() function to pull the policy contents into the policy attribute.
resource "tfe_sentinel_policy" "aws-block-allow-all-cidr" {
  name         = "aws-block-allow-all-cidr"
  description  = "Avoid nasty firewall mistakes (AWS version)"
  organization = "${var.tfe_organization}"
  policy       = "${file("./aws-block-allow-all-cidr.sentinel")}"
  enforce_mode = "hard-mandatory"
}

» Create Policy Set Resources

Create a tfe_policy_set resource for each policy set you wish to create, and specify which policies are part of the set. Each policy set must also set a value for either global or workspace_external_ids, to specify which workspaces it should be enforced on.

  • To build the policy_ids list, interpolate id attributes from your policy resources, like "${tfe_sentinel_policy.aws-block-allow-all-cidr.id}".
  • To build the workspace_external_ids list, interpolate values from your name-to-ID map variable, like "${var.tfe_workspace_ids["app-prod"]}"
# A global policy set
resource "tfe_policy_set" "global" {
  name         = "global"
  description  = "Policies that should be enforced on ALL infrastructure."
  organization = "${var.tfe_organization}"
  global       = true

  policy_ids = [
    "${tfe_sentinel_policy.aws-restrict-instance-type-default.id}",
  ]
}

# A non-global policy set
resource "tfe_policy_set" "production" {
  name         = "production"
  description  = "Policies that should be enforced on production infrastructure."
  organization = "${var.tfe_organization}"

  policy_ids = [
    "${tfe_sentinel_policy.aws-restrict-instance-type-prod.id}",
  ]

  workspace_external_ids = [
    "${var.tfe_workspace_ids["app-prod"]}",
  ]
}

» Importing Resources

If your TFE organization already has some policies or policy sets, make sure to include them when writing your Terraform configuration.

To bring the old resources under management, you can either delete them and let Terraform re-create them, or import them into the Terraform state.

To import existing resources into your TFE workspace, you must configure the atlas backend and run terraform import on your local workstation. Be sure to import resources after you have created a TFE workspace for managing policies, but before you have performed any runs in that workspace.

For the specific import syntax to use, see the documentation for the tfe_sentinel_policy resource and the tfe_policy_set resource. You can find policy and policy set IDs in the URL bar when viewing them in TFE.

» TFE Workspace

Create a new TFE workspace linked to to your policy management repo.

Before performing any runs, go to the workspace's "Variables" page and set the following Terraform variables (using whichever names you used in the configuration):

Once the variables are configured, you can queue a Terraform run to begin managing policies.

» Policy Tests

It's easier and safer to collaborate on policy as code when your code is well-tested. Take advantage of Sentinel's built-in testing capabilities by adding tests to your policy repo.

See the Sentinel testing documentation to learn how to write and run tests. In brief, you should:

  • Create a test/<NAME> directory for each policy file.
  • Add JSON files (one per test case) to those directories. The object in each file should include the following keys:

    • mock — Mock data that represents the test case. Usually you'll mock data for one or more of the Terraform imports; some policies might also require additional imports. For more details about mocking Terraform imports, see:

    • test — Expected results for the policy's rules in this test case. (If the only expected result is "main": true, you can omit the test key.)

    For each policy, make at least two tests: one that obeys the policy, and one that violates the policy (using "test": {"main": false} so that the failed policy results in a passing test). Add more test cases for more complex policies.

  • Run sentinel test (in the root of the policy repo) to see results for all of your tests.

An example Sentinel test:

{
  "test": {
    "main": false,
    "instance_type_allowed": false
  },
  "mock": {
    "tfplan": {
      "resources": {
        "aws_instance": {
          "always-bad": {
            "0": {
              "applied": {
                "ami": "ami-0afae182eed9d2b46",
                "instance_type": "t3.2xlarge",
                "tags": {
                  "Name": "HelloWorld"
                }
              }
            }
          }
        }
      }
    }
  }
}

» Continuous Integration

Once you have working Sentinel tests, use your preferred continuous integration (CI) system to automatically run those tests on pull requests to your policy repo.