Pulumi CrossGuard: Policy-as-Code Testing for Infrastructure

Pulumi CrossGuard: Policy-as-Code Testing for Infrastructure

Unit tests verify that your Pulumi code produces the resources you intend. Policy-as-code verifies that those resources comply with organizational standards — encryption enabled, tags present, no public S3 buckets, required IAM conditions. Pulumi CrossGuard handles the second concern.

CrossGuard policies run on every pulumi up and pulumi preview. They can warn or block deployments. They compose into policy packs that you publish organization-wide, so a single enforcement rule covers every stack your team owns.

How CrossGuard Works

CrossGuard intercepts the resource graph at preview time, before any cloud API calls. Each policy receives the resource's input properties (what you're requesting) and output properties (what the cloud returns). The policy validates both.

pulumi up
  → Pulumi engine generates resource graph
  → CrossGuard evaluates each resource against registered policies
  → mandatory/advisory violations reported
  → if any mandatory violations: deployment blocked
  → if only advisory violations: deployment proceeds with warnings

Two enforcement levels:

  • advisory: warns, does not block
  • mandatory: blocks the deployment if the policy fails

Setting Up a Policy Pack

Install the CrossGuard SDK:

mkdir my-policy-pack
<span class="hljs-built_in">cd my-policy-pack
npm init -y
npm install @pulumi/policy @pulumi/pulumi

Create index.ts:

import { PolicyPack, validateResourceOfType } from "@pulumi/policy";
import * as aws from "@pulumi/aws";

new PolicyPack("company-aws-policies", {
    policies: [
        {
            name: "s3-no-public-access",
            description: "S3 buckets must block all public access.",
            enforcementLevel: "mandatory",
            validateResource: validateResourceOfType(
                aws.s3.BucketPublicAccessBlock,
                (bucket, args, reportViolation) => {
                    if (!bucket.blockPublicAcls ||
                        !bucket.blockPublicPolicy ||
                        !bucket.ignorePublicAcls ||
                        !bucket.restrictPublicBuckets) {
                        reportViolation(
                            "S3 bucket must have all public access blocks enabled."
                        );
                    }
                }
            ),
        },
    ],
});

Run the policy pack against a stack:

pulumi preview --policy-pack ./my-policy-pack
pulumi up --policy-pack ./my-policy-pack

Writing Policies for Common Infrastructure Rules

Required Tags

import { ResourceValidationPolicy } from "@pulumi/policy";

const requiredTagsPolicy: ResourceValidationPolicy = {
    name: "required-tags",
    description: "Resources must have Environment, Team, and CostCenter tags.",
    enforcementLevel: "mandatory",
    validateResource: (args, reportViolation) => {
        // Only check resources that support tags
        if (!args.props.tags) return;

        const required = ["Environment", "Team", "CostCenter"];
        const tags = args.props.tags as Record<string, string>;

        for (const tag of required) {
            if (!tags[tag]) {
                reportViolation(
                    `Resource ${args.urn} is missing required tag: ${tag}`
                );
            }
        }
    },
};

Encryption at Rest

import { validateResourceOfType } from "@pulumi/policy";
import * as aws from "@pulumi/aws";

const rdsEncryptionPolicy = {
    name: "rds-encryption-required",
    description: "RDS instances must have storage encryption enabled.",
    enforcementLevel: "mandatory",
    validateResource: validateResourceOfType(
        aws.rds.Instance,
        (instance, args, reportViolation) => {
            if (!instance.storageEncrypted) {
                reportViolation(
                    "RDS instance must have storageEncrypted set to true."
                );
            }
            if (!instance.kmsKeyId) {
                reportViolation(
                    "RDS instance must use a customer-managed KMS key."
                );
            }
        }
    ),
};

EC2 IMDSv2 Enforcement

const imdsv2Policy = {
    name: "ec2-imdsv2-required",
    description: "EC2 instances must require IMDSv2 (token-based metadata).",
    enforcementLevel: "mandatory",
    validateResource: validateResourceOfType(
        aws.ec2.Instance,
        (instance, args, reportViolation) => {
            const metadata = instance.metadataOptions;
            if (!metadata || metadata.httpTokens !== "required") {
                reportViolation(
                    "EC2 instance must set metadataOptions.httpTokens to 'required' (IMDSv2)."
                );
            }
        }
    ),
};

Testing Policy Packs

CrossGuard ships with a testing framework so you can unit test policies without deploying real infrastructure.

import { PolicyPackTests, runTests } from "@pulumi/policy/testing";

const tests: PolicyPackTests = {
    "s3-no-public-access": [
        {
            description: "Fails when blockPublicAcls is false",
            resource: {
                type: "aws:s3/bucketPublicAccessBlock:BucketPublicAccessBlock",
                props: {
                    blockPublicAcls: false,
                    blockPublicPolicy: true,
                    ignorePublicAcls: true,
                    restrictPublicBuckets: true,
                },
            },
            expectViolations: ["S3 bucket must have all public access blocks enabled."],
        },
        {
            description: "Passes when all blocks are enabled",
            resource: {
                type: "aws:s3/bucketPublicAccessBlock:BucketPublicAccessBlock",
                props: {
                    blockPublicAcls: true,
                    blockPublicPolicy: true,
                    ignorePublicAcls: true,
                    restrictPublicBuckets: true,
                },
            },
            expectViolations: [],
        },
    ],
};

runTests(tests);

Run tests:

npm test
<span class="hljs-comment"># or
pulumi policy <span class="hljs-built_in">test

This runs policies against mock resource inputs — no cloud credentials needed. It's fast and appropriate for CI on every PR.

Stack-Level Policies (validateStack)

