Most teams that run Terraform or OpenTofu in production have a check somewhere that only one person can read. It might be a Python script that walks the plan JSON, a pile of jq in a CI step, or a reviewer who scrolls through every plan looking for a database replacement. These checks work until the person who wrote them moves on.
Tirith is StackGuardian's open-source policy-as-code framework, built to replace that kind of check. It reads the JSON your pipeline already produces, such as the output of terraform show -json or tofu show -json, and evaluates it against policies stored as JSON files. Each check passes, fails, or is skipped, and the result names the resource and value behind it. Tirith is licensed under Apache 2.0, needs no account, and runs on your own machine or CI runner.
A policy in two minutes
Here is a policy that stops a pipeline from deleting or replacing an RDS instance. It lists the actions a database is allowed to take, so a deletion, or a replacement that deletes first, fails the check.
{
"meta": {
"version": "v1",
"required_provider": "stackguardian/terraform_plan"
},
"evaluators": [
{
"id": "db_not_deleted",
"description": "Databases may be created or updated, never deleted or replaced",
"provider_args": {
"operation_type": "action",
"terraform_resource_type": "aws_db_instance"
},
"condition": {
"type": "ContainedIn",
"value": ["no-op", "read", "create", "update"]
}
}
],
"eval_expression": "db_not_deleted"
}
Why list what's allowed instead of blocking delete? A replacement shows up in the plan as two actions, delete and create, and Tirith checks each one separately. A rule that only looks for delete and negates the result would let a replacement through. The allowlist catches both.
Tirith is not on PyPI (pip install tirith installs an unrelated project), so install it from GitHub and pin a tag. It needs Python 3.8 or newer.
pip install "git+https://github.com/StackGuardian/tirith.git@1.2.1"
Produce a plan, convert it to JSON, and evaluate it:
terraform plan -out=tfplan # or: tofu plan -out=tfplan
terraform show -json tfplan > plan.json # or: tofu show -json tfplan > plan.json
tirith -policy-path policy.json -input-path plan.json --fail-on-error
The exit code is what your CI job acts on:
-
0: every policy passed. -
3: a policy failed. In this example, a plan that replacesaws_db_instance.primaryexits3, so the change never reaches apply. -
1: Tirith could not evaluate anything. For example, if this plan doesn't touch a database, there's nothing to check.
Keeping 3 separate from 1 means a broken gate never looks like a policy violation, and a policy that checked nothing never looks like a pass. Without --fail-on-error, Tirith always exits 0 and reports the verdict in its output. That keeps existing pipelines from turning red on upgrade.
No new policy language
You pick a provider, an operation, and a condition such as Equals, ContainedIn, or RegexMatch, then combine checks with &&, || and !. Anyone who can read JSON can review a Tirith policy in a pull request.
Built-in providers cover:
- Terraform and OpenTofu plans
- Infracost cost estimates
- Kubernetes manifests
- StackGuardian workflow configurations
- Any JSON document
So one framework can stop a database replacement, cap EC2 spend at 100 USD a month, and require a liveness probe on every pod. If none of the providers fits your input, the provider architecture is pluggable, and you can write your own.
Run it in the CI you already have
Because policies live in your repository, the same files gate a GitHub Actions job, a GitLab pipeline, and a run on your laptop.
On GitHub Actions, the Tirith IaC governance action finds the plan, posts a pull-request comment, creates a check run, and sets the exit code:
- run: terraform show -json tfplan > plan.json
- uses: StackGuardian/tirith-iac-governance-action@v2.1.1
with:
fail-on-error: true
Anywhere else, call the CLI directly. Any runner that can run a Python container and produce a plan works the same way.
Help while you write policies
Two recent additions catch mistakes before a policy reaches CI:
-
tirith lintchecks policy files without needing a plan. It catches a condition type that doesn't exist, or a provider argument the operation never reads. Both kinds of policy still parse, but they quietly check nothing.tirith lintandtirith fmtare both available as pre-commit hooks. -
tirith uiis a terminal interface for exploring results down to the failing resource, building policies from a form, and experimenting in a playground. It's in beta, and we'd like your feedback on it.
When policy needs to live in one place
Policy files in each repository work well for one team. Once a dozen repositories copy the same policies, they start to drift apart. For StackGuardian users, tirith platform check evaluates a plan against the policies their StackGuardian organization enforces, instead of local files. The input document, verdict format, and exit codes stay the same. Sensitive values are masked on your machine before anything is uploaded, and --no-source stops the Terraform source from being sent.
Here's a walkthrough of tirith platform check:
The open-source CLI doesn't depend on this mode. It stays free and works on its own.
Who it's for
Tirith is for DevSecOps, platform, and cloud teams that need infrastructure guardrails without building and maintaining their own policy engine.
Contribute this Hacktoberfest
Tirith is taking part in Hacktoberfest, and the team has labelled a set of good first issues. They include new evaluators such as NotRegexMatch, StartsWith, and EndsWith, edge-case tests for the numeric evaluators, and CI housekeeping. Comment on an issue to get it assigned, and read the contributing guide before you open a pull request.
New providers, bug fixes, and pull request reviews are welcome too. If you get stuck, ask in the StackGuardian Slack community.
If Tirith is useful to you, a star on GitHub helps other engineers find it.
Top comments (0)