Skip to content

Policy sets

Write Rego policies that check every plan, publish them, and attach them to workspaces.

Updated View as Markdown

Policy sets check every plan against your own Rego rules before it can be applied. You upload policies, publish a version, and attach it to a workspace.

Policies run after every plan, including speculative pull-request plans. Sentinel isn’t supported.

Write a policy

Every policy set must define decision in the package stackship.v1, using Rego v1 syntax. decision returns an object with a findings array. An empty array means the plan passes.

policy.regoplaintext
package stackship.v1

import rego.v1

decision := {"findings": findings}

findings contains finding if {
	some change in input.resource_changes
	change.change.after.acl == "public-read"
	finding := {
		"id": "no-public-buckets",
		"title": "Public bucket",
		"message": "Buckets must not be publicly readable.",
		"resource_address": change.address,
	}
}

Each finding has:

Field Required Limit
id Yes, unique within the result 128 characters
title Yes 160 characters
message Yes 4,000 characters
resource_address No 512 characters
details No A small object for your own metadata

A policy can return at most 500 findings. Unknown fields, a missing decision, or an error during evaluation count as an error, which blocks the run.

Policy input

Your policy receives the plan as JSON:

{
  "version": "stackship.policy.input/v1",
  "run": { "kind": "tracked" },
  "source": {
    "commit_sha": "0123456789abcdef0123456789abcdef01234567",
    "repository_id": "123456789",
    "working_directory": "."
  },
  "workspace": {
    "id": "ws_22222222222222222222222222222222",
    "metadata": { "environment": "production" }
  },
  "resource_changes": [
    {
      "address": "aws_s3_bucket.assets",
      "provider_name": "registry.opentofu.org/hashicorp/aws",
      "change": {
        "actions": ["create"],
        "before": null,
        "after": { "bucket": "northwind-assets", "secret": null },
        "before_sensitive": false,
        "after_sensitive": { "secret": true }
      }
    }
  ]
}
  • run.kind is tracked (push to the tracked branch), speculative (pull request), or manual (CLI or API). Manual runs have a null commit_sha.
  • resource_changes follows the shape of tofu show -json.
  • Sensitive values are replaced with null. Use before_sensitive and after_sensitive to tell a redacted value from a real null.
  • Your policy never sees state, variables, or credentials.

What policies can’t do

Policies run with OPA 1.18.2 and a restricted set of built-ins. Network calls (http.send), environment and time functions, randomness, UUIDs, cryptography, JWT functions, and print aren’t available. The same plan always produces the same result.

Test your policy

Add *_test.rego files next to your policies. Publishing runs opa check --strict and opa test and fails if either fails. Run them locally first:

opa check --strict .
opa test .

Publish a version

Package your .rego files and tests as a .tar.gz archive (up to 100 MB):

tar -czf policies.tar.gz *.rego

Then, in the dashboard:

  1. Open Policies and select Create policy set. Enter a name (lowercase letters, numbers, and hyphens) and an optional description.
  2. On the policy set’s page, under Publish a version, choose a Build workspace and your Rego source archive, then select Publish version.
  3. Wait for the version to show Published. If it shows Failed, fix the source using the failure code next to the version and upload again.
Failure code Meaning
POLICY_COMPILE_FAILED opa check --strict found an error
POLICY_TEST_FAILED A test failed
POLICY_BUNDLE_FAILED The policies couldn’t be packaged; check that stackship.v1.decision is defined

Published versions never change. To update a policy, publish a new version, detach the old version, and attach the new one.

Attach a policy

  1. Open the workspace, then Settings → Policies. You can also use Attach to a workspace on the policy set’s page.
  2. Choose a published version and an Enforcement mode.
  3. Select Attach policy version.

To remove a policy, select Detach next to it and confirm.

The policy set’s Attachments list shows every organization, project, and workspace the set is attached to. Owners and admins can Detach any of them there; it’s the only place in the dashboard to remove an organization or project attachment.

Attaching and detaching affect new runs only; runs already started keep their policies.

Organization and project scope

Use the API to attach a policy set to every workspace in an organization or project:

POST /v1/policy-sets/{policy_set_id}/attachments
Content-Type: application/json
Idempotency-Key: 6f1c...

{
  "policy_version_id": "pol_...",
  "scope": "project",
  "target_id": "prj_...",
  "enforcement_mode": "hard_mandatory"
}

scope is organization, project, or workspace. A policy set can be attached only once along a workspace’s organization → project → workspace path. An overlapping attachment fails with 409 POLICY_ATTACHMENT_OVERLAP — for example, attaching at a project when the set is already attached to the organization or to a workspace in that project, or attaching twice to the same workspace. Detach the existing attachment first.

To list and detach attachments at any scope:

GET /v1/policy-sets/{policy_set_id}/attachments

DELETE /v1/policy-sets/{policy_set_id}/attachments/{attachment_id}
Idempotency-Key: 9b2e...

Each attachment has its scope, target_id and target_name (both null at organization scope), version, and enforcement mode. Detaching requires owner or admin, returns 204, and returns 404 if the attachment is already detached. To change an attachment’s version or scope, detach it and attach again.

Enforcement modes

Mode When the policy returns findings
Advisory Findings are shown on the run; the run continues
Soft mandatory Approval is blocked until someone overrides the finding
Hard mandatory The run fails with Policy failed; it can’t be overridden

A policy error always blocks the run, whatever the mode. On plan-only and speculative runs, which can’t be approved, a soft-mandatory finding also ends the run as Policy failed.

Override a soft policy

Owners and admins can override a soft-mandatory finding so the plan can be approved.

  1. On the run page, expand the Policy stage.
  2. Enter an Override justification (up to 2,000 characters).
  3. Select Override soft policy. Repeat for each failing soft-mandatory policy.

A dashboard override expires after one hour. It applies only to that run and plan and is recorded in the audit log.

With the API, POST /v1/runs/{run_id}/policy-overrides takes the policy_result_digest from GET /v1/runs/{run_id}/policy, a justification, and an expires_at up to 24 hours away.

Answering the override prompt during tofu apply records an override with the justification “OpenTofu CLI soft policy override” that expires after 15 minutes.

Read results

Results appear in the run’s Policy stage, in tofu plan and tofu apply output, and at GET /v1/runs/{run_id}/policy.

Limits

Item Limit
Policy source archive 100 MB
Policy sets attached to one workspace 256
Resource changes in the policy input 5,000
Policy input size 2 MiB
Values in the policy input 50,000
Sensitivity markers in the policy input 20,000
Findings per policy set 500
Result size per policy set 512 KiB

A plan larger than these limits fails policy, and the policy log names the limit, but only when a policy set applies to the workspace. Without one, nothing reads the policy input, so plans of any size continue to approval.

Next steps

Navigation

Type to search…

↑↓ navigate↵ selectEsc close