Safeguard
Infrastructure Security

Pulumi security scanning best practices

Pulumi programs run as real code with live cloud credentials -- here's how to secure state files, dependencies, CrossGuard policy, and CI/CD.

Vikram Iyer
Cloud Security Engineer
7 min read

Pulumi lets you write infrastructure as actual TypeScript, Python, Go, C#, or Java code instead of declarative templates, and that design choice is exactly why it needs a different security playbook than Terraform. When you run pulumi up, Pulumi executes your program directly in the same process that holds your cloud credentials — meaning any malicious code anywhere in your dependency tree, not just in a provider plugin, runs with access to your AWS, Azure, or GCP account. A default pulumi new aws-typescript starter pulls in more than 200 transitive npm packages before you write a single resource. Add in state files that store most resource properties in plaintext unless a value is explicitly marked secret, and Pulumi introduces supply chain and credential-exposure risks that most infrastructure teams haven't built controls for yet. Here is what a real Pulumi security program looks like in 2026, section by section.

Why does writing infrastructure as code in Pulumi create supply chain risk that Terraform doesn't have?

Because Pulumi programs are executable code that runs with live cloud credentials at deploy time, a single compromised npm, PyPI, or Go dependency can exfiltrate secrets or provision resources instead of merely describing them. In September 2025, the "Shai-Hulud" worm self-propagated through npm by stealing developer and CI tokens, ultimately compromising over 500 packages, some with tens of millions of weekly downloads. If a Pulumi TypeScript program depended on any affected package transitively, running pulumi up in CI would execute the attacker's postinstall script with whatever IAM role or AWS_ACCESS_KEY_ID the pipeline had configured — a direct path from a poisoned package to your cloud account. Terraform's HCL is declarative by contrast: provider binaries are pulled from registry.terraform.io and carry their own risk, but no attacker-controlled script runs automatically during terraform plan. Pulumi's flexibility is the feature and the risk, and treating a Pulumi repo like "just config" instead of like an application with a dependency graph is the most common mistake teams make.

How should you store and encrypt Pulumi state files?

Production Pulumi stacks should never use the default local file backend, and they shouldn't rely solely on the default "passphrase" secrets provider either. Pulumi's state checkpoint captures every resource property in the stack; only values explicitly wrapped as secret — via pulumi.secret(), a Config.requireSecret() call, or a resource input already typed as sensitive by the provider — get encrypted before they're written to state. Everything else lands in plaintext JSON. A concrete failure mode: an RDS instance's masterUserPassword passed as a plain string, rather than through pulumi config set --secret db-password, is stored unencrypted in the checkpoint even though the field name suggests it's sensitive. Use the Pulumi Cloud backend (the default since Pulumi 3.0 in 2021) or a self-hosted backend on S3, Azure Blob, or GCS, and pair it with a cloud KMS secrets provider — awskms, azurekeyvault, or gcpkms — instead of the default passphrase provider, so state-level encryption is backed by a key you can rotate and audit independently of the repo.

What is Pulumi CrossGuard, and how does it stop bad infrastructure before it's deployed?

CrossGuard is Pulumi's policy-as-code framework, generally available since Pulumi 2.6 in 2020, and it evaluates policy packs against every pulumi preview and pulumi up, blocking a deployment outright if a resource violates a mandatory rule. A typical aws-typescript policy pack ships around 15 built-in checks aligned to AWS Well-Architected guidance — no public S3 buckets, no unencrypted EBS volumes, no security groups open to 0.0.0.0/0 on port 22 — and each rule can be set to mandatory (blocks the deploy) or advisory (warns but allows it). The key difference from a standalone scanner is timing: CrossGuard runs inside the same engine pass that would create the resource, so a violating S3 bucket is never created in the first place, rather than being flagged and remediated after the fact. Teams running multi-account Pulumi Cloud organizations typically attach a CrossGuard policy group to every stack so the same guardrails apply whether an engineer deploys from a laptop or from CI.

How do you scan Pulumi programs and their dependencies for vulnerabilities before deployment?

You scan a Pulumi program the same way you'd scan any application's source, because its package.json, requirements.txt, or go.mod describes code that executes with cloud credentials at deploy time, not just at runtime. Software composition analysis (SCA) against the lockfile — package-lock.json, poetry.lock, go.sum — should run on every pull request that touches the Pulumi program, exactly as it would for a backend service, and should fail the build on new critical or high-severity findings rather than only reporting them. This matters because a Pulumi program's dependency count is often larger than developers expect: infrastructure code tends to accumulate helper libraries (date/string utilities, YAML parsers, cloud SDK wrappers) with the same laxity teams apply to "just scripts," even though those scripts hold production credentials. Generating a software bill of materials (SBOM) per stack, and diffing it on every change, gives you an audit trail of exactly which packages had access to which cloud account at which point in time — useful both for vulnerability triage and for incident response if a dependency is later flagged.

How should you scope provider credentials so a compromised Pulumi run can't take over your cloud account?

Scope each stack to a short-lived, purpose-built IAM role that can only touch the resources that stack manages, rather than sharing one long-lived admin credential across every stack and every CI job. The blast radius of a compromised dependency or a leaked token is bounded by whatever that token can do — a role scoped to a single VPC and its associated resources limits damage to that VPC, while an org-wide AdministratorAccess key does not. Federate CI credentials with OIDC instead of storing static keys as pipeline secrets; GitHub Actions has supported OIDC federation to AWS since November 2021, and the same pattern exists for Azure and GCP, so a token is minted per job run and expires within the hour instead of sitting in a secrets manager indefinitely. Pulumi ESC (Environments, Secrets, and Configuration), which Pulumi Cloud added in 2023, extends this further by dynamically vending short-lived cloud credentials into a Pulumi program at run time instead of storing them in stack config at all — worth adopting if you're still passing static keys through pulumi config set --secret.

How does your CI/CD pipeline configuration affect Pulumi security?

Pipelines that run pulumi up directly against pull requests, especially from forks, can hand a stranger's code your production cloud credentials before a human has reviewed a single line. The safe pattern is pulumi preview on every PR — which shows the diff of resources that would change without applying anything — gated by required review, with pulumi up restricted to a protected branch or a manual approval step after merge. Fork-triggered workflows deserve extra scrutiny: GitHub's pull_request_target event, which runs with access to repository secrets against code from an untrusted fork, has caused real supply chain incidents across multiple projects since it became a known attack pattern in 2021, and a Pulumi pipeline configured this way effectively offers the same credentials CrossGuard and scoped IAM roles are trying to protect. Pin action versions by commit SHA rather than by tag, and treat the pipeline's cloud credentials as the highest-value secret in the entire system, because in a Pulumi workflow they are.

How Safeguard Helps

Safeguard extends the same scrutiny you apply to application code to your Pulumi programs, because in Pulumi they're the same thing. Reachability analysis determines whether a vulnerable transitive dependency in your Pulumi TypeScript or Python program is actually invoked during a deploy, cutting through alert noise from packages that are present in the lockfile but never exercised. Griffin AI correlates findings across your CrossGuard policy results, dependency graph, and cloud IAM configuration to prioritize the handful of issues — like an overprivileged deploy role paired with an unpinned dependency — that represent real attack paths rather than isolated findings. Safeguard generates and ingests SBOMs per stack so you have an auditable record of exactly what code had access to which cloud account and when, and it opens auto-fix pull requests to bump vulnerable dependencies or tighten a misconfigured resource policy, so remediation lands as a reviewable diff instead of a ticket that sits in a backlog.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.