| title | Roll out the Hulumi policy pack to an existing stack |
|---|---|
| description | Adopt @hulumi/policies on a stack that doesn't use SecureBucket / AccountFoundation yet — get the safety net first, migrate components later. |
You have an existing Pulumi stack, you can't migrate every resource to SecureBucket / AccountFoundation overnight, but you'd like the same safety net catching regressions today. The CrossGuard pack is independently loadable and surfaces a concrete migration list as it runs.
- An existing Pulumi (TypeScript) stack you can
pulumi previewagainst. @pulumi/policyinstalled alongside@hulumi/policies(any caret-compatible1.xSDK; the version Hulumi is tested against is in thepeerDependenciesof@hulumi/policies).- Read-only walkthrough first: load the pack as advisory, see what fires, then promote to mandatory.
pnpm add -D @hulumi/policies @pulumi/policy
mkdir policies && cd policies
cp ../node_modules/@hulumi/policies/PulumiPolicy.yaml .
cat > index.ts <<'EOF'
export { hulumiHardeningPack } from "@hulumi/policies/aws/packs/hulumi-hardening";
EOFImportant:
@pulumi/policy'snew PolicyPack()constructor starts a gRPC server at module load and only one is allowed per process. Always import packs from their dedicated entrypoint files (@hulumi/policies/aws/packs/*), never side-by-side from one file. See lessons/hulumi-m2.md for the rationale.
pulumi preview --policy-pack ./policiesRead every violation as a migration ticket:
| Violation | What to do |
|---|---|
HULUMI-H1-no-raw-bucket |
Replace raw aws.s3.Bucket / aws.s3.BucketV2 with SecureBucket. See components/secure-bucket.md. |
HULUMI-H2-state-backend-encryption (mandatory) |
Move state off file://; if S3, ensure SSE is enabled on the state bucket. |
HULUMI-H3-iac-role-tag (advisory pre-v1.0, mandatory at v1.0) |
Tag your IaC role hulumi:iac-role=true. Pair with the SCP template at v1.0. |
HULUMI-H4-startup-hardened-logging |
A Startup-Hardened SecureBucket is missing its logBucketArn sibling. Wire one up. |
Some violations are genuine bugs — fix them. Others are documented exceptions for your environment. For the second class, use a Suppression:
// policies/index.ts
import { hulumiHardeningPack } from "@hulumi/policies/aws/packs/hulumi-hardening";
import type { Suppression } from "@hulumi/policies";
const suppressions: Suppression[] = [
{
ruleId: "HULUMI-H1-no-raw-bucket",
urnScope: "urn:pulumi:prod::your-stack::aws:s3/bucket:Bucket::legacy-bucket",
reason: "Imported pre-Hulumi; migration tracked in #JIRA-1234.",
expiresAt: "2026-09-30",
},
];
export { hulumiHardeningPack, suppressions };See suppressions.md for the full suppression cookbook.
When the advisory run is clean (or every remaining violation has a documented suppression), wire the pack into CI:
# .github/workflows/ci.yml
- name: Pulumi preview with Hulumi policy pack
run: pulumi preview --policy-pack ./policiesHULUMI-H1, H2, and H4 already fail mandatory on violation. H3 flips to mandatory at the v1.0 release; track the v1.0 CHANGELOG entry for migration paths if you're upgrading from a pre-v1.0 install.
- A clean run reports
policy violations: 0 mandatory, 0 advisory. - A deliberately broken stack (e.g. add an
aws.s3.Bucketoraws.s3.BucketV2toindex.ts) fails withHULUMI-H1and a violation message naming the exact resource. - Removing the
hulumi:iac-role=truetag from your IaC role firesHULUMI-H3advisory pre-v1.0 / mandatory at v1.0.
H2 fires on an unencrypted state bucket but I can't see the bucket in the stack. Pulumi sets PULUMI_BACKEND_URL for every operation; H2 reads that and inspects the stack for a matching SSE configuration. If the state bucket isn't part of the stack being previewed, H2 degrades to advisory (encryption can't be verified) rather than silently passing. Move the state bucket into a Hulumi-managed stack to get full mandatory coverage.
H3 shows up as advisory and I want it mandatory now. It's a one-line override:
import { hulumiHardeningPack } from "@hulumi/policies/aws/packs/hulumi-hardening";
hulumiHardeningPack.policies.find((p) => p.name.includes("H3")).enforcementLevel = "mandatory";Do this only if you're already applying the SCP template — otherwise expect drive-by failures from teams that haven't tagged their IaC role yet.
Both HulumiHardeningPack and CisV5Pack need to load — preview crashes. Don't import them in the same file. Use one entrypoint per process and call pulumi preview --policy-pack ./policies-hulumi --policy-pack ./policies-cis. Each pack's instance lives in its own gRPC server.
Three additional CrossGuard packs ship in @hulumi/policies for K8s/EKS Pulumi programs (added in runbook hulumi-operations-k8s-security Milestone 3):
@hulumi/policies/k8s/packs/hulumi-k8s-hardening— workload + Service rules (WL-1privileged ·WL-2host namespaces ·WL-3mutable image tags ·WL-4resources advisory ·SVC-1public LoadBalancer needs justification).@hulumi/policies/k8s/packs/hulumi-k8s-rbac— RBAC rules (RBAC-1wildcard verbs ·RBAC-2Secret list/watch ·RBAC-3cluster-admin binding).@hulumi/policies/k8s/packs/hulumi-eks-cluster— EKS control-plane rules (EKS-CL-1public endpoint CIDR ·EKS-CL-2audit logging required).
- Day 0 — point
PulumiPolicy.yamlathulumi-k8s-hardening. The four mandatory rules (WL-1, WL-2, WL-3, SVC-1) catch the highest-blast-radius shapes;WL-4is advisory by default and surfaces as a warning rather than a deploy-blocker. - Day 7 — add
hulumi-k8s-rbac. RBAC violations often shake out platform-team tooling on the first preview; expect to add 1-3 suppressions for legitimate cluster operators (external-secrets, metrics-server) — each requires a writtenreasonon the suppression entry. - Day 14 — add
hulumi-eks-cluster.EKS-CL-1typically catches "we setendpointPublicAccess: truebut forgot to restrictpublicAccessCidrs";EKS-CL-2catches "audit logs got disabled in a quick test and never re-enabled".
@pulumi/policy only allows one PolicyPack per process — load each pack with its own --policy-pack flag:
pulumi preview \
--policy-pack ./policies-hulumi \
--policy-pack ./policies-k8s-hardening \
--policy-pack ./policies-k8s-rbac \
--policy-pack ./policies-eks-clusterEach rule respects the existing Suppression API — a { ruleId, urnScope, reason } entry on the suppressions config silences the rule for the matching URN. Suppressions without a non-empty reason are ignored (M3 invariant). Trailing-* glob in urnScope matches by URN prefix; an exact URN matches that one resource.
// PulumiPolicy.yaml or stack config
suppressions:
- ruleId: HULUMI-K8S-RBAC-2
urnScope: "urn:pulumi:prod::p::kubernetes:rbac.authorization.k8s.io/v1:ClusterRole::external-secrets"
reason: "external-secrets operator needs cluster-wide secret list/watch."- tiers.md § HulumiHardeningPack — rule matrix
- components/README.md — what to migrate to once a violation is a real bug.
- suppressions.md — when and how to document exceptions.
- CHANGELOG.md — H3 advisory→mandatory migration paths.