Some policies require looking at the entire resource graph, not individual resources. Use validateStack for rules like "every EC2 instance must have an associated security group" or "no VPC should have more than 10 attached subnets."

import { StackValidationPolicy } from "@pulumi/policy";
import * as aws from "@pulumi/aws";

const noDefaultVPCPolicy: StackValidationPolicy = {
    name: "no-default-vpc-usage",
    description: "Stacks must not use the default VPC.",
    enforcementLevel: "mandatory",
    validateStack: (args, reportViolation) => {
        // Find all EC2 instances in the stack
        const instances = args.resources.filter(
            r => r.type === "aws:ec2/instance:Instance"
        );

        for (const instance of instances) {
            // Check if subnet belongs to default VPC (heuristic: default subnets have
            // CIDRs in 172.31.0.0/16)
            const subnetId = instance.props.subnetId as string;
            if (!subnetId) {
                reportViolation(
                    `EC2 instance ${instance.urn} has no explicit subnet — may be in default VPC.`
                );
            }
        }
    },
};

Remediations (Auto-Fix)

CrossGuard supports automatic remediation — policies that don't just report violations but fix them by mutating resource properties before deployment.

import { remediateResourceOfType } from "@pulumi/policy";

const enforceTagsPolicy = {
    name: "enforce-default-tags",
    description: "Automatically adds ManagedBy tag if missing.",
    enforcementLevel: "advisory",
    remediateResource: remediateResourceOfType(
        aws.ec2.Instance,
        (instance) => {
            const tags = (instance.tags as Record<string, string>) || {};
            if (!tags["ManagedBy"]) {
                return {
                    ...instance,
                    tags: { ...tags, ManagedBy: "pulumi" },
                };
            }
        }
    ),
};

Remediations run with pulumi up --policy-pack ./my-policy-pack. The mutated props are applied without prompting the user.

Publishing Policy Packs to Pulumi Cloud

Instead of passing --policy-pack on every command, publish policy packs to the Pulumi Cloud organization and enforce them on all stacks automatically.

# Publish the policy pack
pulumi policy publish

<span class="hljs-comment"># Enable for all stacks in the organization (default enforcement level)
pulumi policy <span class="hljs-built_in">enable company-aws-policies latest

<span class="hljs-comment"># Override enforcement level for a specific stack
pulumi policy <span class="hljs-built_in">enable company-aws-policies latest \
  --stack my-org/my-project/production \
  --config <span class="hljs-string">'{"s3-no-public-access": {"enforcementLevel": "mandatory"}}'

Once published and enabled, every pulumi preview and pulumi up across your organization automatically runs the policy checks — no per-stack configuration needed.

Policy Configuration

Make policies configurable without rewriting them. Use PolicyConfigSchema to define parameters:

import { PolicyPack, validateResourceOfType, PolicyConfigSchema } from "@pulumi/policy";

const s3RetentionPolicy = {
    name: "s3-minimum-retention",
    description: "S3 lifecycle rules must retain objects for minimum days.",
    enforcementLevel: "advisory",
    configSchema: {
        properties: {
            minimumRetentionDays: {
                type: "integer",
                default: 30,
            },
        },
    } as PolicyConfigSchema,
    validateResource: validateResourceOfType(
        aws.s3.BucketLifecycleConfigurationV2,
        (bucket, args, reportViolation) => {
            const config = args.getConfig<{ minimumRetentionDays: number }>();
            const minDays = config.minimumRetentionDays ?? 30;

            // Check expiration rules
            const rules = bucket.rules || [];
            for (const rule of rules) {
                if (rule.expiration?.days && rule.expiration.days < minDays) {
                    reportViolation(
                        `Lifecycle rule expires objects after ${rule.expiration.days} days, ` +
                        `minimum is ${minDays} days.`
                    );
                }
            }
        }
    ),
};

Configure at enable time:

pulumi policy enable company-aws-policies latest \
  --config <span class="hljs-string">'{"s3-minimum-retention": {"minimumRetentionDays": 90}}'

CI Pipeline Integration

# .github/workflows/pulumi-policy.yml
name: Pulumi Policy Check
on:
  pull_request:
    paths:
      - 'infrastructure/**'
      - 'policy-packs/**'

jobs:
  policy-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - name: Install policy pack deps
        run: cd policy-packs/company-aws && npm ci
      - name: Run policy unit tests
        run: cd policy-packs/company-aws && npm test
      - name: Preview with policies
        run: |
          cd infrastructure
          pulumi preview \
            --policy-pack ../policy-packs/company-aws \
            --stack preview-env
        env:
          PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

CrossGuard vs Checkov for Pulumi

Both tools can scan Pulumi programs, but they operate differently:

Aspect CrossGuard Checkov
Language TypeScript/JavaScript Python
Runs at pulumi preview / pulumi up Pre-deployment, on files
Access to Full resource graph + config Static analysis only
Remediations Yes (auto-fix props) No
Organization enforcement Yes (Pulumi Cloud) Requires separate CI config
Custom logic Full TypeScript, any library Python class or YAML

CrossGuard is the right choice when you're already on Pulumi and want integrated enforcement. Checkov is better for broad multi-tool environments or file-based scanning in git workflows.

Summary

CrossGuard adds policy enforcement to your Pulumi deployment pipeline:

  1. Write policies in TypeScript with validateResource or validateStack
  2. Test them locally with mock resources — no cloud needed
  3. Publish to Pulumi Cloud to enforce organization-wide
  4. Use remediateResource for auto-fixing non-critical violations
  5. Make policies configurable to work across multiple environments

The enforcement happens before any cloud API call, making CrossGuard one of the fastest feedback loops in the IaC testing pyramid.

Read more