Skip to main content

Use OPA for IaC governance

info

Open Policy Agent (OPA) serves as the embedded policy engine within the Harness platform. Go to Harness governance overview for more information.

Using OPA in your Infrastructure as Code Management (IaCM) workspace gives you control over what infrastructure changes are made, for example, to add restrictions such as ensuring that a server instance is under a specified size, or that the estimated cost of a proposed change does not exceed a certain amount.

Harness IaCM allows you to use OPA on the entities that are described in the table below:

Type/EntityEvent/ActionDescription
WorkspaceOn saveThis policy will be evaluated whenever there is a configuration change in a workspace (for example Terraform version, repository details, the value of a variable is updated, etc)
Terraform PlanAfter Terraform PlanThis policy will be evaluated whenever a Terraform plan operation is performed in the IaCM stage (for example plan step, apply/destroy step, etc.). The policy will be evaluated against the Plan schema
Terraform StateAfter Terraform PlanThis policy will be evaluated after a Terraform plan operation is performed in the IaCM stage, against the state file. You can use this event to validate policy on resources, before applying any changes.
Terraform StateAfter Terraform ApplyThis policy will be evaluated after a Terraform apply operation is performed in the IaCM stage, against the state file. You can use this event to validate policy on resources, after applying any changes.

Follow these steps to enforce governance using OPA:

  1. Create the policies you would like to enforce
  2. Create a policy set and configure it to work against the right entity and operation

The OPA policies will work against the standard schema of the Terraform plan and state.

Writing policies against Workspace

The following attributes are available to access and write policies against

Attributes:

  • account: The account identifier of the workspace.
  • org: The org identifier of workspace.
  • project: The project identifier of the workspace.
  • created: Created timestamp.
  • updated: Updated timestamp.
  • description: Workspace description.
  • name: Name of the workspace.
  • provider_connector: The connector for the infrastructure provider. The exact attributes within this object change depending on the type of connector.
  • provisioner: The provisioner (terraform).
  • provisioner_version: The provisioner version.
  • repository: The repository from which Harness pulls the IAC.
  • repository_commit: The commit or tag Harness pulls from the IAC code. Should be null if repository_branch is specified.
  • repository_branch: The branch from which Harness pulls from the IAC code. Should be null if repository_commit is specified.
  • repository_connector: The connector used to pull the IAC. The exact attributes within this object change depending on the type of connector.
  • status: The workspace status.
  • environment_variables: A map of the environment variables.
  • terraform_variables: A map of the terraform variables.

OPA Policy examples

Workspace policies

To write policies against the Workspace, please use the following Workspace example:

Some examples include the following:

  • Ensure Terraform version is greater than a specific version.
  • Ensure specific connectors are used.
  • Ensure specific repositories are used (only corporate ones and not public).

This policy denies workspaces using Terraform provisioner with a version less than 1.5.4.

  package workspaces

# deny
deny[msg] {
# if the provisioner is terraform
input.workspace.provisioner == "terraform"
# and the version is greater than 1.5.4
semver.compare(input.workspace.provisioner_version, "1.5.4") == -1
msg := sprintf("the version was %s but the policy specifies > 1.5.4", [input.workspace.provisioner_version])
}

Terraform plan and state policies

You can use OPA to enforce policies at the terraform plan or state level, for example:

  • Define the list of allowed AMIs
  • Define the list of allowed instance types
  • Define tags that must be present in all instances
  • Deny any plan or state that makes use of an AMI outside of the allowed list
  • Deny any plan or state that makes use of an instance type outside of the allowed list
  • Deny any plan or state that makes specified instances that are missing any of the required tags
package terraform_plan

# DEFINE THE LIST OF ALLOWED AMIs
# NOTE Try changing the allowed AMIs to see the policy fail
allowed_amis = ["ami-0aa7d40eeae50c9a9"]

# DEFINE THE LIST OF ALLOWED INSTANCE TYPES
# NOTE Try changing the allowed instance types to see the policy fail
allowed_instance_types = ["t2.nano", "t2.micro"]

# DEFINE TAGS THAT MUST BE PRESENT IN ALL INSTANCES
# NOTE Try changing the required tags to see the policy fail
required_tags = ["Name", "Team"]

# DENY ANY PLAN THAT MAKES USE OF AN AMI OUTSIDE OF THE ALLOWED LIST
deny[sprintf("%s: ami %s is not allowed", [r.address, r.values.ami])] {
r = input.planned_values.root_module.resources[_]
r.type == "aws_instance"
not contains(allowed_amis, r.values.ami)
}

# DENY ANY PLAN THAT MAKES USE OF AN INSTANCE TYPE OUTSIDE OF THE ALLOWED LIST
deny[sprintf("%s: instance type %s is not allowed", [r.address, r.values.instance_type])] {
r = input.planned_values.root_module.resources[_]
r.type == "aws_instance"
not contains(allowed_instance_types, r.values.instance_type)
}

# DENY ANY PLAN THAT MAKES SPECIFIED INSTANCES THAT ARE MISSING ANY OF THE REQUIRED TAGS
deny[sprintf("%s: missing required tag '%s'", [r.address, required_tag])] {
r = input.planned_values.root_module.resources[_]
r.type == "aws_instance"
existing_tags := [key | r.values.tags[key]]
required_tag := required_tags[_]
not contains(existing_tags, required_tag)
}

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