Skip to main content

Pulumi Ep 14: Policy as Code with CrossGuard

Rachmat Hidayat
Author
Rachmat Hidayat
Learn & sharing insights on TypeScript, Go, Kubernetes, DevOps, DevSecOps, SRE, Platform Engineering, AI/ML Engineering, and MLOps.
pulumi - This article is part of a series.
Part 14: This Article
Unit testing (Episode 13) is great for validating logic within a single project. But what if your organization has 50 different Pulumi projects? You need a centralized mechanism to enforce global rules—like “No public S3 buckets” or “All EC2 instances must be tagged.” This is where CrossGuard (Policy-as-Code) shines.

1. What is CrossGuard?
#

CrossGuard is Pulumi’s native Policy-as-Code engine. It allows Platform/Security Engineers to author rules using standard TypeScript or Python.

These rules are bundled into a Policy Pack. When developers run pulumi up, the Pulumi CLI downloads the Policy Pack and evaluates the proposed infrastructure (the Preview Plan) against the rules.

If a rule is violated, CrossGuard can either issue a warning, or outright block the deployment and terminate the pulumi up command.

CrossGuard vs OPA/Rego
#

While OPA is incredibly powerful, it requires learning a specialized, complex logic language (Rego). Because CrossGuard uses TypeScript, your security team can write policies using the exact same language, syntax, and typing they use for the infrastructure itself.


2. Creating a Policy Pack
#

A Policy Pack is an isolated Node.js project. It lives in a separate repository from your actual infrastructure code.

Initialize a new Policy Pack:

mkdir my-org-policies
cd my-org-policies
pulumi policy new aws-typescript

This generates an index.ts file tailored for policy definitions. Let’s write two rules.

Rule 1: The “No Public Buckets” Rule (Mandatory)
#

We want to strictly prohibit the creation of AWS S3 buckets that have an acl set to public-read.

// my-org-policies/index.ts
import * as aws from "@pulumi/aws";
import { PolicyPack, validateResourceOfType } from "@pulumi/policy";

new PolicyPack("my-org-security-rules", {
    policies: [
        {
            name: "s3-no-public-read",
            description: "S3 Buckets must not be publicly readable.",
            // enforcementLevel can be "advisory", "mandatory", or "remediate"
            enforcementLevel: "mandatory",
            
            // The validation logic
            validateResource: validateResourceOfType(aws.s3.Bucket, (bucket, args, reportViolation) => {
                if (bucket.acl === "public-read" || bucket.acl === "public-read-write") {
                    // If this runs, the deployment is BLOCKED
                    reportViolation("You cannot create a public S3 bucket in this organization.");
                }
            }),
        }
    ],
});

Rule 2: The “Cost Control” Rule (Advisory)
#

We want to warn developers if they are provisioning a massive EC2 instance, but we will allow the deployment to proceed.

        {
            name: "ec2-cost-control",
            description: "Warn against expensive instance types.",
            enforcementLevel: "advisory",
            
            validateResource: validateResourceOfType(aws.ec2.Instance, (instance, args, reportViolation) => {
                if (instance.instanceType.startsWith("m5.16x") || instance.instanceType.startsWith("c5.18x")) {
                    // If this runs, it prints a yellow warning, but doesn't block the build
                    reportViolation("Warning: This instance type costs >$1000/month. Ensure you have budget approval.");
                }
            }),
        }

3. Applying the Policy Pack
#

There are two ways to execute CrossGuard policies against an infrastructure stack.

Method A: Local Enforcement
#

During local development, a developer can run their infrastructure code and explicitly point to a local directory containing the Policy Pack.

Navigate to your infrastructure project (e.g., pulumi-first-project) and run:

pulumi up --policy-pack ../my-org-policies

Expected Terminal Output:

Previewing update (dev)

Policy Violations:
    [mandatory]  my-org-security-rules v1.0.0  s3-no-public-read
    S3 Buckets must not be publicly readable.
    You cannot create a public S3 bucket in this organization.

error: preview failed: 1 mandatory policy violation found

The CLI refuses to proceed.

Method B: Organization-Wide Enforcement (Server-Side)
#

The true power of CrossGuard is server-side enforcement.

The Security Team can publish the Policy Pack directly to the Pulumi Service SaaS platform:

# Inside the Policy Pack directory
pulumi policy publish my-organization

Once published, you can configure the Pulumi Service to enforce this Policy Group on every single stack in the organization.

Now, when a developer runs a standard pulumi up on their laptop (or in Jenkins), the Pulumi CLI securely downloads the latest rules from the Pulumi Service in the background and evaluates them. The developer cannot bypass this.


4. Remediation Policies (Auto-Fixing)
#

CrossGuard has a third, advanced enforcement level: remediate.

Instead of just blocking a deployment, a Remediation Policy automatically modifies the proposed infrastructure state before it reaches AWS.

For example, if a developer forgets to add standard Cost Allocation Tags, you can write a policy that injects them automatically.

        {
            name: "auto-tag-ec2",
            description: "Automatically inject missing CostCenter tags.",
            enforcementLevel: "remediate",
            
            remediateResource: (resource, args) => {
                // If it's an EC2 instance and missing the tag...
                if (resource.type === "aws:ec2/instance:Instance") {
                    const tags = args.props["tags"] || {};
                    if (!tags["CostCenter"]) {
                        // Mutate the proposed properties!
                        tags["CostCenter"] = "IT-Platform";
                        return { props: { ...args.props, tags } };
                    }
                }
            },
        }

Troubleshooting & Common Errors
#

  1. TypeError: Cannot read properties of undefined in a Policy

    • Root Cause: You tried to access a property (like bucket.acl) but the developer didn’t explicitly set it in their index.ts file, meaning the property is undefined.
    • Solution: Always check for undefined or use optional chaining (bucket.acl === "public-read") to ensure your policies don’t crash on sparsely configured resources.
  2. Policy violates Outputs

    • Root Cause: A developer used an Output (e.g., generating an S3 bucket name dynamically using an EC2 instance ID), but your policy tries to run .startsWith() on it.
    • Solution: CrossGuard runs on the Preview phase. If a value is unknown until the Apply phase, CrossGuard receives a special token (pulumi.unknown). You must check if the value is known before validating it.

Conclusion & Next Steps
#

CrossGuard elevates Pulumi from a simple IaC tool into a comprehensive enterprise governance platform. By writing policies in TypeScript and publishing them to the Pulumi Service, you guarantee that no non-compliant infrastructure can ever reach your cloud accounts.

We have now covered almost every facet of writing, testing, and securing TypeScript IaC. But what if your company is split? The Backend team uses Python, the Platform team uses TypeScript, and the DevOps team uses Go.

How can a Python developer consume a complex ComponentResource written in TypeScript?

In the Final Episode (Ep 15): Multi-Language Components, we will explore the cutting edge of Pulumi: using the Pulumi Packages protocol to share infrastructure abstractions across completely different programming languages.

pulumi - This article is part of a series.
Part 14: This Article