Policies
Declarative guardrails evaluated at specific lifecycle phases before or after infrastructure changes are applied.
Built-in types: customer_zone | required_tags | naming_pattern | script | sbom_pinned_versions | sbom_allowed_registries | sbom_denied_packages | sbom_max_components | sbom_license | Phases: validate / build / plan / deploy | Enforcement: deny / warn / audit | Declared in: configuration.spec.policies
Overview
Policies let you declare constraints that strata enforces automatically as part of its standard commands — no extra scripts or wrappers required.
Unlike lifecycle hooks, which run arbitrary scripts at named points in the pipeline, policies evaluate structured data against a rule and produce a pass / fail result. The enforcement level determines what happens on failure: block the pipeline, emit a warning, or silently record the result.
Feature |
Lifecycle hooks |
Policies |
|---|---|---|
What they do |
Run arbitrary scripts |
Evaluate structured rules against config/plans |
Where declared |
|
|
Failure mode |
Non-zero exit code stops the pipeline |
Configurable: |
Scope |
Single document (workspace/namespace/…) |
Whole deployment (cross-document visibility) |
Example use case |
Run a backup before apply |
Prevent resources in disallowed regions |
The practical difference: if you need to enforce something about the infrastructure plan or configuration values, use a policy. If you need to do something at a lifecycle point (download a file, rotate a secret, send a notification), use a lifecycle hook.
Configuration
Policies are declared in your configuration.yaml under spec.policies. Each policy is an item in the list.
apiVersion: strata.huybrechts.xyz/v1
kind: configuration
meta:
name: my-configuration
spec:
policies:
- name: zone_enforcement
type: customer_zone
phase: plan
enforcement: deny
description: "Reject any Terraform resource targeting a disallowed region"
Fields
Field |
Required |
Type |
Description |
|---|---|---|---|
|
Yes |
string |
Unique identifier for this policy. Used in logs and manifest records. |
|
Yes |
string |
Policy implementation to use. See Policy types. |
|
Yes |
string |
Lifecycle phase when this policy runs. See Phases. |
|
No |
string |
What happens on failure: |
|
No |
bool |
Set to |
|
No |
string |
Human-readable note recorded in logs and the deployment manifest. |
|
No |
mapping |
Type-specific settings. Required by some policy types (e.g. |
Policy Types
Built-in types
Type |
Phase |
What it checks |
|---|---|---|
|
|
Terraform resource regions vs. the tenant’s allowed zones |
|
|
Allow or deny Terraform resource types (allowlist or blocklist) |
|
|
Required tags/labels present on all resources in the platform artifact |
|
|
|
|
|
Remote references follow configured tag naming conventions |
|
any |
Delegates to an external command (OPA, Checkov, custom script) |
|
|
SBOM components have pinned, non-floating version tags |
|
|
Container images originate only from approved registries |
|
|
No SBOM component matches a purl/name blocklist pattern |
|
|
Total SBOM component count stays within a configured budget |
|
|
SBOM component licenses match an allow/deny list (via |
|
|
CVE vulnerability findings stay within a configured severity threshold |
|
|
Estimated monthly cost (from |
tenant_zone
Evaluates at the plan phase, after terraform plan produces a plan JSON and before terraform apply runs.
It reads:
The tenant’s allowed zones from
tenant.auto.tfvars.jsonin the working directory.The zone-to-region mapping from
configuration.spec.zones.Every
createandupdateresource change in the plan JSON.
For each resource change, it checks whether the target region (from location or region in the plan) falls within the tenant’s allowed regions. Any resource targeting a disallowed region is reported as a violation.
No configuration block is needed — the policy reads zone data from the configuration file automatically.
resource_type_restrictions
Evaluates at the plan phase. Reads every create and update resource change from the Terraform plan JSON and checks the resource type against a configured list.
Two modes:
deny(default) — the policy fails if any planned resource has a type on the denied list. Use this to block specific resource types outright.allow— the policy fails if any planned resource has a type that is not on the allowed list. Use this for a strict allowlist.
# Block bare VMs — use managed node pools instead
- name: no_bare_vms
type: resource_type_restrictions
phase: plan
enforcement: deny
configuration:
mode: deny
types:
- azurerm_virtual_machine
- aws_instance
- google_compute_instance
# Strict allowlist
- name: approved_types_only
type: resource_type_restrictions
phase: plan
enforcement: deny
configuration:
mode: allow
types:
- azurerm_kubernetes_cluster
- azurerm_storage_account
Configuration fields:
Field |
Type |
Default |
Description |
|---|---|---|---|
|
string |
|
Operating mode: |
|
list of string |
required |
Terraform resource type strings (e.g. |
|
list of string |
|
Which plan actions trigger the check ( |
Skipped gracefully when no plan data is available or types is empty.
naming_pattern
Evaluates at the validate phase, after structural validation completes. Requires the --deep flag on strata validate to load the configuration service.
It checks whether the meta.name field in the loaded configuration file matches the regex in configuration.pattern, using a full-string match.
Skipped gracefully when no configuration service has been loaded (i.e. strata validate was run without --deep).
- name: naming_convention
type: naming_pattern
phase: validate
enforcement: warn
configuration:
pattern: "^[a-z][a-z0-9-]*$"
script
Delegates policy evaluation to an external command — OPA, Checkov, a custom shell script, or any executable that can read JSON from stdin.
strata launches the command, writes a JSON context object to its standard input, and waits for it to exit. Exit code 0 is a pass; any non-zero exit code is a failure. Stdout is captured as the violation message.
Context sent on stdin:
{"phase": "plan", "work_path": "/path/to/workspace"}
The script type can run at any phase — set phase to whichever phase you want the evaluation to occur. Timeout defaults to 30 seconds. Skipped gracefully when no command is configured.
- name: opa_check
type: script
phase: plan
enforcement: deny
configuration:
command: "opa eval --data policy.rego --input /dev/stdin -"
timeout: 30
ref_convention
Evaluates at the validate phase. Checks that remote references (git tags, branches, or commit SHAs) in deployments and environments follow declared naming conventions.
Useful for enforcing that production environments only pin to semantic version tags (e.g., v1.2.0), not branch names like main or raw commit SHAs. Works in tandem with ADR 0017 (tag-based release workflows) to validate release discipline and detect accidental manual pins.
Skipped gracefully when no deployments/environments are found, or when no remote patterns are configured.
- name: release_tag_conventions
type: ref_convention
phase: validate
enforcement: warn
configuration:
remotes:
- name: my-service
release_pattern: "^v\\d+\\.\\d+\\.\\d+$" # semver only
quality_pattern: "^tested(-\\d+)?$" # quality gate tags
- name: tf-landscape
release_pattern: "^v\\d+\\.\\d+\\.\\d+$"
Configuration fields:
Field |
Type |
Description |
|---|---|---|
|
list |
List of remote repository configurations |
|
string |
Remote repository name (from |
|
string |
Regex pattern for release tags (e.g., |
|
string |
Regex pattern for quality-gate tags (e.g., |
At least one pattern (release_pattern or quality_pattern) must be configured per remote.
Validation logic:
For each deployment and environment in the workspace
For each
spec.overrides.remotes[]referenceIf the remote is configured in the policy, check its reference against the declared patterns
Report a violation if the reference matches neither pattern
Example violations:
environment 'production' → remote 'my-service' reference 'main'
does not match expected pattern (release: ^v\d+\.\d+\.\d+$ or quality: ^tested(-\d+)?$)
deployment 'acme' → remote 'tf-landscape' reference 'abc1234f'
does not match expected pattern (release: ^v\d+\.\d+\.\d+$)
sbom_pinned_versions
Evaluates at the build phase, after the SBOM has been generated. Checks that every SBOM component from the selected collectors has an explicit, deterministic version — no missing versions, no floating tags (e.g. :latest, any tag marked strata:tag-stability=floating).
Skipped gracefully when no SBOM components are available.
|
Type |
Default |
Description |
|---|---|---|---|
|
list of string |
all |
Restrict checks to these collector names ( |
|
bool |
|
When |
|
bool |
|
When |
- name: pin_all_images
type: sbom_pinned_versions
phase: build
enforcement: deny
configuration:
collectors: [image, compose]
allow_latest: false
require_digest: false
sbom_allowed_registries
Evaluates at the build phase. Checks that every container image component (pkg:docker/… or pkg:oci/…) originates from one of the configured registry prefixes. Non-docker/OCI purls (Helm charts, Python packages, etc.) are ignored.
Skipped gracefully when no SBOM components are available or when allowed is not configured.
|
Type |
Default |
Description |
|---|---|---|---|
|
list of string |
(required) |
Registry prefix strings — e.g. |
|
list of string |
|
Restrict checks to these collector names |
Single-segment image names (e.g. pkg:docker/nginx@1.25) are resolved to docker.io/library.
- name: trusted_registries
type: sbom_allowed_registries
phase: build
enforcement: deny
configuration:
allowed:
- ghcr.io/myorg
- mcr.microsoft.com
- docker.io/library
sbom_denied_packages
Evaluates at the build phase. Blocks any SBOM component whose purl or name matches one of the configured glob patterns (Python fnmatch syntax). Use this for emergency CVE responses, deprecated package blocks, or license violations.
Skipped gracefully when no SBOM components are available or when denied is not configured.
|
Type |
Default |
Description |
|---|---|---|---|
|
list of string |
(required) |
purl glob patterns ( |
|
string |
|
Custom message appended to each violation |
- name: block_vulnerable
type: sbom_denied_packages
phase: build
enforcement: deny
configuration:
denied:
- "pkg:pypi/setuptools@<70.0.0"
- "pkg:npm/event-stream@*"
- "log4j*"
reason: "Blocked by security policy SEC-2026-041"
sbom_max_components
Evaluates at the build phase. Enforces an upper bound on the total number of SBOM components and optionally per-collector limits. Useful as a complexity budget to detect dependency sprawl before it ships.
Skipped gracefully when no SBOM components are available or when neither max_count nor per_collector is configured.
|
Type |
Default |
Description |
|---|---|---|---|
|
int |
none |
Maximum total component count across all collectors |
|
map string → int |
none |
Per-collector limits keyed by collector name |
- name: complexity_budget
type: sbom_max_components
phase: build
enforcement: warn
configuration:
max_count: 200
per_collector:
image: 50
deps: 150
sbom_license
Evaluates at the build phase. Checks each component’s strata:license property against configurable allow and deny lists using fnmatch glob patterns. Useful for enforcing open-source license compliance without external API calls — works entirely offline with data already in the SBOM.
Collectors or lockfile parsers that capture license metadata set properties["strata:license"] = "MIT" on their SbomComponentModel. Components without this property are governed by the unknown_action setting.
Skipped gracefully when no SBOM components are available or when neither allowed nor denied is configured.
|
Type |
Default |
Description |
|---|---|---|---|
|
list of string |
none |
SPDX license globs — only these are permitted (e.g. |
|
list of string |
none |
SPDX license globs — these are always blocked (e.g. |
|
string |
|
What to do when |
When both allowed and denied are configured, denied is checked first — an explicit deny always wins.
- name: license_compliance
type: sbom_license
phase: build
enforcement: deny
configuration:
allowed:
- MIT
- Apache-2.0
- BSD-*
- ISC
denied:
- GPL-3.0-only
- AGPL-*
unknown_action: warn
cost_threshold
Evaluates at the plan phase. Reads cost.json from the deployment build directory
(generated by strata cost show) and checks that the total estimated monthly cost
does not exceed a configured maximum.
Skipped gracefully when:
cost.jsondoes not exist (runstrata cost show -f deploy.yamlfirst)max_monthlyis not configuredThe total cost is zero or unparseable
When environment_pattern is set, the policy only activates for environments whose
name matches the glob pattern — useful for applying different thresholds per tier.
|
Type |
Default |
Description |
|---|---|---|---|
|
number |
— |
Maximum allowed monthly cost. Required. |
|
string |
|
Currency label used in violation messages (informational — not used for conversion). |
|
string |
none |
Glob pattern for environment names. Policy is skipped for non-matching environments. |
Prerequisites: strata cost show must run before strata deploy to generate cost.json.
The policy reads the pre-computed cost file — it does not call Infracost itself.
# Block production deployments over €10,000/month
- name: prod_cost_gate
type: cost_threshold
phase: plan
enforcement: deny
description: "Block deployments with monthly cost over €10,000"
configuration:
max_monthly: 10000
currency: EUR
# Warn when dev environments exceed €500/month
- name: dev_cost_cap
type: cost_threshold
phase: plan
enforcement: warn
configuration:
max_monthly: 500
environment_pattern: "dev*"
currency: EUR
Enforcement Levels
Level |
Behaviour on failure |
|---|---|
|
Stops the pipeline immediately. Exit code |
|
Logs a warning to the console and continues. The pipeline completes normally. |
|
Records the result in the deployment manifest only. No console output unless |
Use deny for hard constraints (compliance, security, data residency). Use warn for advisory checks during a rollout period. Use audit to collect baseline data before deciding whether to enforce.
Phases
Phase |
Triggered by |
When it runs |
|---|---|---|
|
|
After Pydantic structural validation and cross-reference checks pass |
|
|
After the platform artifact is generated |
|
|
After |
|
|
After |
The plan phase has the highest impact: it sits between planning and applying, giving policies access to the full Terraform plan JSON. A deny at this phase prevents any infrastructure change from being made.
Example Configuration
Single policy — zone enforcement
apiVersion: strata.huybrechts.xyz/v1
kind: configuration
meta:
name: production
annotations:
description: Production environment configuration
spec:
zones:
eu-west:
regions:
- westeurope
- northeurope
eu-central:
regions:
- germanywestcentral
- swedencentral
policies:
- name: zone_enforcement
type: customer_zone
phase: plan
enforcement: deny
description: "All provisioned resources must be in customer-allowed zones"
With this configuration, running strata deploy run will evaluate every resource in the Terraform plan against the customer’s declared zones. If any resource targets us-east-1 or another region outside the allowed set, the deploy stops before apply with exit code 3.
See Also
Lifecycles — lifecycle hooks for running scripts at named pipeline points
Validators — structural validation of individual YAML files
Exit Codes —
3= validation failure (policydenyfailure)ADR 0006 — design decisions behind the policy engine