Examples & Use Cases

Overview

Scalr implements OPA checks in two different stages: pre-plan and/or post-plan. The pre-plan stage can only evaluate information available before the plan, such as the run source, who executed the run, VCS details, and more. The post-plan stage can evaluate everything included in the pre-plan as well as information included in the terraform plan JSON, specifically deployment details about the resources being created, changed, and deleted.

The following policies are a few examples, to see more, visit this repo.

Pre-Plan

The pre-plan checks execute right before the Terraform plan stage. At this stage, Scalr has access to the tfrun data, which OPA can evaluate. The tfrun data contains information such as the run source, the user who executed the run, commit authors, workspace name, and anything else that is relates to how the run execution started.

Prevent a Destructive Run

If you are using the Scalr provider to manage Scalr workspaces, you may want to put checks in to make sure that users are not destroying workspaces that have active state:

# Enforces that workspaces are tagged with the names of the providers.

package terraform

import input.tfplan as tfplan


deny["Can not destroy workspace with active state"] {
    resource := tfplan.resource_changes[_]
    "delete" == resource.change.actions[count(resource.change.actions) - 1]
    "scalr_workspace" == resource.type

    resource.change.before.has_resources
}

Prevent Auto-Apply

Want to prevent auto-apply runs from happening in an environment such as prod? The following policy will check to see if the workspace setting has auto-apply enabled:

# Allprod  runs must be approved
package terraform
import input.tfrun as tfrun

# Deny if auto-apply is enabled
deny["auto-apply is not allowed"] {
  tfrun.workspace.auto_apply == true
}

Limit Run Sources

Want to ensure only approved users can use the Terraform or Tofu CLI to kick off runs? You can limit run sources with this policy:

package terraform

import input.tfrun as tfrun

allowed_cli_users = ["d.johnson", "j.smith"]

array_contains(arr, elem) {
  arr[_] = elem
}

get_basename(path) = basename{
    arr := split(path, "/")
    basename:= arr[count(arr)-1]
}

deny["User is not allowed to perform runs from Terraform CLI"] {
    "cli" == tfrun.source
    not array_contains(allowed_cli_users, tfrun.created_by.username)
}

Post-Plan

The post-plan checks execute right after the Terraform plan stage. At this stage, Scalr has access to the tfrun and tfplan data, which OPA can evaluate. The tfrun data contains information such as the run source, the user who executed the run, commit authors, workspace name, and anything else that relates to how the run execution started.

Limit Module Source

Want to enforce where modules are being pulled from for specific resources? The following module will evaluate the module source and prevent resources from being pulled from sources they shouldn't be:

# Enforce that specificied resource types are only created by specific modules and not in the root module.

package terraform

import input.tfplan as tfplan


# Map of resource types which must be created only using module
# with corresponding module source
resource_modules = {
    "aws_db_instance": "terraform-aws-modules/rds/aws"
}

array_contains(arr, elem) {
  arr[_] = elem
}

deny[reason] {
    resource := tfplan.resource_changes[_]
    action := resource.change.actions[count(resource.change.actions) - 1]
    array_contains(["create", "update"], action)
    module_source = resource_modules[resource.type]
    not resource.module_address
    reason := sprintf(
        "%s cannot be created directly. Module '%s' must be used instead",
        [resource.address, module_source]
    )
}

deny[reason] {
    resource := tfplan.resource_changes[_]
    action := resource.change.actions[count(resource.change.actions) - 1]
    array_contains(["create", "update"], action)
    module_source = resource_modules[resource.type]
    parts = split(resource.module_address, ".")
    module_name := parts[1]
    actual_source := tfplan.configuration.root_module.module_calls[module_name].source
    not actual_source == module_source
    reason := sprintf(
        "%s must be created with '%s' module, but '%s' is used",
        [resource.address, module_source, actual_source]
    )
}

Enforce Tagging

Do you have standard tags that must be applied to all resources? The tagging policy will check to ensure those tags are part of the Terraform code:

# Enforces a set of required tag keys. Values are bot checked

package terraform

import input.tfplan as tfplan


required_tags = ["owner", "department"]


array_contains(arr, elem) {
  arr[_] = elem
}

