Sign in
Menu
Sign up
Docs
Govern your organization

Rulesets in the console

This page is for the organization admin who sets operational standards for every team. By the end you will have a Baseline that every module from your organization's host receives, a named ruleset that one module opts into, and a way to confirm from the downloaded module that both arrived. It is also the reference for which rule parameters a module source may carry on each host.

  • An organization's operational rules live in two groups. The Baseline applies to every module served from the organization's own host. A ruleset is a named group that one module opts into by adding ?ruleset=<name> to its source.
  • ?ruleset= selects a group the organization already published. It cannot add a parameter, change a value, or remove a rule; a selected ruleset is applied on top of the Baseline, never instead of it.
  • The live rules apply only while the organization's rules state is Enforced. Disabled and Preview keep the configuration without applying it.
  • A project environment is different: it serves the rules frozen in its bound snapshot, and refuses ?ruleset=.
  • The module's manifest records which rules came from a ruleset, by name and content hash.

How it works

The Baseline

The Baseline is every rule instance in the live store that carries no ruleset name. Each rule in it is an instance of a catalog rule with its own parameters: the tag keys to ignore, the regions to allow, the instance types to deny. A parameter is validated against the catalog when you save it: an unknown parameter or a value of the wrong type is refused, and a parameter with no default that you leave out is absent rather than null. Which rules exist and what each one enforces is the rule catalog's job; this page is about where an instance lives.

Named rulesets

A ruleset is a name attached to rule instances, not a separate object with its own life. Giving a rule a ruleset name moves it out of the Baseline; clearing the name moves it back.

PropertyBehaviour
Creating oneAdd a rule under a new name, or move an existing rule into it. The name must match ^[a-z][a-z0-9-]{1,63}$.
MembershipThe live store holds one instance of each catalog rule per organization, so a rule is in the Baseline or in exactly one ruleset. It cannot be in two rulesets, and a rule in a ruleset is not in the Baseline. A disabled rule is skipped wherever it lives.
RenamingRenames every member at once. A name that already exists is refused with a 409; nothing is merged silently.
DeletingUngroups: the members return to the Baseline with their parameters intact. Nothing is removed from the store.
Registry visibilityThe registry knows a ruleset by the enabled rules that carry its name. A ruleset whose rules are all disabled, or one you have just created and not yet filled, is unknown to the registry and refused as such.
Versioning, approvalNone. A ruleset is live-edited; a change is visible on the next fresh download. There is no immutable version, no approval step and no content pin a source string can name. The manifest's content hash tells you which content a given build saw.

A rule that moves from the Baseline into a ruleset stops applying to every other module. If the intent is "every module, plus one extra rule for these three", the extra rule goes in the ruleset and the shared rules stay in the Baseline.

The rules state

The rules state has three positions: Disabled, Preview and Enforced. Only Enforced sends the live rules to a build. In Disabled and Preview the Baseline and every ruleset are stored and shown but not applied, and a module served in either state carries no live rule. A change of state reaches a workspace on its next fresh download (terraform init -upgrade where the module is already cached), like any rule change.

Two stores meet on the organization's host

The Operational Rules page edits a live store: the Baseline and the rulesets. The organization's baseline snapshot, a published config that the organization's own host serves when no project or environment is named (see Configs and snapshots), may name rules too. The registry serves the union. The rules frozen in that snapshot are outside the rules-state toggle: they are part of a published posture and apply while the organization points at that snapshot.

When both stores name the same rule with different parameters, the snapshot's parameters win, because a snapshot was published under review and a live row can be edited by anyone with the admin role. The collision is logged rather than resolved silently, so a live edit to a rule the snapshot also names does not reach the download.

An organization whose baseline snapshot was never set gets a 404 ORG_BASELINE_NOT_CONFIGURED on its own host rather than a module with no posture.

Where it is in the console

Open Operational Rules in the organization's dashboard at app.compliance.tf/orgs/<alias>/operational-rules.

  • The Baseline is the first card, marked applies to every module. It cannot be renamed or deleted.
  • Each named ruleset is its own group on the same page.
  • The rules state toggle sits at the top of the page.

The organization rules API backs this page. It uses your signed-in dashboard session, not a ctf_ registry token. The registry token only authenticates module downloads.

Selecting rules in the source URL

Every rule parameter a module source can carry is defined here. Other pages link to this section instead of repeating it.

Which parameters a request may carry depends on the host it addresses:

