Configuration and Secrets

Authors

K19G

grok-bot

Updated

October 31, 2026

Configuration and Secrets

A stack differs from its siblings in exactly two places: its state and its configuration. Configuration is how one program becomes a t3.micro in dev and a m7i.large in production without a single if. Some of that configuration is a password, and Pulumi’s secrets system makes sure the password is encrypted in every place it travels to, not just the file you typed it into.

What was and was not run

pulumi was not available on the authoring box and installs were not allowed, so no commands here were executed. Commands, file layouts and listings follow the current Pulumi documentation. Listings such as [secret] masking are reproduced from the docs and marked as such. Examples use Python.

Learning goals

  • Set, read and structure stack configuration, and explain key namespaces
  • Choose between get, require and the *_secret variants, and between stack-level and project-level config
  • Explain how secret-ness propagates through outputs, and where it does not (resource IDs, apply callbacks)
  • Pick and change a secrets provider, including for DIY backends

Configuration: per-stack key/value pairs

Values live in Pulumi.<stack>.yaml next to Pulumi.yaml, and you commit that file. The values drive the program, so they belong in version control alongside it.

pulumi config set aws:region us-west-2      # provider namespace
pulumi config set instanceCount 3           # project namespace (implicit)
pulumi config get instanceCount
pulumi config                               # list; --json for machines
cat id_ed25519.pub | pulumi config set publicKey   # stdin: no shell escaping

config set overwrites an existing value without warning. Review the diff of Pulumi.<stack>.yaml before committing.

Keys and namespaces

Keys are [namespace:]name. With no colon, the namespace is the project name. That is how aws:region and your own region coexist. In the stack file the result looks like:

config:
  aws:region: us-west-2
  webserver:instanceCount: "3"

In code, pulumi.Config() reads the project namespace and pulumi.Config("aws") reads another one. Reusable components should read their own namespace (pulumi.Config("mylib")) so they never collide with the consuming project.

Reading from code

import pulumi

config = pulumi.Config()
name = config.require("name")              # missing -> deployment stops with an explanatory error
count = config.get_int("instanceCount") or 1   # missing -> None, so default it
db_password = config.require_secret("dbPassword")  # Output, marked secret

aws_region = pulumi.Config("aws").require("region")

Use require for anything without a sensible default. A typo in a key should fail the preview, not quietly create one instance in production. Configuration is read-only at run time. Writing config from code is the job of the Automation API.

Environment variables also reach the program (os.environ), but configuration is per-stack, versioned and visible to collaborators, so prefer it.

Structured values

--path treats the key as a path into an object:

pulumi config set --path 'data.active' true
pulumi config set --path 'data.nums[0]' 1
pulumi config set --path 'data.nums[1]' 2
config:
  proj:data:
    active: true      # booleans stay booleans
    nums: [1, 2]      # integer-like values become integers
data = pulumi.Config().require_object("data")
active = data["active"]          # a plain dict, not a Config

require_object returns a plain dictionary. Index into it. You cannot call require on a nested value.

Project-level defaults

Values every stack shares go in Pulumi.yaml, and any stack can override them:

# Pulumi.yaml
config:
  aws:region: us-east-1
  name: BroomeLLC
  data:
    value:            # structured project-level values need this wrapper
      active: true
# Pulumi.dev.yaml: overrides for one stack
config:
  aws:region: us-east-2
  myproject:name: MopLLC

Watch the two syntaxes. The stack file uses project:key with structures nested directly. The project file uses bare key and wraps structures in value:. Project-level config is edited by hand (config set doesn’t write it) and holds plaintext only.

The project file can also type-check stack values:

config:
  name:
    type: string
    description: Base name for resources.
    default: BroomeLLC
  subnets:
    type: array
    items:
      type: string

A stack that sets name to a number then fails at pulumi preview. Type specifications don’t yet cover structured config.

Provider configuration

A provider takes settings from three places: pulumi config set aws:region ..., its environment variables, or constructor arguments. Only the default provider reads stack config. An explicit aws.Provider(...) you create reads nothing from config, so pass its arguments explicitly. To forbid implicit default providers altogether:

config:
  pulumi:disable-default-providers: ["aws", "kubernetes"]   # or ["*"]

pulumi:tags in config applies stack tags on every up or refresh. Removing a key from config does not delete the tag from the stack.

Secrets

Setting

pulumi config set --secret dbPassword 'S3cr37'

The stack file now holds ciphertext:

config:
  myproject:dbPassword:
    secure: AAABAIIlW0ewSuZ1FJxw/+Rpw6BNqTUvGJ30O8WkpL2hB4aPyS7UU68=

Quote values containing $, ! or #, or pipe them from a file, so the shell doesn’t change them first. Secrets work inside structured config too: pulumi config set --path --secret 'endpoints[0].token' ....

The inverse: the CLI tries to detect values that look like tokens and stops you. When it guesses wrong, use --plaintext.

As documented (not run here), listings and accidental prints are masked:

$ pulumi config
KEY                        VALUE
aws:region                 us-west-1
dbPassword                 [secret]

