<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Akshat Tandon</title>
    <description>The latest articles on DEV Community by Akshat Tandon (@akshat_sg).</description>
    <link>https://dev.to/akshat_sg</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F83204%2F01ce489d-a7d0-4ed1-8bdf-3276bbf524fc.jpg</url>
      <title>DEV Community: Akshat Tandon</title>
      <link>https://dev.to/akshat_sg</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/akshat_sg"/>
    <language>en</language>
    <item>
      <title>Meet Tirith: Open-Source Policy as Code for Terraform and OpenTofu Plans</title>
      <dc:creator>Akshat Tandon</dc:creator>
      <pubDate>Tue, 06 Oct 2026 13:03:11 +0000</pubDate>
      <link>https://dev.to/akshat_sg/meet-tirith-open-source-policy-as-code-for-terraform-and-opentofu-plans-4daa</link>
      <guid>https://dev.to/akshat_sg/meet-tirith-open-source-policy-as-code-for-terraform-and-opentofu-plans-4daa</guid>
      <description>&lt;p&gt;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 &lt;code&gt;jq&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/StackGuardian/tirith" rel="noopener noreferrer"&gt;Tirith&lt;/a&gt; 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 &lt;code&gt;terraform show -json&lt;/code&gt; or &lt;code&gt;tofu show -json&lt;/code&gt;, 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  A policy in two minutes
&lt;/h2&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"meta"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"required_provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stackguardian/terraform_plan"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"evaluators"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"db_not_deleted"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Databases may be created or updated, never deleted or replaced"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"provider_args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"operation_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"terraform_resource_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aws_db_instance"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ContainedIn"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"no-op"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"create"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"update"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"eval_expression"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"db_not_deleted"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why list what's allowed instead of blocking &lt;code&gt;delete&lt;/code&gt;? A replacement shows up in the plan as two actions, &lt;code&gt;delete&lt;/code&gt; and &lt;code&gt;create&lt;/code&gt;, and Tirith checks each one separately. A rule that only looks for &lt;code&gt;delete&lt;/code&gt; and negates the result would let a replacement through. The allowlist catches both.&lt;/p&gt;

&lt;p&gt;Tirith is not on PyPI (&lt;code&gt;pip install tirith&lt;/code&gt; installs an unrelated project), so install it from GitHub and pin a tag. It needs Python 3.8 or newer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"git+https://github.com/StackGuardian/tirith.git@1.2.1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Produce a plan, convert it to JSON, and evaluate it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform plan &lt;span class="nt"&gt;-out&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tfplan               &lt;span class="c"&gt;# or: tofu plan -out=tfplan&lt;/span&gt;
terraform show &lt;span class="nt"&gt;-json&lt;/span&gt; tfplan &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; plan.json  &lt;span class="c"&gt;# or: tofu show -json tfplan &amp;gt; plan.json&lt;/span&gt;
tirith &lt;span class="nt"&gt;-policy-path&lt;/span&gt; policy.json &lt;span class="nt"&gt;-input-path&lt;/span&gt; plan.json &lt;span class="nt"&gt;--fail-on-error&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exit code is what your CI job acts on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;0&lt;/code&gt;: every policy passed.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;3&lt;/code&gt;: a policy failed. In this example, a plan that replaces &lt;code&gt;aws_db_instance.primary&lt;/code&gt; exits &lt;code&gt;3&lt;/code&gt;, so the change never reaches apply.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;1&lt;/code&gt;: Tirith could not evaluate anything. For example, if this plan doesn't touch a database, there's nothing to check.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keeping &lt;code&gt;3&lt;/code&gt; separate from &lt;code&gt;1&lt;/code&gt; means a broken gate never looks like a policy violation, and a policy that checked nothing never looks like a pass. Without &lt;code&gt;--fail-on-error&lt;/code&gt;, Tirith always exits &lt;code&gt;0&lt;/code&gt; and reports the verdict in its output. That keeps existing pipelines from turning red on upgrade.&lt;/p&gt;

&lt;h2&gt;
  
  
  No new policy language
&lt;/h2&gt;

