compliance.tf

Operational Rules

  • Operational Rules transform your Terraform modules at download time to enforce your organization's operational standards.
  • Rules handle lifecycle blocks, tagging, provisioner removal, and instance type restrictions.
  • No forks, no wrapper modules. Same terraform init, same modules.
  • Rules and compliance controls are independent layers. Controls run first, then rules. Changes are merged additively.
  • On your organization's host, rules come from a Baseline that applies to every module and named rulesets a module selects with ?ruleset=.

The problem rules solve

Terraform's lifecycle meta-argument cannot be parameterized. You cannot pass prevent_destroy = true into an upstream module as a variable. There is no input for it, no override mechanism, no dynamic configuration. This has been an open request since 2015 (hashicorp/terraform#3116) with hundreds of reactions across related issues.

When a platform team needs prevent_destroy on S3 buckets, ignore_changes on tags, and no provisioner blocks, they respond with one of these workarounds:

  • Fork the module. Maintain a copy with lifecycle blocks added. Repeat for every module. Sync upstream updates manually.
  • Write wrapper modules. Add a layer of indirection that still needs lifecycle blocks inside.
  • Add PR review checklists. "Did you add prevent_destroy?" Depends on human memory. Does not scale.
  • Wire up OPA or Sentinel policies. Can flag missing lifecycle blocks, but cannot add them to the code.

Forking 5-10 modules creates a 5-10x maintenance multiplier. Upstream updates require manual re-forking and testing. Drift between forked versions across teams becomes inevitable. Platform teams become bottlenecks for module updates.

Operational Rules eliminate this. Define your operational standards once, and every module downloaded from the compliance.tf registry has them applied automatically.


Two pillars: Compliance Controls and Operational Rules

compliance.tf solves two distinct problems with one product:

Compliance ControlsOperational Rules
What it enforcesRegulatory requirementsOrganizational standards
Driven byFrameworks (SOC 2, PCI DSS, HIPAA, NIST...)Platform team decisions
ExamplesEncryption at rest, access logging, public access blockingprevent_destroy, ignore_changes, instance restrictions
Who configurescompliance.tf (framework-defined)Org admins (org-defined)
Applied whenModule download (transparent)Module download (transparent)
Can be disabledPer-module via ?disable= parameter on a framework hostPer-request via ?rules= on a framework host; on your organization's host only by changing the Baseline or a ruleset

Both layers apply during terraform init. Developers do not need to know either layer exists. They get hardened, standards-compliant modules by default.


How rules work

  1. Org admin configures rules. On the Operational Rules page (or its API), rules go into the organization's Baseline, which applies to every module downloaded from the organization's host, or into a named ruleset that a module selects with ?ruleset=<name>. On a framework host, developers can also name rules per module with ?rules=.
  2. Developer runs terraform init. The compliance.tf registry resolves the developer's org, looks up the org's rule configuration, and applies rules during module download.
  3. Module arrives with rules applied. Lifecycle blocks, provisioner removal, and instance restrictions are already in the HCL. The developer runs terraform plan and terraform apply normally.

Rules are applied server-side, at the same time as compliance controls. The downloaded module is standard Terraform HCL. You can read it, diff it, and audit it like any other module.

Per-request overrides are available using the ?rules= query parameter in the HTTPS module source URL, on the framework and canonical hosts. On your organization's own host they are refused with a 400 rather than ignored, because there the organization's configuration is the whole answer. The one query parameter an organization host does accept is ?ruleset=<name>, which selects a group of rules the organization published and can only add to the Baseline (see Baseline rules and rulesets):

# Add a rule for this download, on top of your organization's rules
module "s3_bucket" {
  source = "https://soc2.compliance.tf/terraform-aws-modules/s3-bucket/aws?version=5.0.0&add_rules=lifecycle_prevent_destroy_data"
}

# Remove a rule for this download
module "s3_bucket" {
  source = "https://soc2.compliance.tf/terraform-aws-modules/s3-bucket/aws?version=5.0.0&rules=-lifecycle_ignore_tags"
}

# Both at once — one parameter each
module "s3_bucket" {
  source = "https://soc2.compliance.tf/terraform-aws-modules/s3-bucket/aws?version=5.0.0&rules=-lifecycle_ignore_tags&add_rules=lifecycle_prevent_destroy_data"
}

The `+` add prefix is gone — use `?add_rules=`

Adding a rule used to be spelled ?rules=%2Blifecycle_prevent_destroy_data, and the encoding was the whole problem. In a URL query string a literal + means a space, to every decoder between your machine and the registry, so ?rules=+lifecycle_ignore_tags arrived as a bare name with a leading space — and a bare name replaces your organization's rules. A prefix that only survives when you percent-encode it fails silently the rest of the time, so it was removed rather than better documented.

Additive selection is ?add_rules=, which needs no encoding. There is no alias and no grace period: a source URL still carrying %2B is refused with a 400 that names ?add_rules=. The - prefix on ?rules= is unchanged.


How rules interact with compliance controls