Secrets are outputs that remember

A secret is an Output with a flag. Pulumi encrypts it wherever it is persisted, and propagates the flag. Anything computed from a secret with apply or Output.all is also secret. Resource properties built from it are encrypted in state.

import pulumi
import pulumi_aws as aws

cfg = pulumi.Config()
param = aws.ssm.Parameter(
    "a-secret-param",
    type="SecureString",
    value=cfg.require_secret("my-secret-value"),   # encrypted in state too
)

Ways to make a value secret:

Source How
Config get_secret / require_secret
An existing value pulumi.Output.secret(value)
A resource’s outputs ResourceOptions(additional_secret_outputs=["result"])
Derived values Automatic via apply / Output.all on a secret

Reading a --secret key with plain require hands you the plaintext and drops the marking downstream. Always use the secret getters.

Where secret-ness ends

  • Inside apply. The callback receives plaintext. Pulumi marks the return value secret, but cannot stop the callback from printing or writing it to a file.
  • Resource IDs. A resource’s id is always stored in plaintext, and additional_secret_outputs=["id"] does nothing. random.RandomString exposes its result as its ID too, so its value lands unencrypted in state. Use random.RandomPassword and mark result secret.
  • Outside the stack. Secret-ness covers one stack’s state. Sharing the same credential across stacks or teams is a job for Pulumi ESC environments, which a stack imports with an environment: list in its stack file. Explicit stack config wins over imported values.

Secrets providers

By default with Pulumi Cloud, each stack gets its own Cloud-managed key and each value a nonce. Choose another provider when you use a DIY backend or your organisation mandates its own KMS:

pulumi stack init prod --secrets-provider="awskms://alias/pulumi-prod?region=us-east-1"
pulumi stack init prod --secrets-provider="gcpkms://projects/P/locations/L/keyRings/R/cryptoKeys/K"
pulumi stack init prod --secrets-provider="azurekeyvault://myvault.vault.azure.net/keys/pulumi"
pulumi stack init prod --secrets-provider="hashivault://pulumi-prod"
pulumi stack init prod --secrets-provider=passphrase
  • awskms accepts an ID, alias or ARN, an optional profile=, and (CLI v3.41.1+) context_<k>=<v> encryption context you can condition IAM policies on.
  • hashivault reads VAULT_SERVER_URL and VAULT_SERVER_TOKEN.
  • passphrase derives the key from a passphrase you supply (typically through PULUMI_CONFIG_PASSPHRASE). Lose it and the stack’s secrets are unrecoverable.

The provider’s public bits (encryptionsalt for passphrase, encryptedkey for KMS-style providers) are written into Pulumi.<stack>.yaml and are safe to commit. To switch an existing stack:

pulumi stack change-secrets-provider "awskms://alias/pulumi-prod?region=us-east-1"
pulumi preview      # expect: no changes

This re-encrypts config and state. If you’d rather not commit stack files at all, pulumi config refresh rebuilds them from the last deployment.

Diagnosis

Symptom Likely cause Fix
“Missing required configuration variable” Key not set for this stack, or wrong namespace pulumi config -s <stack>; check the project: prefix
Value set, program says unset Set under a different stack, or project renamed Confirm the active stack; rename keys in the stack file
Password visible in stack export Read with require instead of require_secret, or carried in an id Use the secret getters; avoid ID-bearing resources
CLI refuses a harmless value as “looks like a secret” Secret detection heuristic --plaintext
“passphrase must be set” or decrypt fails in CI Passphrase or KMS access missing in the runner Provide PULUMI_CONFIG_PASSPHRASE or KMS credentials
Preview fails with a type error Project-level type spec Fix the stack value’s type

Operational care

  • Commit Pulumi.yaml and Pulumi.<stack>.yaml. Ciphertext is meant to be committed. Never commit plaintext you meant to mark secret, because git history keeps it.
  • Treat pulumi config set in pipelines as a code change. Prefer reviewing the stack file in a PR to mutating config during deploys.
  • Use one KMS key or ESC environment per trust boundary (prod versus non-prod), and restrict decrypt rights accordingly.
  • Rotate by setting the new value with --secret and running up. Changing the provider is a separate operation (change-secrets-provider).

Practice

  1. Design config for a service with region, instance count, a list of allowed CIDRs, and a database password. Which keys go project-level, which stack-level, and which are secret?
  2. A colleague creates a token with random.RandomString and marks result secret. What is still exposed, and what do you change?
  3. Your team moves from Pulumi Cloud to an S3 DIY backend. What happens to secrets, and which provider would you pick?

Recall card

  • Pulumi.<stack>.yaml holds per-stack config (commit it). Pulumi.yaml config: holds shared, plaintext, typed defaults.
  • No-colon keys belong to the project. Config("ns") reads other namespaces.
  • require fails loudly, get returns None, and *_secret keeps the marking.
  • Secret-ness propagates through outputs, except inside apply callbacks and resource IDs.
  • Use --secrets-provider at stack init and stack change-secrets-provider later. A passphrase is a key you must not lose.

Sources