Pulumi state records every resource your programs manage, and the secrets inside it are only as safe as the encryption provider, the backend and the people who can read the stack. Pulumi lets teams write infrastructure in general purpose languages, store state in Pulumi Cloud or a self-managed backend, and centralise secrets and short-lived cloud credentials in Pulumi ESC (Environments, Secrets, and Configuration). Each of those layers has customer-side settings that decide who can decrypt what. This guide covers how to choose and configure a secrets provider, how to secure Pulumi Cloud and DIY state backends, how to design ESC environments and their permissions, how to replace static cloud keys with OIDC login, and how to enforce guardrails with Pulumi Policies. It is written for platform engineers and cloud security teams. This page focuses on building the controls before anything goes wrong.
This is an independent guide from NHI Mgmt Group, based on Pulumi's public documentation as of November 2026. It is not sponsored or reviewed by Pulumi. Product features change, so confirm settings against Pulumi's current documentation.
Key takeaways
- Stack Read includes decrypt. Pulumi's Stack Read permission set carries the stack:decrypt scope, and for Pulumi Cloud stacks decryption keys are supplied automatically to readers, so treat read access to a production stack as secret access.
- Pick the secrets provider at stack creation. Pulumi Cloud encrypts with a per-stack key by default; awskms, azurekeyvault, gcpkms and hashivault let you hold the key yourself, and passphrase suits local or DIY use with a strong, managed passphrase.
- A DIY backend makes you the platform. With S3, Azure Blob, Google Cloud Storage, PostgreSQL or file backends you own access control, encryption at rest, auditing and backups for the .pulumi folder.
- Environment Open is the privileged ESC right. Environment Read shows definitions, but Environment Open resolves secrets and mints cloud credentials, so grant it per environment, per consumer.
- Prefer OIDC login over stored keys. The aws-login, azure-login and gcp-login providers exchange Pulumi's OIDC token for short-lived cloud credentials, so ESC environments need not hold long-lived access keys.
How identity and access work in Pulumi
Three separate trust relationships sit behind every pulumi up: the identity that talks to the state backend, the cloud credentials the providers use, and the key that encrypts secrets in state. Pulumi documents that state does not include your cloud credentials; they stay on the machine where the CLI runs, even with Pulumi Cloud.
| Layer | Pulumi Cloud backend | DIY backend |
|---|---|---|
| Backend login | pulumi login with a Pulumi access token or an OIDC-exchanged token; RBAC decides stack access | pulumi login s3://, azblob://, gs://, postgres:// or file://; the cloud or database identity decides access |
| Secrets encryption | Per-stack key managed by Pulumi Cloud by default, or a customer provider | passphrase or a customer KMS or Vault provider |
| Who can decrypt | Anyone with Stack Read or higher on the stack | Anyone who has the passphrase or can use the KMS or Vault key, plus read access to state |
| History and recovery | Transactional checkpoint API with journaling | .pulumi/history and .pulumi/backups objects in the bucket |
| Audit | Pulumi Cloud audit logs (edition dependent) | Your cloud storage and KMS audit logs |
How secrets are marked and encrypted
A value is encrypted in state when it is marked secret. Use pulumi config set --secret for configuration (pulumi config then displays [secret]), getSecret or requireSecret when reading configuration in code, pulumi.secret() or the language equivalent to wrap a computed value, and the additionalSecretOutputs resource option to keep a resource's outputs encrypted wherever they flow. Combining a secret with apply or Output.all produces a secret output. Three behaviours from Pulumi's documentation shape your design:
- Resource IDs are plain text: adding id to additionalSecretOutputs has no effect. Pulumi recommends random.RandomPassword with additionalSecretOutputs: ["result"] instead of random.RandomString, whose value is its ID.
- apply callbacks receive plain text: Pulumi cannot stop code inside a callback from printing or saving the value, so review apply blocks in code review.
- Stack config files are safe to commit: Pulumi.<stack>.yaml holds ciphertext under secure keys, plus encryptionsalt for passphrase stacks or encryptedkey for KMS providers. The risk is values that were never marked secret.
Admin roles and privileged access
Pulumi Cloud RBAC combines permission sets with entity access over stacks and ESC environments. Pulumi's RBAC documentation lists the built-in organisation roles (Reader, Builder, Maintainer, Admin and Billing Manager) in all editions, with custom roles and teams on the Pro and Enterprise editions; the permission sets below are what decide secret exposure.
| Permission set | Includes | Secret exposure | Give it to |
|---|---|---|---|
| Stack Read | Read, preview, stack:export, stack:encrypt, stack:decrypt | Can decrypt stack secrets | People and pipelines that genuinely need the stack's secrets; use outputs or ESC for everyone else |
| Stack Write | Adds updates, imports, deployment settings, schedules, webhooks | As Stack Read, plus changes infrastructure | The deployment pipeline for that stack |
| Stack Admin | Adds delete, change access, transfer, rename | Full control | Stack owners only |
| Environment Read | Definitions, versions, tags, schedules, rotation history | Does not open the environment | Reviewers and auditors |
| Environment Open | Adds opening environments and versions, cloning | Resolves secrets and issues credentials | The specific stacks, apps and pipelines that consume it |
| Environment Write | Adds editing, rotating, versions, tags, schedules, webhooks | Can change what consumers receive | Environment owners |
| Environment Admin | Adds delete | Full control | Platform team |
- Separate read from decrypt by design: publish values other teams need as stack outputs that are not secret, or through StackReference to outputs, rather than giving them Stack Read on a stack full of secrets.
- Treat environment edits as production changes: Pulumi ESC lists Versioning, Approvals and Webhooks among its concepts; use version tags for promoted environments and require approval for production edits where your edition provides it.
- DIY backends: privileged access is whoever can write to the bucket or database and use the KMS key. Keep those groups as small as the Pulumi Cloud Stack Admin group would be.
Integrations, API tokens and service accounts
Choosing a secrets provider
Set the provider when you create the stack with pulumi stack init <name> --secrets-provider="<provider>://<settings>", and change it later with pulumi stack change-secrets-provider. Pulumi implements providers through the Go Cloud Development Kit.
| Provider | Setting format | Where it fits |
|---|---|---|
| Pulumi Cloud default | None: per-stack key with a per-value nonce | Pulumi Cloud stacks where RBAC is your decrypt control |
| passphrase | Passphrase supplied at run time; encryptionsalt stored in stack config | Local and small DIY setups; keep the passphrase in a secrets manager, never in the repository |
| awskms | awskms://<key id, alias/name or ARN>?region=<region>, optional profile and encryption context parameters | AWS estates that want KMS key policy and CloudTrail on every decrypt |
| azurekeyvault | azurekeyvault://<vault>.vault.azure.net/keys/<key> | Azure estates using Key Vault RBAC |
| gcpkms | gcpkms://projects/<p>/locations/<l>/keyRings/<r>/cryptoKeys/<k> with purpose ENCRYPT_DECRYPT | Google Cloud estates using Cloud KMS IAM |
| hashivault | hashivault://<key> with VAULT_SERVER_URL and VAULT_SERVER_TOKEN | Teams standardised on Vault's Transit secrets engine |
With a KMS provider, decrypting a stack needs both access to state and permission on the key, which gives you a second, independently audited gate. NHI Mgmt Group recommends a KMS or Vault provider for any production stack on a DIY backend, with the encryption context parameters on awskms set to the project and environment so key policy can tell stacks apart.
Securing a DIY state backend
- Least privilege on S3: Pulumi documents exactly four actions for the full stack lifecycle: s3:ListBucket on the bucket ARN, and s3:GetObject, s3:PutObject and s3:DeleteObject on the objects. Scope them to the prefix for each project where you share a bucket.
- Azure Blob: the backend authenticates through the Azure SDK for Go; prefer identity-based sign-in (for example az login or a managed identity) with the Storage Blob Data Contributor role over a shared AZURE_STORAGE_KEY or AZURE_STORAGE_SAS_TOKEN.
- Google Cloud Storage: authenticate with Application Default Credentials from a workload identity rather than a key file.
- PostgreSQL: Pulumi advises against putting credentials in the postgres:// login command; supply them from the environment or a secrets manager.
- Protect history: the .pulumi folder holds stacks/, locks/, history/ and backups/. Old checkpoints contain old ciphertext, so retention, versioning and access rules apply to all of it, not just the current state file.
- Keep the backend current: backends that still use the pre-3.61.0 global namespace are deprecated; plan pulumi state upgrade, which is one-way.
Pulumi ESC environments
An ESC environment is a YAML document that can import other environments, hold values marked with fn::secret, pull secrets from external stores and issue short-lived credentials. Consumers use it from a stack's configuration or with pulumi env run.
- Login providers: aws-login, azure-login, gcp-login and gh-login issue short-lived credentials; an oidc provider mints a token other services can trust.
- Secrets providers: aws-secrets, aws-parameter-store, azure-secrets, gcp-secrets, vault-secrets and others read from the store of record at open time, so the secret is not copied into ESC.
- Rotators: aws-iam, azure-app-secret, mysql, postgres, password, snowflake-user and external rotate credentials on a schedule through fn::rotate.
- Imports: split environments into a base layer per account and thin per-application layers, so Environment Open on an application layer does not expose unrelated secrets.
OIDC instead of stored cloud keys
The aws-login provider supports an oidc block with roleArn, sessionName and an optional duration, and a static block with accessKeyId and secretAccessKey. Pulumi recommends OIDC. The IAM role trusts the federated principal api.pulumi.com/oidc with sts:AssumeRoleWithWebIdentity, an audience condition of aws:<your Pulumi org> and a subject condition that matches the environment. Optional policyArns narrow the session further. For the CI side, OIDC token exchange can give pipelines a short-lived Pulumi token instead of a stored PULUMI_ACCESS_TOKEN; see the CI/CD Pipeline Identity Security Guide and the GitHub Actions OIDC Federation Hardening Guide.
Logging and detection
- Pulumi Cloud audit logs: available on the Pro and Enterprise editions according to Pulumi's audit logs guide, with automated export (for example to Amazon S3 or Microsoft Sentinel) on Enterprise. Confirm the event types you need, such as decryption and stack export, in the audit log format reference and send them to your SIEM.
- KMS and Vault audit: with awskms, azurekeyvault, gcpkms or hashivault, every decrypt appears in CloudTrail, Azure Monitor, Cloud Audit Logs or the Vault audit device, which works for DIY backends too.
- Object storage logs: enable data access logging on the state bucket and alert on reads of stacks/ or history/ by identities other than the deployment roles.
- Cloud sign-ins from OIDC roles: AssumeRoleWithWebIdentity events for Pulumi roles, with session names that identify the environment.
- Policy findings: Pulumi Policies violations from preventative and audit policy groups.
Metrics that show it is working
- Share of production stacks using a customer KMS or Vault provider, or Pulumi Cloud with Stack Read limited to named consumers.
- Number of ESC environments holding static cloud keys (target: zero, replaced by login providers).
- Number of principals with Environment Open on each production environment, reviewed quarterly.
- Share of pipelines using OIDC token exchange rather than stored Pulumi tokens.
- Mandatory policy violations blocked per month, and the time to fix advisory findings.
Hardening baseline
| Area | Baseline (NHI Mgmt Group recommendation) |
|---|---|
| Secrets in code | Every credential set with config set --secret or wrapped with pulumi.secret(); additionalSecretOutputs on resources that return secrets; random.RandomPassword, not RandomString, for generated secrets |
| Secrets provider | KMS or Vault provider for DIY production stacks; passphrase only with a managed, high-entropy passphrase |
| State backend | Pulumi Cloud with RBAC, or a DIY bucket with the four documented actions, encryption at rest, versioning, access logging and no public access |
| Access | Stack Read granted per stack, not organisation wide; Environment Open per environment and consumer |
| Cloud credentials | ESC login providers with OIDC; no static blocks in production environments |
| Guardrails | Pulumi Policies packs at mandatory level for critical rules, run with --policy-pack locally or through policy groups in Pulumi Cloud |
Rollout plan
- Inventory: list stacks, their backend, their secrets provider (visible in stack config), and every principal with Stack Read and Environment Open.
- Find plain-text secrets: search stack config files and exported state for credentials that were never marked secret, and fix them with config set --secret or additionalSecretOutputs.
- Move providers: run pulumi stack change-secrets-provider to a KMS or Vault key on one non-production stack, confirm preview and up still work, then repeat for production. Roll back by changing the provider again; the old key must stay available until then.
- Restructure ESC: replace static blocks with oidc login, split environments by import layer, and narrow Environment Open grants.
- Enforce policy: start packs at advisory, review findings for two weeks, then move critical rules to mandatory.
- Rotate: after any provider change or access clean-up, rotate the underlying secrets, because old ciphertext remains in history and backups.
Decision guidance
- Pulumi Cloud or DIY: Pulumi Cloud brings RBAC, audit logs, transactional checkpoints and ESC integration; a DIY backend suits teams that must keep state in their own accounts and are ready to run access, encryption, backup and audit themselves.
- Small teams: Pulumi Cloud with the default provider, tight Stack Read grants and OIDC for CI covers most needs.
- Regulated estates: a customer KMS provider gives key-level control and an independent decrypt log; Pulumi lists CIS, ISO 27001, NIST, PCI DSS and other framework policy packs on its Enterprise edition.
Common misconfigurations
- Organisation-wide Stack Read: a default role that lets every member read every stack also lets them decrypt every stack secret.
- Passphrase in the repository or CI log: the passphrase is committed next to the encryptionsalt, or echoed in a pipeline, which makes the encryption decorative.
- Secrets surfaced as IDs or unmarked outputs: a generated password exported as a plain output or produced by a resource whose ID is the secret.
- Public or broadly readable state bucket: a DIY bucket shared with many roles, including history and backups that hold earlier ciphertext and plain-text resource data.
- Static keys in ESC: environments using aws-login static blocks or long-lived keys under fn::secret where an OIDC role would work.
- One giant environment: every secret in a single ESC environment, so any consumer with Environment Open receives everything.
Framework mapping
| Pulumi control | NIST SP 800-53 Rev 5 | NIST CSF 2.0 | MITRE ATT&CK technique addressed |
|---|---|---|---|
| Secrets marked and encrypted with a managed provider | SC-28, IA-5 | PR.DS-01 | T1552.001 Credentials In Files |
| Stack and environment permission sets | AC-6, AC-2 | PR.AA-05 | T1213 Data from Information Repositories |
| OIDC login providers instead of static keys | IA-5 | PR.AA-01 | T1078.004 Cloud Accounts |
| Pulumi Policies at mandatory level | CM-6, CM-2 | PR.PS-01 | T1098 Account Manipulation |
| Audit logs and KMS decrypt logging | AU-2, AU-6 | DE.CM-03 | T1528 Steal Application Access Token |
Practitioner checklist
- Every production stack has a recorded backend, secrets provider and owner.
- Production stacks on DIY backends use awskms, azurekeyvault, gcpkms or hashivault.
- Stack Read on production stacks is limited to named people and pipelines that need the secrets.
- DIY state buckets grant only the documented actions, with encryption, versioning, access logging and no public access.
- Generated secrets use RandomPassword with additionalSecretOutputs, and apply callbacks never log values.
- ESC environments use oidc login blocks, external secrets providers and rotators instead of static keys.
- Environment Open is granted per environment and consumer, and reviewed quarterly.
- CI uses OIDC token exchange instead of stored Pulumi access tokens.
- Critical Pulumi Policies rules run at mandatory enforcement.
- Audit, KMS and storage logs reach the SIEM, with alerts on unexpected decrypts and exports.
Standards and references
- Pulumi: Secrets handling and encryption providers
- Pulumi: State and backends
- Pulumi: Using a DIY backend
- Pulumi: RBAC permission sets
- Pulumi: ESC documentation
- Pulumi: ESC aws-login provider
- Pulumi: Policies as code
Related NHI Mgmt Group resources: Infrastructure as Code Security Guide · IaC identity policy checks · Secrets Management Guide · Cloud Workload Identity Guide