Compliance controls and operational rules are separate, independent layers. Controls run first (regulatory enforcement), rules run second (operational standards), and overlapping changes merge additively — see the FAQ below for details.


Frequently asked questions

Compliance and Controls

No. Operational Rules and compliance controls are independent layers. Controls are applied first (regulatory enforcement), rules are applied second (operational standards). A rule cannot override, weaken, or bypass a compliance control.

If a rule and a control affect the same resource attribute, the changes are merged additively (e.g., both adding to ignore_changes). Explicit conflict detection, where a rule contradicts a control, is planned for a future release.

Rules live in a separate scope from controls: controls enforce encryption, logging, access blocking, and other regulatory requirements. Rules enforce lifecycle blocks, tagging behavior, provisioner removal, and instance restrictions.

Rules are not compliance controls and should not be presented to auditors as regulatory enforcement. They are operational safety measures.

That said, several rules support operational practices that auditors view favorably:

  • Prevent Destroy Data and Prevent Destroy Encryption reduce the risk of accidental data loss, which supports data protection and availability objectives.
  • No Provisioners removes a supply chain risk vector from module code, which supports change management and code integrity objectives.
  • Restrict Instance Types supports cost governance, which is relevant to resource management controls.

If an auditor asks about these, position them as operational safeguards that complement your compliance controls, not as compliance controls themselves.

Workflow and Adoption

Yes. Per-request overrides are available using the ?rules= query parameter in the HTTPS module source URL:

# Remove a rule for this download
module "s3_bucket" {
  source = "https://soc2.compliance.tf/terraform-aws-modules/s3-bucket/aws?version=5.0.0&rules=-lifecycle_prevent_destroy_data"
}

# Add a rule that is not in org defaults
module "s3_bucket" {
  source = "https://soc2.compliance.tf/terraform-aws-modules/s3-bucket/aws?version=5.0.0&add_rules=lifecycle_ignore_ami_changes"
}

Free tier: You can apply rules per-request by adding ?rules= to your module source URL. Rules are not forced; you choose which to include.

Trial, paid and enterprise plans: Org admins configure the organization's Baseline and rulesets on the Operational Rules page. On the framework and canonical hosts, individual developers can still override per request: ?rules=-<id> removes a rule for that download and ?add_rules=<id> adds one. On the organization's own host both ?rules= and ?add_rules= are refused with a 400: a rule stops applying to a module only when it is taken out of the Baseline, or kept in a ruleset that module does not select. Org admins can also disable a rule for the entire organization. See Baseline rules and rulesets.

Yes. Operational Rules modify the module source code (standard HCL) before it is downloaded. The resulting module works identically with Terraform and OpenTofu. There are no provider-specific or CLI-specific dependencies in the rule transformations.

All lifecycle meta-arguments (prevent_destroy, ignore_changes) and the absence of provisioner blocks are standard HCL features supported by both Terraform and OpenTofu.

Rules are applied at download time (terraform init). If your org admin enables a new rule or changes the rule configuration, the change takes effect on the next terraform init -upgrade that downloads a fresh copy of the module.

Modules already downloaded and cached in .terraform/modules/ are not retroactively modified. To pick up new rules, run terraform init -upgrade. You can inspect the downloaded .tf files to see which rules were applied to a given module copy.

Not yet. All rules are currently managed by compliance.tf. Org-authored rules (custom HCL transformation rules defined by your team) are on the roadmap. When available, custom rules will follow the same transformation, manifest, and preview model as built-in rules.

Yes. Rules are applied server-side during module download. Any tool that runs terraform init and downloads modules from the compliance.tf registry will receive modules with rules applied. This includes:

  • Atlantis: configure the compliance.tf access token in the Atlantis server's .terraformrc
  • Terragrunt: calls terraform init under the hood. Configure credentials as you would for plain Terraform.
  • HCP Terraform agents: configure the compliance.tf token in the workspace or organization credential settings
  • Spacelift, env0, Scalr: configure the access token in each platform's credential management

No platform-specific integration is required. If the platform can download modules from a private Terraform registry, rules work.

Trust and Transparency

Inspect the downloaded module source directly. The module is standard HCL. Open any .tf file in .terraform/modules/ and read it. The lifecycle blocks, provisioner removals, and other transformations are visible in the code.

A compliancetf-manifest.json file is written into any module compliance.tf builds for you, listing each rule requested, the file paths it changed, and a variant fingerprint. terraform init writes it into the module directory under .terraform/modules/, so the record arrives with the artifact and can be read from your own checkout. It records files, not resource addresses.

A request that needs no controls and no organization rules is served the published module straight from storage, with no build behind it. That module usually carries a manifest too, describing the published release rather than a posture applied for you - and on one path there is no manifest at all, because a build that degrades to zero controls and zero rules produces no variant to write one into. That path exists only off an organization's host: on one, the patch step runs unconditionally, precisely so a zero-posture build still produces an artifact that says so instead of an unpatched module nobody can tell apart from an unenforced one. Do not read presence as proof: check whether posture.rules_hash actually holds a value. Depending on when the release was built the block may be missing entirely or present with null fields, and neither means a posture was applied - a populated rules_hash does.