get_basename(path) = basename{
    arr := split(path, "/")
    basename:= arr[count(arr)-1]
}

# Extract the tags catering for Google where they are called "labels"
get_tags(resource) = labels {
    # registry.terraform.io/hashicorp/google -> google
    provider_name := get_basename(resource.provider_name)
    "google" == provider_name
    labels := resource.change.after.labels
} else = tags {
    tags := resource.change.after.tags
} else = empty {
    empty := {}
}

deny[reason] {
    resource := tfplan.resource_changes[_]
    action := resource.change.actions[count(resource.change.actions) - 1]
    array_contains(["create", "update"], action)
    tags := get_tags(resource)
    # creates an array of the existing tag keys
    existing_tags := [ key | tags[key] ]
    required_tag := required_tags[_]
    not array_contains(existing_tags, required_tag)

    reason := sprintf(
        "%s: missing required tag %q",
        [resource.address, required_tag]
    )
}

Black List a Provider

If you want to control which providers should not be used, you may want to blacklist specific providers:

# Prevent specified providers from being used

package terraform

import input.tfplan as tfplan

# Blacklisted Terraform providers
not_allowed_provider = [
  "azurerm"
]


array_contains(arr, elem) {
  arr[_] = elem
}

get_basename(path) = basename{
    arr := split(path, "/")
    basename:= arr[count(arr)-1]
}

deny[reason] {
    resource := tfplan.resource_changes[_]
    action := resource.change.actions[count(resource.change.actions) - 1]
    array_contains(["create", "update"], action)  # allow destroy action

    # registry.terraform.io/hashicorp/aws -> aws
    provider_name := get_basename(resource.provider_name)
    array_contains(not_allowed_provider, provider_name)

    reason := sprintf(
        "%s: provider type %q is not allowed",
        [resource.address, provider_name]
    )
}

Buckets Must Be Private

Want to make sure all of your S3 buckets are private? The following policy blocks public buckets:

# Check S3 bucket is not public

package terraform

import input.tfplan as tfplan

deny[reason] {
	r = tfplan.resource_changes[_]
	r.mode == "managed"
	r.type == "aws_s3_bucket"
	r.change.after.acl == "public"

	reason := sprintf("%-40s :: S3 buckets must not be PUBLIC", 
	                    [r.address])
}

Allowed Regions

In this example, we want to restrict which regions users are allows to deploy to depending on the cloud provider used:

# Enforce a list of allowed locations / availability zones for each provider

package terraform

import input.tfplan as tfplan

allowed_locations = {
    "aws": ["us-east-1", "us-east-2"],
    "azurerm": ["eastus", "eastus2"],
    "google": ["us-central1-a", "us-central1-b", "us-west1-a"]
}

array_contains(arr, elem) {
  arr[_] = elem
}

get_basename(path) = basename{
    arr := split(path, "/")
    basename:= arr[count(arr)-1]
}

eval_expression(plan, expr) = constant_value {
    constant_value := expr.constant_value
} else = reference {
    ref = expr.references[0]
    startswith(ref, "var.")
    var_name := replace(ref, "var.", "")
    reference := plan.variables[var_name].value
}

get_location(resource, plan) = aws_region {
    # registry.terraform.io/hashicorp/aws -> aws
    provider_name := get_basename(resource.provider_name)
    "aws" == provider_name
    provider := plan.configuration.provider_config[_]
    "aws" = provider.name
    region_expr := provider.expressions.region
    aws_region := eval_expression(plan, region_expr)
} else = azure_location {
    provider_name := get_basename(resource.provider_name)
    "azurerm" == provider_name
    azure_location := resource.change.after.location
} else = google_zone {
    provider_name := get_basename(resource.provider_name)
    "google" == provider_name
    google_zone := resource.change.after.zone
}

deny[reason] {
    resource := tfplan.resource_changes[_]
    location := get_location(resource, tfplan)
    provider_name := get_basename(resource.provider_name)
    not array_contains(allowed_locations[provider_name], location)

    reason := sprintf(
        "%s: location %q is not allowed",
        [resource.address, location]
    )
}

Example Repo

See more examples in the following repo:https://github.com/Scalr/sample-tf-opa-policies/tree/master