&lt;p&gt;You pick a provider, an operation, and a condition such as &lt;code&gt;Equals&lt;/code&gt;, &lt;code&gt;ContainedIn&lt;/code&gt;, or &lt;code&gt;RegexMatch&lt;/code&gt;, then combine checks with &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;, &lt;code&gt;||&lt;/code&gt; and &lt;code&gt;!&lt;/code&gt;. Anyone who can read JSON can review a Tirith policy in a pull request.&lt;/p&gt;

&lt;p&gt;Built-in providers cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Terraform and OpenTofu plans&lt;/li&gt;
&lt;li&gt;Infracost cost estimates&lt;/li&gt;
&lt;li&gt;Kubernetes manifests&lt;/li&gt;
&lt;li&gt;StackGuardian workflow configurations&lt;/li&gt;
&lt;li&gt;Any JSON document&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it in the CI you already have
&lt;/h2&gt;

&lt;p&gt;Because policies live in your repository, the same files gate a GitHub Actions job, a GitLab pipeline, and a run on your laptop.&lt;/p&gt;

&lt;p&gt;On GitHub Actions, the &lt;a href="https://github.com/StackGuardian/tirith-iac-governance-action" rel="noopener noreferrer"&gt;Tirith IaC governance action&lt;/a&gt; finds the plan, posts a pull-request comment, creates a check run, and sets the exit code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform show -json tfplan &amp;gt; plan.json&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;StackGuardian/tirith-iac-governance-action@v2.1.1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;fail-on-error&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anywhere else, call the CLI directly. Any runner that can run a Python container and produce a plan works the same way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Help while you write policies
&lt;/h2&gt;

&lt;p&gt;Two recent additions catch mistakes before a policy reaches CI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;tirith lint&lt;/code&gt; checks 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. &lt;code&gt;tirith lint&lt;/code&gt; and &lt;code&gt;tirith fmt&lt;/code&gt; are both available as pre-commit hooks.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tirith ui&lt;/code&gt; is 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.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When policy needs to live in one place
&lt;/h2&gt;

&lt;p&gt;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, &lt;code&gt;tirith platform check&lt;/code&gt; 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 &lt;code&gt;--no-source&lt;/code&gt; stops the Terraform source from being sent.&lt;/p&gt;

&lt;p&gt;Here's a walkthrough of &lt;code&gt;tirith platform check&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/8vArDov5mAo" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;The open-source CLI doesn't depend on this mode. It stays free and works on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who it's for
&lt;/h2&gt;

&lt;p&gt;Tirith is for DevSecOps, platform, and cloud teams that need infrastructure guardrails without building and maintaining their own policy engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Contribute this Hacktoberfest
&lt;/h2&gt;

&lt;p&gt;Tirith is taking part in Hacktoberfest, and the team has labelled a set of &lt;a href="https://github.com/StackGuardian/tirith/issues?q=is%3Aopen+label%3A%22good+first+issue%22" rel="noopener noreferrer"&gt;good first issues&lt;/a&gt;. They include new evaluators such as &lt;code&gt;NotRegexMatch&lt;/code&gt;, &lt;code&gt;StartsWith&lt;/code&gt;, and &lt;code&gt;EndsWith&lt;/code&gt;, edge-case tests for the numeric evaluators, and CI housekeeping. Comment on an issue to get it assigned, and read the &lt;a href="https://github.com/StackGuardian/tirith/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;contributing guide&lt;/a&gt; before you open a pull request.&lt;/p&gt;

&lt;p&gt;New providers, bug fixes, and pull request reviews are welcome too. If you get stuck, ask in the &lt;a href="https://join.slack.com/t/stackguardian-ol78820/shared_invite/zt-2ksag36j9-OjmXqQmyXudgYrV6FmesIQ" rel="noopener noreferrer"&gt;StackGuardian Slack community&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If Tirith is useful to you, a star on &lt;a href="https://github.com/StackGuardian/tirith" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; helps other engineers find it.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>terraform</category>
      <category>devops</category>
      <category>hacktoberfest</category>
    </item>
  </channel>
</rss>
