Skip to content

Policy as Code in ThothCTL

Overview

ThothCTL integrates policy-as-code at multiple layers of the IaC lifecycle. Policies define what is allowed, enforced, or blocked — from project structure to security posture to drift response to AI decision-making.

%%{init: {'theme':'base', 'themeVariables': {
  'primaryColor':'#3b82f6',
  'primaryTextColor':'#ffffff',
  'primaryBorderColor':'#2563eb',
  'lineColor':'#94a3b8',
  'secondaryColor':'#10b981',
  'tertiaryColor':'#8b5cf6',
  'background':'transparent',
  'mainBkg':'#3b82f6',
  'secondBkg':'#10b981',
  'tertiaryBkg':'#8b5cf6',
  'clusterBkg':'rgba(241, 245, 249, 0.05)',
  'clusterBorder':'#475569',
  'titleColor':'currentColor',
  'edgeLabelBackground':'transparent',
  'nodeTextColor':'#ffffff',
  'textColor':'currentColor',
  'nodeBorder':'#1e293b',
  'fontSize':'14px'
}}}%%
graph TB
    subgraph Structure["📐 Structure Policy"]
        S1[".thothcf.toml<br/>[project_structure]"]
        S2["check project iac"]
    end

    subgraph Security["🔒 Security Policy"]
        P1["policy/*.rego<br/>(OPA / Conftest)"]
        P2["scan iac --tools opa"]
    end

    subgraph Operations["⚙️ Operations Policy"]
        O1[".driftpolicy<br/>ai_decision_config.yaml"]
        O2["check iac --type drift<br/>ai-review decide"]
    end

    S1 --> S2
    P1 --> P2
    O1 --> O2

    S2 --> Gate{"Policy Gate"}
    P2 --> Gate
    O2 --> Gate

    Gate -->|All pass| Deploy["✅ Deploy"]
    Gate -->|Violation| Block["❌ Block"]

    classDef structStyle fill:#3b82f6,stroke:#60a5fa,stroke-width:2px,color:#fff
    classDef secStyle fill:#8b5cf6,stroke:#a78bfa,stroke-width:2px,color:#fff
    classDef opsStyle fill:#10b981,stroke:#34d399,stroke-width:2px,color:#fff
    classDef gateStyle fill:#f59e0b,stroke:#fbbf24,stroke-width:2px,color:#fff
    classDef resultStyle fill:#15803d,stroke:#4ade80,stroke-width:2px,color:#ffffff
    classDef blockStyle fill:#dc2626,stroke:#f87171,stroke-width:2px,color:#ffffff

    class S1,S2 structStyle
    class P1,P2 secStyle
    class O1,O2 opsStyle
    class Gate gateStyle
    class Deploy resultStyle
    class Block blockStyle

Policy Types

1. Project Structure Policy

File: .thothcf.toml[project_structure]
Evaluated by: thothctl check project iac
Purpose: Enforce that projects follow organizational folder/file conventions.

[project_structure]
root_files = [".gitignore", "README.md", ".thothcf.toml"]
ignore_folders = [".git", ".terraform", ".terragrunt-cache"]

[[project_structure.folders]]
name = "modules"
mandatory = true
type = "root"
content = ["main.tf", "variables.tf", "outputs.tf"]

[[project_structure.folders]]
name = "environments"
mandatory = true
type = "root"

Enforcement: Hard failure if mandatory folders/files are missing.

thothctl check project iac
# ✅ modules/ exists with required files
# ❌ FAIL: environments/ is missing (mandatory)

2. Security Policy (OPA/Rego)