Address formRules applied?ruleset=?rules=, ?add_rules=
https://<alias>.compliance.tf/<namespace>/<name>/<provider>, the organization's own hostThe Baseline, plus the rules frozen in the organization's baseline snapshot, plus the ruleset the request names, if anyHonouredRefused, 400
<alias>.compliance.tf/<project>/<env>/<module>, a project environmentThe rules list frozen in the snapshot the environment is bound to, nothing elseRefused, 400Refused, 400
soc2.compliance.tf, registry.compliance.tf and the other framework and canonical hostsThe token holder's organization Baseline when that organization's rules state is Enforced, plus a ruleset of that organization if the request names one, adjusted by ?rules= and ?add_rules=Honoured, against the token holder's organizationHonoured
Which rules a download receives Flowchart: a request on an organization's bare host receives the Baseline, the rules frozen in the baseline snapshot and any ruleset the source selects; a project environment receives only the rules of its bound snapshot; a framework host receives the token holder's organization Baseline when its rules state is Enforced, plus any ruleset the request names, adjusted by ?rules= and ?add_rules=. All three produce a built module with a manifest inside. ?ruleset= binding ?rules= Organization host/<ns>/<name>/<provider> Project environment/<project>/<env>/<module> Framework hostsoc2.compliance.tf Baseline + rulesetlive rules + snapshot Bound snapshotfrozen rules only Caller's Baseline+ ruleset · ?rules= edits Built modulemanifest inside LegendAddress formRule sourceSelected per module callOutput

How a module request resolves covers the whole path a request takes; this table is the rules column of it.

?rules= and ?add_rules= on a framework host

SpellingEffect for this download
?rules=a,bA bare list replaces the organization's Baseline with the named rules
?rules=-aRemoves rule a from the Baseline
?add_rules=aAdds rule a on top of the Baseline
?rules=-a&add_rules=bBoth, one parameter each; both work against the Baseline
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 grammar is strict, and each refusal is a 400 rather than a silent reinterpretation:

  • Rule ids are flat snake_case slugs, the same shape as control ids. A name in ?rules= must match ^-?[a-z][a-z0-9_]{1,63}$. ?add_rules= takes bare ids only, ^[a-z][a-z0-9_]{1,63}$; ?add_rules=-<id> is refused with a message naming ?rules=-<id>.
  • A ?rules= list is all bare or all - prefixed. Mixing the two is refused.
  • A bare ?rules= list cannot be combined with ?add_rules=: a bare list replaces the Baseline, so nothing is left to add to.
  • A + in ?rules= or ?add_rules=, in either spelling, is refused with a message naming ?add_rules=. A literal + in a query string decodes to a space, so the old + prefix turned an add into a replace whenever it was not percent-encoded.

Names only, never parameters. Each rule named in a URL runs with its catalog defaults. A rule whose defaults are empty, such as Allowed regions or Ignore selected tag keys, is accepted from a URL and does nothing: the manifest records it not_configured. Parameters are set per rule on the Operational Rules page and travel through the Baseline or a ruleset.

To see what a list does before you commit it, run it in the Rules Playground or the Rules Preview API.

?ruleset=

A module opts into a ruleset from its source, on the HTTPS form of the organization's own address:

module "logs_bucket" {
  source = "https://bobthecorp.compliance.tf/terraform-aws-modules/s3-bucket/aws?version=5.0.0&ruleset=ignore-scanner-tags"
}

The build gets the Baseline, whatever the baseline snapshot froze, and every enabled rule named ignore-scanner-tags. The parameters are the ones the organization saved. The URL gives a name and nothing else.

RequestAnswer
Name matches an enabled rule in the organizationApplied, on top of the Baseline
Name is not a ruleset of this organization, or all its rules are disabled400 ORG_HOST_RULESET_UNKNOWN
Name does not match ^[a-z][a-z0-9-]{1,63}$400 ORG_HOST_RULESET_INVALID
?ruleset= on a project environment (<alias>.compliance.tf/<project>/<env>/<module>)400 ORG_HOST_RULESET_UNSUPPORTED_ON_PROJECT_PATH: that path serves its snapshot, and attributing a ruleset it cannot apply would be a false claim
?ruleset= together with ?rules=, ?add_rules=, ?enable=, ?disable= or ?framework=400 ORG_HOST_POSTURE_OVERRIDE_REJECTED: the five posture parameters stay refused on an organization host, with or without a ruleset

On a framework or canonical host, ?ruleset= resolves the same way against the token holder's organization: the named ruleset is added to that organization's Baseline, and an unknown or malformed name gets the same 400. A bare ?rules= list in the same request replaces the result, as above.

Three consequences of the mechanism:

  • Per module call means per distinct source string. The registry sees the URL, never the module label. Two blocks with byte-identical sources are one download and one variant; to give one bucket a ruleset and another none, their source strings must differ.
  • Editing a ruleset changes the artifact, not the version. The served module version does not encode the ruleset, but the variant identity includes a hash of the ruleset's content, so a changed ruleset is a new build rather than a stale cache hit. A workspace that already resolved the module keeps its copy until terraform init -upgrade. Editing the Baseline changes the artifact the same way.
  • Every rule in the ruleset is applied to every module that selects it. There is no per-resource targeting: a rule scoped to aws_s3_bucket acts on every bucket resource in the module, and a module with no matching resource records the rule as not applicable.

Why the organization's host refuses the other five