The fingerprint at posture.rules_hash is a 16-character truncated SHA-256 covering the whole variant identity - the framework and the controls you enabled or disabled, the posture snapshot, and your organization's rules and their parameters - not the rule list alone. It is also the suffix on the served module version, which is what makes it useful: comparing the two answers "did my organization's change actually reach this module?" from the module directory alone.

It also carries a degraded list, and the distinction matters if you rely on the manifest as evidence. An empty list means the build reported its posture and dropped nothing. A populated list names what was dropped. The field is always present, and a null there - not a missing key - means the build never reported, so the artifact does not attest whether your organization's rules were applied, which is not the same as attesting that none were needed.

degraded answers a narrower question than it appears to. It reports that something stopped the build assembling your organization's full posture - the rules could not be fetched, say, or no baseline had been authored. Treat the entries as reasons to look into, not as a closed list. It does not report whether the rules that were applied went on to enforce anything, so degraded: [] is not evidence that everything was enforced. That is the rule list's job, below.

Read the rule list the same way. It records the rules requested, not the rules that acted. A rule can appear, be applied, and still change nothing - because your organization consented to a permissive setting, because it was never configured, or because it does not apply to this module. Each entry therefore carries an outcome of enforced, waived_by_consent, not_configured, or not_applicable, and an enforcing rule that still tolerates an unset value is marked allows_unset. The same absence rule holds throughout: a missing field means the build did not classify that rule - an older producer, typically - and must never be read as enforcement.

When the Preview API becomes available, you will also be able to see unified diffs before downloading.

For now, you can compare the downloaded .tf files directly across environments. If the same module version was downloaded with the same ?rules= parameter, or from the same organization Baseline and ruleset, the resulting HCL will be identical. A simple diff or checksum of the downloaded module directories confirms consistency.

The compliance.tf registry records a variant fingerprint in the compliancetf-manifest.json file inside a module it builds for you, as posture.rules_hash - a 16-character truncated SHA-256 over the whole variant identity, not a hash of the rule list alone. It is not returned in a response header: Terraform reads exactly one header (X-Terraform-Get) and persists none, so a header could never reach you. The manifest is written to disk by terraform init, which is what makes the hash checkable from your own machine.

When you need to intentionally destroy a protected resource (database migration, bucket replacement, key rotation), use a per-request override to remove the Prevent Destroy Data or Prevent Destroy Encryption rule for a single terraform init:

module "s3_bucket" {
  source = "https://soc2.compliance.tf/terraform-aws-modules/s3-bucket/aws?version=5.0.0&rules=-lifecycle_prevent_destroy_data"
}

Run terraform init -upgrade to download the module without prevent_destroy. Run your terraform destroy or replacement plan. After the operation, change the source back to remove the override and run terraform init -upgrade again to restore the protection.

The per-request override is visible in the downloaded HCL: the prevent_destroy lifecycle block will be absent.

On your organization's host that override is refused with a 400. A Baseline rule applies to every module, so the break-glass path there is an admin decision: disable the rule, or move it into a ruleset the affected module does not select, for the duration of the operation, then restore it. A project environment's posture comes from its bound snapshot and waivers instead. Org admins can also change the Baseline if the exception is needed permanently.

Downloaded modules are standard Terraform HCL. The lifecycle blocks, tagging behavior, and provisioner changes are all written directly in the .tf files. To leave compliance.tf:

  1. Change the module source URL back to terraform-aws-modules/...
  2. Manually add the lifecycle blocks that your team needs (or don't; they are standard Terraform features you can add to any module)
  3. Run terraform init -upgrade and terraform plan

Because the lifecycle settings in compliance.tf modules are identical to what you would write by hand, there is no state migration, no resource recreation, and no proprietary format to unwind. If your existing state already has the values that rules enforce (e.g., prevent_destroy = true), the plan will show no changes.

Fair question. Here is what compliance.tf provides that a wrapper repo does not:

  • Transparency: The downloaded module is plain HCL. You can read every .tf file and see exactly what was transformed. A manifest file (compliancetf-manifest.json) records the rules requested, what each one actually did, and a variant fingerprint. A wrapper repo may add the same lifecycle blocks, but has no standard manifest format documenting what it changed.
  • Consistency: When the same rules are applied (via ?rules= on a framework host, or the organization's Baseline and rulesets), the same module version produces identical HCL. Wrapper repos depend on consistent builds and artifact promotion, which is possible but adds moving parts.
  • No maintenance burden: Wrapper repos require re-wrapping on every upstream module update. compliance.tf applies rules to the latest upstream version automatically.
  • Inspectability: Open the downloaded .tf files and read them. When the Preview API ships, you can see the diffs before downloading.

That said, if your wrapper repo is well-maintained and working, rules are not a forced migration. They are an option for teams that want the same outcome without the fork tax.


Where to go next

On this page

Ask AI about this

Help improve this page