File: policy/*.rego
Evaluated by: thothctl scan iac --tools opa
Purpose: Evaluate IaC code against custom security, naming, and compliance rules using the OPA policy language.

Modes: - conftest (default): Static analysis of .tf / .yaml files - opa: Plan-based evaluation against tfplan.json

# policy/s3.rego
package main

deny[msg] {
    resource := input.resource.aws_s3_bucket[name]
    not resource.server_side_encryption_configuration
    msg := sprintf("S3 bucket '%s' must have encryption enabled", [name])
}

deny[msg] {
    resource := input.resource.aws_s3_bucket[name]
    resource.acl == "public-read"
    msg := sprintf("S3 bucket '%s' must not be public", [name])
}

Enforcement:

# Static HCL analysis (conftest mode)
thothctl scan iac --tools opa

# Plan-based evaluation
thothctl scan iac --tools opa --opa-mode opa --policy-dir ./policy

Options:

Option Description
--opa-mode conftest Static file analysis (default)
--opa-mode opa Plan-based evaluation via opa exec
--policy-dir PATH Directory containing .rego files (default: policy/)
--opa-namespace Rego namespace (conftest mode)
--opa-data-dir Additional data directory for policies

3. Drift Response Policy

File: .driftpolicy (YAML)
Evaluated by: thothctl check iac --type drift
Purpose: Define per-resource tolerance for infrastructure drift — block, alert, accept, or ignore.

# .driftpolicy
coverage_threshold: 90.0

rules:
  - resource: "aws_security_group.*"
    severity_override: critical
    action: block_deploy

  - resource: "aws_instance.*"
    attribute: "tags.*"
    action: auto_accept

  - resource: "aws_db_instance.*"
    action: alert

  - resource: "aws_cloudwatch_log_group.*"
    action: ignore

Actions:

Action Behavior
block_deploy Fail CI, prevent deployment until drift is resolved
alert Warn but allow deployment
auto_accept Silently accept the drift (e.g., tag-only changes)
ignore Remove from report entirely

Enforcement:

thothctl check iac --type drift --recursive
# Drift in aws_security_group.api → ACTION: block_deploy → ❌ CI fails
# Drift in aws_instance.web tags  → ACTION: auto_accept → ✅ Ignored

4. AI Decision Policy

File: .thothctl/ai_decision_config.yaml
Evaluated by: thothctl ai-review decide
Purpose: Define thresholds for automated PR approve/reject/request-changes decisions.

# .thothctl/ai_decision_config.yaml
approve_thresholds:
  risk_score_max: 20
  confidence_min: 0.90
  critical_issues_max: 0
  high_issues_max: 0
  compliance_violations_max: 0

reject_thresholds:
  risk_score_min: 85
  confidence_min: 0.85
  critical_issues_min: 1

safety:
  max_auto_approvals_per_day: 50
  max_auto_rejections_per_day: 20
  cooldown_between_actions: 300  # seconds
  emergency_labels: ["emergency", "hotfix", "security-patch"]
  trusted_bots: ["dependabot", "renovate"]

blocking_patterns:
  - hardcoded_secrets
  - public_s3_buckets
  - unencrypted_databases
  - overly_permissive_iam

Enforcement:

thothctl ai-review decide --pr-number 42 --dry-run
# Risk score: 15 → below approve_thresholds.risk_score_max (20)
# Confidence: 0.95 → above confidence_min (0.90)
# Decision: APPROVE ✅

Policy Hierarchy

Policies are resolved in this order (most specific wins):

%%{init: {'theme':'base', 'themeVariables': {
  'primaryColor':'#3b82f6',
  'primaryTextColor':'#ffffff',
  'primaryBorderColor':'#2563eb',
  'lineColor':'#94a3b8',
  'secondaryColor':'#10b981',
  'tertiaryColor':'#8b5cf6',
  'background':'transparent',
  'mainBkg':'#3b82f6',
  'secondBkg':'#10b981',
  'tertiaryBkg':'#8b5cf6',
  'clusterBkg':'rgba(241, 245, 249, 0.05)',
  'clusterBorder':'#475569',
  'titleColor':'currentColor',
  'edgeLabelBackground':'transparent',
  'nodeTextColor':'#ffffff',
  'textColor':'currentColor',
  'nodeBorder':'#1e293b',
  'fontSize':'14px'
}}}%%
graph TB
    Org["🏢 Organization<br/>Git Policy Repository"] --> Space["🗂️ Space<br/>.thothcf_project.toml"]
    Space --> Project["📁 Project<br/>.thothcf.toml"]

    Org -->|"Inherited by all"| Space
    Space -->|"Override per team"| Project

    classDef orgStyle fill:#8b5cf6,stroke:#a78bfa,stroke-width:2px,color:#fff
    classDef spaceStyle fill:#3b82f6,stroke:#60a5fa,stroke-width:2px,color:#fff
    classDef projStyle fill:#10b981,stroke:#34d399,stroke-width:2px,color:#fff

    class Org orgStyle
    class Space spaceStyle
    class Project projStyle
Level Source Scope
Organization Git repository (shared across all teams) Global governance baseline
Space .thothcf_project.toml in space root Team/domain-specific overrides
Project .thothcf.toml in project root Project-specific exceptions

Resolution rule: Project-level policies override Space-level, which override Organization-level. Structure policies are replaced entirely (no merge). Security policies (Rego) are additive (all levels evaluated).


Organization Policy Repository

Organization-level policies live in a dedicated Git repository that acts as the single source of truth for governance across all teams, domains, and workloads.

Repository Structure

org-iac-policies/
├── README.md
├── .thothcf.toml                    # Org-level defaults
├── domains/                          # Policies per business domain
│   ├── fintech/
│   │   ├── policy/                   # Rego policies for fintech
│   │   │   ├── encryption.rego       # PCI-DSS encryption requirements
│   │   │   ├── network.rego          # No public subnets
│   │   │   └── data.rego             # Data residency rules
│   │   ├── .thothcf.toml            # Structure rules for fintech projects
│   │   └── .driftpolicy             # Strict drift tolerance
│   │
│   ├── platform/
│   │   ├── policy/
│   │   │   ├── naming.rego           # Platform naming conventions
│   │   │   └── modules.rego          # Approved modules only
│   │   └── .thothcf.toml
│   │
│   └── data-engineering/
│       ├── policy/
│       │   ├── storage.rego          # S3/Glue/Redshift rules
│       │   └── compute.rego          # EMR/Spark guardrails
│       └── .thothcf.toml
├── workloads/                        # Policies per workload type
│   ├── containers/
│   │   ├── policy/
│   │   │   ├── ecs.rego              # ECS task hardening
│   │   │   └── eks.rego              # EKS cluster policies
│   │   └── .thothcf.toml
│   │
│   ├── serverless/
│   │   ├── policy/
│   │   │   ├── lambda.rego           # Lambda function constraints
│   │   │   └── api_gateway.rego      # API Gateway policies
│   │   └── .thothcf.toml
│   │
│   └── databases/
│       ├── policy/
│       │   ├── rds.rego              # Multi-AZ, encryption, backup
│       │   └── dynamodb.rego         # Capacity and encryption
│       └── .thothcf.toml
├── layers/                           # Policies per infrastructure layer
│   ├── networking/
│   │   ├── policy/
│   │   │   ├── vpc.rego              # VPC CIDR, flow logs
│   │   │   ├── security_groups.rego  # No 0.0.0.0/0 ingress
│   │   │   └── dns.rego              # Route53 conventions
│   │   └── .thothcf.toml
│   │
│   ├── security/
│   │   ├── policy/
│   │   │   ├── iam.rego              # Least privilege, no wildcard
│   │   │   ├── kms.rego              # Key rotation, deletion protection
│   │   │   └── secrets.rego          # Secrets Manager policies
│   │   └── .thothcf.toml
│   │
│   └── observability/
│       ├── policy/
│       │   ├── cloudwatch.rego       # Required alarms per service
│       │   └── logging.rego          # Log retention minimums
│       └── .thothcf.toml
├── compliance/                       # Framework-specific compliance mappings
│   ├── soc2/
│   │   ├── policy/
│   │   │   └── soc2_controls.rego    # SOC2 control enforcement
│   │   └── mapping.yaml             # Finding → SOC2 control mapping
│   ├── cis-aws/
│   │   └── policy/
│   │       └── cis_benchmark.rego
│   └── iso27001/
│       └── policy/
│           └── iso_controls.rego
└── shared/                           # Shared policies (applied everywhere)
    ├── policy/
    │   ├── tagging.rego              # Required tags for all resources
    │   ├── regions.rego              # Allowed regions
    │   └── cost_controls.rego        # Instance size limits
    ├── .driftpolicy                  # Default drift tolerance
    └── ai_decision_config.yaml       # Default AI decision thresholds

How Projects Consume Organization Policies

ThothCTL resolves organization policies from a configured Git repository or local path. The policy repository can be set via:

Option 1: Space initialization

thothctl init space -s my-space --policy-repo https://github.com/my-org/org-iac-policies.git

This stores governance.policy_repo in ~/.thothcf/spaces.toml.

Option 2: Environment variable

export THOTH_POLICY_REPO=/path/to/local/clone
# or
export THOTH_POLICY_REPO=https://github.com/my-org/org-iac-policies.git

Option 3: Direct path on scan command

# Point to a specific subfolder within your org repo
thothctl scan iac --tools opa --policy-dir layers/networking/policy

# Or absolute path
thothctl scan iac --tools opa --policy-dir /path/to/org-iac-policies/domains/fintech/policy

Policy Resolution Order

When thothctl scan iac --tools opa runs, the OPA scanner resolves policies in this order:

%%{init: {'theme':'base', 'themeVariables': {
  'primaryColor':'#3b82f6',
  'primaryTextColor':'#ffffff',
  'primaryBorderColor':'#2563eb',
  'lineColor':'#94a3b8',
  'secondaryColor':'#10b981',
  'tertiaryColor':'#8b5cf6',
  'background':'transparent',
  'mainBkg':'#3b82f6',
  'secondBkg':'#10b981',
  'tertiaryBkg':'#8b5cf6',
  'clusterBkg':'rgba(241, 245, 249, 0.05)',
  'clusterBorder':'#475569',
  'titleColor':'currentColor',
  'edgeLabelBackground':'transparent',
  'nodeTextColor':'#ffffff',
  'textColor':'currentColor',
  'nodeBorder':'#1e293b',
  'fontSize':'14px'
}}}%%
graph TD
    A["thothctl scan iac --tools opa<br/>--policy-dir <path>"] --> B{"Project has<br/>local policy/?"}
    B -->|Yes| C["✅ Use project/policy/"]
    B -->|No| D{"Is absolute<br/>path?"}
    D -->|Yes| E["✅ Use absolute path"]
    D -->|No| F{"THOTH_POLICY_REPO<br/>set?"}
    F -->|No| G["❌ No policy found<br/>(skip scan)"]
    F -->|Yes| H{"path exists in<br/>org repo?"}
    H -->|Yes| I["✅ Use org_repo/<path>"]
    H -->|No| J{"org_repo/shared/<br/>policy exists?"}
    J -->|Yes| K["✅ Use org_repo/shared/policy"]
    J -->|No| G

    classDef found fill:#10b981,stroke:#34d399,stroke-width:2px,color:#fff
    classDef notfound fill:#991b1b,stroke:#f87171,stroke-width:2px,color:#fff
    classDef decision fill:#3b82f6,stroke:#60a5fa,stroke-width:2px,color:#fff

    class C,E,I,K found
    class G notfound
    class B,D,F,H,J decision

Resolution examples:

--policy-dir value THOTH_POLICY_REPO Resolved path
policy (default) not set <project>/policy/
policy (default) /path/to/org-repo <org-repo>/shared/policy (fallback)
layers/networking/policy /path/to/org-repo <org-repo>/layers/networking/policy
domains/fintech/policy /path/to/org-repo <org-repo>/domains/fintech/policy
workloads/databases/policy /path/to/org-repo <org-repo>/workloads/databases/policy
/absolute/path/to/policies (ignored) /absolute/path/to/policies

Configuration in .thothcf.toml

Projects declare which domain, workload, and layer they belong to. This determines which org policies apply when scanning:

[thothcf]
project_id = "payment-service"
project_type = "terraform-terragrunt"

# Policy selectors — determines which org policy subfolder to use
# with: thothctl scan iac --tools opa --policy-dir domains/fintech/policy
[thothcf.governance]
domain = "fintech"
workload = "containers"
layer = "networking"
compliance = ["soc2", "cis-aws"]

Note: Automatic policy selection based on [thothcf.governance] selectors is on the roadmap. Currently, you specify the subfolder explicitly via --policy-dir or use the shared/policy fallback.

Space Configuration

When a space is initialized with --policy-repo, the governance config is stored in ~/.thothcf/spaces.toml:

[spaces.my-space.governance]
policy_repo = "https://github.com/my-org/org-iac-policies.git"

Example: Organization Policy Repository

A reference implementation is available as a GitHub template:

🔗 thothforge/org-iac-policies — Example organization policy repository with pre-built policies for AWS, naming conventions, tagging, and compliance frameworks.

# Use as a template for your organization
gh repo create my-org/iac-policies --template thothforge/org-iac-policies

Integration Points

Where policies are evaluated in the workflow

Developer writes IaC
       ├──► thothctl check project iac        → Structure policy
       ├──► thothctl scan iac --tools opa     → Security policy (Rego)
       ├──► thothctl scan iac --tools checkov → Built-in CIS/AWS rules
       ├──► thothctl check iac --type drift   → Drift policy
       └──► thothctl ai-review decide         → Decision policy
                                                 (uses scan results as input)

CI/CD Integration Example

# .github/workflows/iac-policy.yml
name: IaC Policy Gate

on: [pull_request]

jobs:
  policy-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install ThothCTL
        run: pip install thothctl

      # Layer 1: Structure
      - name: Check project structure
        run: thothctl check project iac

      # Layer 2: Security (OPA + Checkov)
      - name: Security scan with policies
        run: |
          thothctl scan iac --tools checkov opa --recursive

      # Layer 3: Drift (if state available)
      - name: Drift detection
        run: thothctl check iac --type drift --recursive

      # Layer 4: AI decision
      - name: AI review decision
        run: |
          thothctl ai-review decide \
            --pr-number ${{ github.event.pull_request.number }} \
            --repository ${{ github.repository }}

Writing Custom Policies

OPA/Rego Quick Start

  1. Create a policy/ directory in your project root:
mkdir policy
  1. Write a Rego policy:
# policy/naming.rego
package main

# Enforce resource naming convention
deny[msg] {
    resource := input.resource[type][name]
    not regex.match(`^(dev|stg|prd)-[a-z]+-[a-z0-9-]+$`, name)
    msg := sprintf(
        "Resource '%s.%s' violates naming convention: must be '{env}-{service}-{name}'",
        [type, name]
    )
}
  1. Run:
thothctl scan iac --tools opa --policy-dir policy

Policy Examples

Policy File What It Enforces
Encryption required policy/encryption.rego All S3/RDS/EBS must have encryption
No public access policy/network.rego Security groups can't allow 0.0.0.0/0 ingress
Tag compliance policy/tags.rego Required tags: Environment, Owner, CostCenter
Module versioning policy/modules.rego All modules must pin exact versions
Region restriction policy/regions.rego Only approved regions allowed

Relationship to FdI (Framework-defined Infrastructure)

In the FdI model, policies become the framework rules that govern code generation:

Today:  Developer writes IaC → policies validate after the fact
Future: Policies constrain generation → code is compliant by construction

The intent-to-IaC generation engine (roadmap) will read these same policy files to produce code that already passes all checks:

Policy Layer FdI Role
Structure ([project_structure]) Generated code follows the required structure
Security (policy/*.rego) Generated code uses only approved patterns
Drift (.driftpolicy) Generated code accounts for drift tolerance
AI decisions (ai_decision_config) Auto-approve low-risk generated changes