Pulumi Secrets and Configuration Management
Secrets—database passwords, API keys, OAuth tokens, encryption keys—are the crown jewels of infrastructure. Storing them in plain text or hardcoding them in code is a compliance violation and a security vulnerability. Pulumi handles secrets securely by encrypting them in the state file and only decrypting them during deployment. This article teaches you to manage secrets in Pulumi, rotate them safely, and integrate with AWS Secrets Manager and other secret backends.
Defining Secrets in Pulumi
Pulumi treats secrets as a special data type: values marked as secrets are encrypted in the state file and redacted from logs. Mark a value as a secret with config.require_secret():
# Set a database password as a secret
pulumi config set --secret db_password 'postgres123!'
In your code, retrieve it as a secret:
import pulumi
import pulumi_aws as aws
config = pulumi.Config()
# Retrieve the secret; Pulumi decrypts it only when needed
db_password = config.require_secret('db_password')
# Create an RDS database with the secret password
db = aws.rds.Instance('app-db',
allocated_storage=20,
engine='postgres',
instance_class='db.t3.micro',
username='postgres',
password=db_password, # Safe to pass; Pulumi doesn't log it
)
When you run pulumi up, Pulumi decrypts the password in memory, passes it to the RDS provisioning API, and never writes it to stdout or logs.
Encrypting Secrets at Rest
By default, Pulumi uses a service-managed key for encryption (Pulumi Cloud). For production, use a customer-managed encryption key:
# Use AWS KMS to encrypt Pulumi state
pulumi config set --stack prod encryptionsalt '...'
# Or, configure a custom encryption provider in Pulumi.yaml
In Pulumi.prod.yaml, specify a custom encryption provider:
name: my-infra
runtime: python
encryptionSalt: >
xxxxxxxxxx # Generated once, stored securely
encryptionProvider: awskms
encryptionProvider:
awskms:
keyId: arn:aws:kms:us-east-1:123456789:key/12345678-1234-1234-1234-123456789
This ensures secrets are encrypted with your own AWS KMS key, giving you full control over decryption permissions.
Integrating with AWS Secrets Manager
For production, store secrets in AWS Secrets Manager rather than Pulumi config. Your infrastructure reads secrets from Secrets Manager at deploy time.
import pulumi
import pulumi_aws as aws
import json
config = pulumi.Config()
# Create a secret in AWS Secrets Manager
db_secret = aws.secretsmanager.Secret('db-secret',
description='RDS database credentials',
force_overwrite_replica_secret=True
)
# Store the actual secret value
db_secret_version = aws.secretsmanager.SecretVersion('db-secret-version',
secret_id=db_secret.id,
secret_string=pulumi.Output.secret(
json.dumps({
'username': 'postgres',
'password': 'postgres123!',
'engine': 'postgres',
'host': 'db.example.com',
'port': 5432
})
)
)
# Grant the application IAM role permission to read the secret
app_policy = aws.iam.RolePolicy('app-secrets-policy',
role='app-role',
policy=pulumi.Output.concat(
'{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"secretsmanager:GetSecretValue","Resource":"',
db_secret.arn,
'"}]}'
)
)
# Output the secret name so applications know where to find it
pulumi.export('db_secret_name', db_secret.name)
Applications read the secret from Secrets Manager:
import boto3
import json
secrets_client = boto3.client('secretsmanager')
response = secrets_client.get_secret_value(SecretId='db-secret')
secret = json.loads(response['SecretString'])
print(f"Connecting to {secret['host']} as {secret['username']}")
This approach keeps secrets out of Pulumi config and Git entirely; they live in AWS and are accessed on-demand.
Rotating Secrets Safely
Secrets must be rotated regularly (every 90 days is standard). Pulumi makes this seamless:
- Create a new version of the secret in Secrets Manager.
- Update the application to use the new version (or rely on Secrets Manager's versioning).
- Deactivate the old version after apps have migrated.
AWS Secrets Manager supports automatic rotation via Lambda. Here's a manual rotation in Pulumi:
import pulumi
import pulumi_aws as aws
import json
from datetime import datetime
config = pulumi.Config()
# New secret value
new_password = f'postgres-{datetime.now().strftime("%Y%m%d%H%M%S")}!'
# Update the secret version
db_secret_version = aws.secretsmanager.SecretVersion('db-secret-version-new',
secret_id='db-secret',
secret_string=pulumi.Output.secret(
json.dumps({
'username': 'postgres',
'password': new_password,
'engine': 'postgres',
'host': 'db.example.com',
'port': 5432
})
)
)
# Optional: Update RDS password to match
db = aws.rds.Instance('app-db',
password=new_password,
apply_immediately=True,
skip_final_snapshot=True
)
pulumi.export('new_password_rotated_at', datetime.now().isoformat())
Run this during a maintenance window to rotate the secret and update RDS in sync.
Handling Sensitive Data in Outputs
Outputs are printed after pulumi up completes. If an output contains a secret, Pulumi automatically redacts it. However, be explicit for clarity:
import pulumi
import pulumi_aws as aws
config = pulumi.Config()
api_key = config.require_secret('api_key')
# Create a resource that uses the secret
api_key_secret = aws.secretsmanager.Secret('api-key',
secret_string=pulumi.Output.secret(api_key)
)
# Output the secret ARN (safe; the ARN itself is not sensitive)
pulumi.export('api_key_arn', api_key_secret.arn)
# Never export the actual secret value; it will be redacted anyway, but it's clear intent
# pulumi.export('api_key_value', api_key) # BAD: don't do this
When you run pulumi up, the ARN is printed, but the secret value is redacted.
Environment-Specific Secrets
Different environments have different secrets. Manage them with separate config files:
# Dev secrets (local SQLite, mock API key)
pulumi config set --stack dev --secret db_password 'dev-only'
pulumi config set --stack dev --secret api_key 'mock-api-key'
# Prod secrets (AWS RDS, real API key)
pulumi config set --stack prod --secret db_password 'VERY_SECURE_PASSWORD'
pulumi config set --stack prod --secret api_key 'prod-api-key-from-provider'
Your code reads the appropriate secret for the stack:
import pulumi
config = pulumi.Config()
environment = pulumi.get_stack()
db_password = config.require_secret('db_password')
api_key = config.require_secret('api_key')
print(f"Deploying to {environment} with password '{db_password}' and API key '{api_key}'")
Running this:
pulumi stack select dev
pulumi up # Uses dev secrets
pulumi stack select prod
pulumi up # Uses prod secrets
Validating Secrets Before Deployment
Validate that required secrets are present before attempting deployment:
import pulumi
config = pulumi.Config()
environment = pulumi.get_stack()
# List of required secrets
required_secrets = ['db_password', 'api_key', 'jwt_secret']
for secret_name in required_secrets:
try:
secret_value = config.require_secret(secret_name)
except pulumi.ConfigMissingError:
raise Exception(f"Missing required secret '{secret_name}' in {environment} stack. "
f"Set it with: pulumi config set --secret {secret_name} <value>")
print(f"All required secrets found for {environment} stack")
If a secret is missing, pulumi up fails with a clear error message before attempting any cloud operations.
Key Takeaways
- Mark sensitive data as secrets with
config.require_secret()so Pulumi encrypts them in state files. - For production, use AWS Secrets Manager instead of Pulumi config to centralize secret management.
- Rotate secrets regularly by creating new versions and updating resources in sync.
- Pulumi automatically redacts secret values from logs and exports.
- Validate that all required secrets are present before deploying.
Frequently Asked Questions
Can I commit Pulumi.yaml files with secrets to Git?
Yes, but only if you use --secret to mark them. Secrets are encrypted in the YAML file and are unreadable without the encryption key. Never commit unencrypted passwords.
What if I accidentally committed a secret to Git?
- Immediately rotate the secret (change password, revoke API key).
- Remove the secret from Git history using
git-filter-repoorbfg. - Force-push the cleaned history (carefully; coordinate with team).
How do I share secrets with team members?
Use Pulumi Cloud's role-based access control, or store secrets in AWS Secrets Manager and grant IAM permissions. Never email or Slack unencrypted secrets.
Can I use environment variables to pass secrets?
Yes. Set export AWS_SECRET_VARIABLE=value, then read it in Python with os.environ.get('AWS_SECRET_VARIABLE'). However, Pulumi's config management is more secure and auditible.
How do I handle secrets for third-party APIs?
Treat them as Pulumi secrets. If an API key is needed at deploy time, store it in Pulumi config. If needed only by the running application, store it in AWS Secrets Manager and have the app fetch it.