opa-policies.md9.6 KB
View on GitHubOPA Policy Reference for Atmos
Rego Fundamentals for Atmos
All Atmos OPA policies must use the package atmos declaration and define errors rules.
Atmos evaluates all rules and collects error messages from the errors set. If any error
messages are present, validation fails.
Minimal Policy Structure
# Required package declarationpackage atmos# Optional importsimport future.keywords.in# Error rules: each rule adds a message to the errors set when its conditions are trueerrors[message] {# conditions...message = "Error description"}
Input Structure
The input object contains the full component configuration. Key fields:
Core Component Configuration
input.vars # Component variables (the values passed to Terraform)input.settings # Component settingsinput.env # Environment variablesinput.backend # Backend configurationinput.backend_type # Backend type (e.g., "s3")input.metadata # Component metadatainput.metadata.component # The Terraform component nameinput.workspace # Terraform workspace name
Context Variables
input.vars.namespace # Organization namespaceinput.vars.tenant # Organizational unitinput.vars.environment # Region/environment identifierinput.vars.stage # Account/stage identifierinput.vars.name # Component instance nameinput.vars.tags # Resource tags map
Execution Context (available during plan/apply)
input.process_env # Map of OS environment variablesinput.cli_args # List of CLI arguments (e.g., ["terraform", "plan"])input.tf_cli_vars # Map from -var arguments with type conversioninput.env_tf_cli_args # List from TF_CLI_ARGS env varinput.env_tf_cli_vars # Map from TF_CLI_ARGS -var values with type conversion
Writing Deny Rules
Simple Field Validation
package atmos# Deny if a required variable is missingerrors[message] {not input.vars.regionmessage = "The 'region' variable is required"}# Deny if a boolean is incorrectly set for the environmenterrors[message] {input.vars.stage == "prod"input.vars.map_public_ip_on_launch == truemessage = "Public IP mapping on launch is not allowed in production"}
Numeric Range Validation
package atmoserrors[message] {input.vars.instance_count > 10message = sprintf("instance_count cannot exceed 10, got %d", [input.vars.instance_count])}errors[message] {input.vars.stage == "prod"input.vars.min_size < 2message = sprintf("Production requires min_size >= 2, got %d", [input.vars.min_size])}
String Pattern Validation
package atmos# Validate naming conventionserrors[message] {not re_match("^[a-z][a-z0-9-]*$", input.vars.name)message = sprintf("Name '%s' must be lowercase alphanumeric with hyphens", [input.vars.name])}# Validate CIDR formaterrors[message] {not re_match("^([0-9]{1,3}\\.){3}[0-9]{1,3}/[0-9]{1,2}$", input.vars.cidr_block)message = sprintf("Invalid CIDR block: '%s'", [input.vars.cidr_block])}
Note: Backslashes in regex patterns must be double-escaped: \\. to match a literal dot.
List and Array Validation
package atmos# Validate list lengtherrors[message] {input.vars.stage == "dev"count(input.vars.availability_zones) != 2message = "Dev environment must use exactly 2 availability zones"}# Validate list contentserrors[message] {az := input.vars.availability_zones[_]not startswith(az, input.vars.region)message = sprintf("AZ '%s' does not match region '%s'", [az, input.vars.region])}# Check for prohibited values in a listerrors[message] {cidr := input.vars.allowed_cidr_blocks[_]cidr == "0.0.0.0/0"message = "Open CIDR block 0.0.0.0/0 is not allowed"}
Map and Tag Validation
package atmos# Required tagserrors[message] {required := {"Environment", "Team", "CostCenter", "Project"}missing := required - {key | input.vars.tags[key]}count(missing) > 0message = sprintf("Missing required tags: %v", [missing])}# Tag value constraintserrors[message] {input.vars.tags.Environmentallowed_envs := {"dev", "staging", "prod"}not input.vars.tags.Environment in allowed_envsmessage = sprintf("Invalid Environment tag: '%s'. Must be one of: %v",[input.vars.tags.Environment, allowed_envs])}
Command-Aware Policies
Blocking Apply in Specific Conditions
package atmos# Block apply if a variable has an unsafe valueerrors[message] {count(input.cli_args) >= 2input.cli_args[0] == "terraform"input.cli_args[1] == "apply"input.vars.delete_protection == falseinput.vars.stage == "prod"message = "Cannot apply in prod with delete_protection disabled"}
Environment Variable Requirements
package atmos# Require approval for productionerrors[message] {"apply" in input.cli_argsinput.vars.stage == "prod"not input.process_env.DEPLOYMENT_APPROVEDmessage = "Set DEPLOYMENT_APPROVED=true for production deployments"}# Validate AWS region matches configurationerrors[message] {input.process_env.AWS_REGIONinput.vars.regioninput.process_env.AWS_REGION != input.vars.regionmessage = sprintf("AWS_REGION '%s' does not match configured region '%s'",[input.process_env.AWS_REGION, input.vars.region])}
CLI Variable Validation
package atmos# Block sensitive variables from CLIerrors[message] {sensitive_vars := {"password", "secret", "api_key", "token"}cli_var := sensitive_vars[_]input.tf_cli_vars[cli_var]message = sprintf("Sensitive variable '%s' must not be passed via CLI", [cli_var])}
Modular Policies
Constants Module
# stacks/schemas/opa/catalog/constants/constants.regopackage atmos.constantsmax_dev_instances := 3max_prod_instances := 50required_tags := {"Environment", "Team", "CostCenter"}name_regex := "^[a-z][a-z0-9-]{1,62}[a-z0-9]$"name_error := "Name must be 3-64 chars, lowercase alphanumeric with hyphens"
Using Constants in Policies
# stacks/schemas/opa/vpc/validate-vpc.regopackage atmosimport data.atmos.constants.required_tagsimport data.atmos.constants.name_regeximport data.atmos.constants.name_errorerrors[name_error] {not re_match(name_regex, input.vars.name)}errors[message] {missing := required_tags - {key | input.vars.tags[key]}count(missing) > 0message = sprintf("Missing required tags: %v", [missing])}
Helper Functions Module
# stacks/schemas/opa/catalog/helpers/helpers.regopackage atmos.helpersis_production {input.vars.stage == "prod"}is_development {input.vars.stage == "dev"}has_tag(tag_name) {input.vars.tags[tag_name]}
Example Policies
VPC Component Policy
package atmosimport future.keywords.in# No public IPs in productionerrors[message] {input.vars.stage == "prod"input.vars.map_public_ip_on_launch == truemessage = "Public IPs on launch are not allowed in production"}# Limit AZs in deverrors[message] {input.vars.stage == "dev"count(input.vars.availability_zones) > 2message = "Dev is limited to 2 availability zones"}# Validate CIDR blockerrors[message] {not re_match("^10\\.", input.vars.ipv4_primary_cidr_block)message = "VPC CIDR must be in the 10.0.0.0/8 range"}# Require flow logs in productionerrors[message] {input.vars.stage == "prod"not input.vars.vpc_flow_logs_enabledmessage = "VPC flow logs are required in production"}
EKS Cluster Policy
package atmos# Minimum node count for productionerrors[message] {input.vars.stage == "prod"input.vars.min_node_count < 3message = sprintf("Production EKS requires min 3 nodes, got %d",[input.vars.min_node_count])}# Validate Kubernetes versionerrors[message] {allowed_versions := {"1.28", "1.29", "1.30"}not input.vars.kubernetes_version in allowed_versionsmessage = sprintf("Kubernetes version '%s' is not approved. Use: %v",[input.vars.kubernetes_version, allowed_versions])}# Require encryptionerrors[message] {not input.vars.encryption_config_enabledmessage = "EKS encryption must be enabled"}
Cost Control Policy
package atmos# Block expensive instance types in deverrors[message] {input.vars.stage == "dev"expensive := {"m5.xlarge", "m5.2xlarge", "c5.xlarge", "c5.2xlarge","r5.xlarge", "r5.2xlarge"}input.vars.instance_type in expensivemessage = sprintf("Instance type '%s' is too expensive for dev",[input.vars.instance_type])}# Limit storage in non-productionerrors[message] {input.vars.stage != "prod"input.vars.storage_gb > 100message = sprintf("Non-production storage limited to 100GB, got %d",[input.vars.storage_gb])}
Best Practices
- Always use
sprintffor dynamic error messages with variable interpolation - Use
import future.keywords.infor cleaner set membership checks - Separate constants and helper functions into reusable modules
- Write environment-specific rules using
input.vars.stageorinput.vars.environment - Double-escape backslashes in regex patterns (
\\.not\.) - Test policies with
atmos validate componentbefore deploying to CI/CD - Use
input.cli_argsto create command-aware policies that only apply during plan or apply - Provide actionable error messages that tell the user how to fix the issue