» 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"
}

» 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.6"
}

» Optional: Use a Data Source to Get Workspace IDs

The tfe_policy_set resource uses workspace IDs, which can be found on a workspace's settings page. There are several ways to work with these IDs in your configuration, all of which have advantages and disadvantages:

Method Pros Cons
Use literal ID strings. Highly secure, since workspace IDs never change. Policy set configs are opaque. Accidental misconfigurations are difficult to spot.
Use a data source to look up IDs by name. Convenient and easy to read. Renaming a workspace (requires admin permissions) can remove it from a policy set.
Use a variable to map workspace names to IDs. Secure and easy to read. Keeping the variable up-to-date is inconvenient.
Use external_id attribute of tfe_workspace resources. Secure, readable, automatically updated. Only available if you already manage your TFE workspaces with the tfe Terraform provider.

In our examples, we want to clearly show what our policy sets are doing, which is much easier if we can refer to workspaces by name. The tfe_workspace_ids data source (in tfe provider versions ≥ 0.6) makes it easy to use workspace names:

data "tfe_workspace_ids" "all" {
  names        = ["*"]
  organization = "${var.tfe_organization}"
}

locals {
  # Use a shorter name for this map in policy set resources:
  workspaces = "${data.tfe_workspace_ids.all.external_ids}"
}

» 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.

  • Use the policy_ids list to specify which policies to include. Reference the id attributes from your policy resources, like "${tfe_sentinel_policy.aws-block-allow-all-cidr.id}".
  • Set a value for either global or workspace_external_ids, to specify which workspaces these policies should be enforced on.

# 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 = [
    "${local.workspaces["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.