ty policies to be written in TypeScript, sharing types with infrastructure code. This eliminates context switching and enables unit testing of policies.
- CI/CD Enforcement: Policies run during
pulumi preview in the CI pipeline. Violations block merges, ensuring no non-compliant code reaches the state.
- State Security: State files are encrypted at rest and access-controlled via IAM roles scoped to the CI runner.
- Supply Chain Integrity: Modules are pinned to specific versions with checksums. External providers are validated against a trusted registry.
Step-by-Step Implementation
- Initialize Policy Pack: Create a dedicated package for security policies.
- Define Enforcement Levels: Use
mandatory for critical security controls and warning for best practices.
- Integrate with CI: Configure the pipeline to run policy checks against the preview plan.
- Secure State Backend: Configure remote state with encryption and strict access policies.
Code Implementation
1. Infrastructure Definition (index.ts)
Define resources using TypeScript. The type system helps catch configuration errors early.
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
// S3 Bucket with encryption enabled
const bucket = new aws.s3.BucketV2("secure-bucket", {
bucketPrefix: "prod-data-",
});
// Apply server-side encryption
new aws.s3.BucketServerSideEncryptionV2("bucket-encryption", {
bucket: bucket.id,
rule: {
applyServerSideEncryptionByDefault: {
sseAlgorithm: "aws:kms",
},
},
});
// Security Group: Explicit deny public access
const sg = new aws.ec2.SecurityGroup("app-sg", {
ingress: [{
protocol: "tcp",
fromPort: 443,
toPort: 443,
cidrBlocks: ["10.0.0.0/8"], // Internal only
}],
egress: [{
protocol: "-1",
fromPort: 0,
toPort: 0,
cidrBlocks: ["0.0.0.0/0"],
}],
});
2. Security Policy Pack (policies/index.ts)
Write policies in TypeScript. These run against the Pulumi plan, validating resources before creation.
import { policyPack, ResourceValidationArgs, validateResourceOfType } from "@pulumi/policy";
import * as aws from "@pulumi/aws";
import * as pulumi from "@pulumi/pulumi";
export const securityPack = policyPack({
name: "prod-security-pack",
description: "Mandatory security policies for production infrastructure.",
policies: [
{
name: "s3-no-public-acl",
description: "S3 buckets must not have public ACLs.",
enforcementLevel: "mandatory",
validateResource: validateResourceOfType(aws.s3.BucketV2, (bucket, args, reportViolation) => {
if (bucket.acl && (bucket.acl === "public-read" || bucket.acl === "public-read-write")) {
reportViolation("S3 bucket ACL must not be public. Use private ACL.");
}
}),
},
{
name: "sg-no-public-ssh",
description: "Security groups must not allow SSH from the internet.",
enforcementLevel: "mandatory",
validateResource: validateResourceOfType(aws.ec2.SecurityGroup, (sg, args, reportViolation) => {
const hasPublicSsh = sg.ingress?.some(rule =>
rule.protocol === "tcp" &&
rule.fromPort === 22 &&
rule.toPort === 22 &&
rule.cidrBlocks?.includes("0.0.0.0/0")
);
if (hasPublicSsh) {
reportViolation("Security group allows SSH from 0.0.0.0/0. Restrict to internal CIDRs.");
}
}),
},
{
name: "enforce-tags",
description: "All resources must have CostCenter and Owner tags.",
enforcementLevel: "warning",
validateResource: (args, reportViolation) => {
const tags = args.resource.tags;
if (!tags?.CostCenter || !tags?.Owner) {
reportViolation("Resource missing required tags: CostCenter, Owner.");
}
},
},
],
});
3. CI/CD Integration (github-actions.yml)
Enforce policies during the pull request workflow.
name: IaC Security Check
on:
pull_request:
paths:
- 'infrastructure/**'
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pulumi/actions@v4
with:
command: preview
stack-name: dev
env:
PULUMI_CONFIG_PASSPHRASE: ${{ secrets.PASSPHRASE }}
# Policy pack is automatically applied via Pulumi.yaml configuration
4. Pulumi Configuration (Pulumi.yaml)
Link the policy pack to the project.
name: my-infra
runtime: nodejs
description: Secure infrastructure project
config:
policyPacks:
- ./policies
Rationale
TypeScript policies provide compile-time safety. If the AWS provider updates a resource schema, the policy code fails to compile, alerting the team immediately. This prevents stale policies that miss new vulnerability classes. The mandatory enforcement level acts as a hard gate, while warning allows progressive adoption without blocking delivery.
Pitfall Guide
-
Ignoring State File Security
- Mistake: Storing state files in unencrypted S3 buckets or local disk. State files contain resource IDs, IPs, and sometimes plaintext secrets.
- Fix: Use remote backends with server-side encryption (SSE-KMS) and restrict access via IAM policies. Enable versioning to recover from corruption.
-
Over-Reliance on Default Rule Sets
- Mistake: Enabling all rules in a scanning tool without tuning. This generates noise, leading developers to disable checks.
- Fix: Curate policies based on organizational risk appetite. Start with critical controls (e.g., public access, encryption) and expand gradually. Use
warning for non-critical checks.
-
Secrets in Variables and Logs
- Mistake: Hardcoding secrets in IaC files or passing them via environment variables that appear in CI logs.
- Fix: Use a secrets manager (e.g., AWS Secrets Manager, HashiCorp Vault) and reference secrets via secure lookups. Mark sensitive outputs in IaC to prevent logging. Integrate
git-secrets or trufflehog in pre-commit hooks.
-
Treating IaC as Immutable Without Drift Detection
- Mistake: Assuming the deployed state matches the code. Manual changes or external tools can cause drift.
- Fix: Implement scheduled drift detection jobs. Run
pulumi preview or terraform plan periodically. Alert on differences and enforce remediation workflows.
-
Supply Chain Attacks on Modules
- Mistake: Referencing modules from untrusted sources or using mutable tags like
latest.
- Fix: Pin module versions to specific commits or hashes. Use private registries for internal modules. Scan modules for vulnerabilities before use. Verify checksums of providers.
-
Skipping Policy Unit Tests
- Mistake: Assuming policies work correctly without verification.
- Fix: Write unit tests for policies using mock resources. Test positive and negative cases. Ensure policies handle edge cases like null values or dynamic references.
-
Security as a Blocker vs. Guardrail
- Mistake: Throwing errors without guidance, frustrating developers.
- Fix: Provide actionable error messages. Link to documentation or examples. Offer automated remediation suggestions where possible. Position security as an enabler of safe velocity.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Startup / Small Team | Open Source Scanners (Checkov) + GitLeaks | Low setup overhead, immediate value, community support. | Low |
| Enterprise / Multi-Cloud | Pulumi Policy Packs + OPA | Type safety, complex logic support, unified workflow across clouds. | Medium |
| Highly Regulated | TAC + Manual Review + Immutable Artifacts | Audit trails, compliance mapping, strict change control. | High |
| Legacy Migration | Drift Detection + Post-Deploy Scan | Non-intrusive discovery, prioritizes remediation of existing debt. | Medium |
| Serverless Focus | SAM/Serverless Framework + Custom Plugins | Framework-specific validation, optimized for event-driven resources. | Low |
Configuration Template
GitHub Actions Workflow with Pulumi Policy Enforcement
name: IaC Security & Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
AWS_REGION: us-east-1
jobs:
security-check:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install Dependencies
run: npm ci
- name: Secret Scan
uses: trufflesecurity/trufflehog@main
with:
extra_args: --only-verified
- name: Pulumi Preview
uses: pulumi/actions@v4
with:
command: preview
stack-name: dev
# Policies defined in Pulumi.yaml run automatically
- name: Policy Unit Tests
run: npm test -- --testPathPattern=policies
deploy:
needs: security-check
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Pulumi Up
uses: pulumi/actions@v4
with:
command: up
stack-name: prod
Quick Start Guide
- Install Pulumi CLI: Run
curl -fsSL https://get.pulumi.com | sh and verify with pulumi version.
- Initialize Project: Execute
pulumi new aws-typescript to create a TypeScript-based AWS project.
- Add Policy Pack: Create a
policies directory, install @pulumi/policy, and define your first mandatory policy (e.g., no public S3 buckets).
- Configure Project: Add
policyPacks: [./policies] to your Pulumi.yaml file.
- Run Preview: Execute
pulumi preview. The policy pack will evaluate resources and block violations before deployment. Commit and push to enforce via CI.