An organization's host refuses ?rules=, ?add_rules=, ?enable=, ?disable= and ?framework= with a 400 ORG_HOST_POSTURE_OVERRIDE_REJECTED, on both of its address forms and on the presence of the key alone: an empty ?rules= is refused too. Any of them would let whoever edits a source string author or subtract posture, and production posture must not be editable from a module block. Refusing rather than ignoring is deliberate: a silently dropped override leaves you believing the posture you named was applied.

?ruleset= is different in kind. It carries a name, and the name resolves on the server to rules and parameters an organization admin saved. It cannot introduce a value the organization did not configure, and it cannot remove anything: the Baseline is always applied, and a ruleset only adds to it. The source string selects among postures the organization published; it never writes one.

Credentials for the organization's host

Both address forms also work in the Terraform Registry format, without https:// (bobthecorp.compliance.tf/terraform-aws-modules/s3-bucket/aws with a version argument), and Terraform then reads the token from a TF_TOKEN_bobthecorp_compliance_tf variable or a credentials block, as on any registry host. A source that carries ?ruleset= is an HTTPS source, and for it Terraform reads .netrc instead, the same mechanism the HTTPS URL format uses on a framework host, with the organization's alias as the machine:

machine bobthecorp.compliance.tf
    login anything
    password ctf_EXAMPLE_TOKEN

The token must belong to a member of that organization, or be an organization token bound to it. Any other credential is refused with a 403, whatever the path.

Apply a ruleset to one module

A vulnerability scanner writes a scanner-last-seen tag on the buckets it inspects, and Terraform plans to remove it on every run. You want that key ignored on the log buckets the scanner touches, not on every bucket.

  1. On the Operational Rules page, create a ruleset ignore-scanner-tags and add Ignore selected tag keys to it with tag_keys = ["scanner-last-seen"]. Leave resource_types at its default, aws_s3_bucket. Confirm the rules state is Enforced.

  2. Point the affected module at the ruleset:

    module "alb_logs" {
      source = "https://bobthecorp.compliance.tf/terraform-aws-modules/s3-bucket/aws?version=5.0.0&ruleset=ignore-scanner-tags"
    }

    Every other s3-bucket call keeps its plain source and receives the Baseline (and the baseline snapshot's rules) only.

  3. Run terraform init; the source changed, so Terraform downloads the module again. In .terraform/modules/alb_logs/main.tf the bucket resource now carries ignore_changes = [tags["scanner-last-seen"]], and compliancetf-manifest.json lists lifecycle_ignore_tag_keys with outcome: "enforced" and ruleset.name: "ignore-scanner-tags".

Naming this rule in ?rules= on a framework host is accepted, but it achieves nothing: ?rules= carries names only, the rule's default tag list is empty, and an empty tag list ignores no keys. A ruleset is the only way to give it tag keys and apply it to some modules and not others.

What it does not do

  • No versions and no approval. A Baseline or ruleset edit is live on the next fresh download. If a rule change needs review before it reaches production, put the rule in a config and promote a snapshot through Promotion and gates: a project environment serves only the rules frozen in its bound snapshot.
  • No subtraction from a URL on the organization's host. ?ruleset= only adds. Removing a Baseline rule for one module means moving that rule into a ruleset the other modules select.
  • No per-resource targeting inside a module, as above.
  • No parameters in a URL on any host.

Who may do what

ActionRequired
Read the Baseline and the rulesetsMembership in the organization
Add, edit, move, enable or disable a rule; rename or ungroup a ruleset; change the rules stateThe admin role, on a trial, paid or enterprise plan. On the free plan these writes are refused with a 403 and an upgrade message; see Plans and entitlements
Download a module from the organization's hostA registry token that belongs to a member, or an organization token bound to the organization; see Registry tokens

Roles and permissions across the organization are summarized on Access control.

What gets recorded

Edits to the Baseline and rulesets, and changes to the rules state, write no record in the audit trail. What each build received is recorded in the module itself.

The compliancetf-manifest.json written into the module lists every rule the build received under operational_rules[]. A rule that arrived through a ruleset carries one extra field:

{
  "rule": "lifecycle_ignore_tag_keys",
  "outcome": "enforced",
  "ruleset": { "name": "ignore-scanner-tags", "content_hash": "3f9c2a71b0e4d8c5" }
}

name is the ruleset the source selected. content_hash is a 16-character truncated SHA-256 over the ruleset's rules and parameters as the build saw them, so two modules built from the same ruleset content carry the same hash and an edited ruleset produces a different one. Baseline rules carry no ruleset field at all; its absence means "part of the organization's baseline posture", not "unknown". The manifest does not tell a live Baseline rule from one frozen in the baseline snapshot: both are unstamped.

Read outcome before reading presence, as on every rule: a listed rule can have enforced nothing, because its parameters were empty or because the module had no resource it targets. How to read the rest of the manifest is on Rules overview.

On this page

Ask AI about this

Help improve this page