Enforcing Data Boundaries in Pulumi with Stack Outputs and Secrets
Learn how to architect a minimal Pulumi deployment that uses stack outputs for shared data and config secrets for sensitive values, ensuring clear data boundaries and secure remote state management.
14 Aug 2026, 08:12 UTC

Problem Statement
When building multiple Pulumi stacks that need to share data—such as a shared VPC or a database endpoint—developers often run into two classic pitfalls:
- Data leakage: Sensitive values (e.g., DB passwords) are accidentally exported as stack outputs and become visible to any stack that depends on them.
- State drift: Remote state stored in Pulumi Service can become corrupted or out of sync, causing deployments to fail or create duplicate resources.
To keep the system simple yet secure, we can adopt a minimal two‑stack architecture that uses Pulumi’s built‑in stack outputs for non‑secret data and config secrets for anything that must stay confidential.
Requirements & Constraints
- Use Pulumi Service as the remote state backend. This provides encryption at rest, audit logging, and RBAC.
- All sensitive values must be stored as config secrets (e.g.,
pulumi config set dbPassword mySecret --secret). - Only non‑secret identifiers (IDs, URLs, ARNs) should be exported as stack outputs.
- Stacks must be deployable independently, but the
appstack must be able to reference outputs from thecorestack. - Operational checks should catch state drift, missing outputs, and concurrent updates.
Smallest Suitable Design
We keep the architecture to two stacks:
- core – provisions shared resources (e.g., VPC, subnet, IAM roles) and exports only the identifiers needed by other stacks.
- app – consumes the core stack’s outputs via
pulumi stack outputand uses config secrets for environment‑specific data.
Example directory structure:
├── core
│ ├── Pulumi.yaml
│ ├── index.ts
│ └── Pulumi.config
├── app
│ ├── Pulumi.yaml
│ ├── index.ts
│ └── Pulumi.config
└── README.md
Core stack outputs (non‑secret):
export const vpcId = vpc.id;
export const subnetIds = subnets.map(s => s.id);
export const dbEndpoint = db.endpoint;
App stack consumes them:
const coreStack = pulumi.getStack(); // e.g., "org/project/core"
const vpcId = pulumi.output(pulumi.output(coreStack).apply(s => s.vpcId));
const dbEndpoint = pulumi.output(coreStack).apply(s => s.dbEndpoint);
// Use config secrets for credentials
const dbPassword = pulumi.getConfig('dbPassword');
Trust & Data Boundaries
- Secrets: Stored in Pulumi config with
--secret. Pulumi Service encrypts them at rest and never logs the plaintext. - Outputs: The core stack exposes only non‑secret identifiers. If a piece of data is sensitive, it must stay in config and not be exported.
- RBAC: Use Pulumi Service’s role‑based access control to restrict who can view or modify each stack. Grant read‑only access to the app stack for the core stack’s outputs.
- Output filtering: If you need to expose a subset of outputs, use
pulumi stack exportand filter the JSON manually before sharing.
Operational Checks
- Validate changes:
pulumi previewon both stacks to see what will change before applying. - Stack lock: Enable
pulumi config set lock trueto prevent concurrent updates that could corrupt state. - Audit logs: In Pulumi Service UI, review the
Activitytab for each stack to confirm that state changes are recorded and that secrets remain hidden. - Show secrets: Run
pulumi stack --show-secretson the core stack to confirm that the config values are stored encrypted (the output will be masked). - Drift detection: Periodically run
pulumi refreshto detect any divergence between the real world and the state file.
Failure Modes & Redesign Triggers
- State corruption: If
pulumi refreshreports many changes that don’t match the actual infra, consider restoring the stack from a backup or re‑creating the stack. - Missing outputs: A deployment failure in the app stack that references an undefined output indicates that the core stack didn’t export it. Add the missing export and redeploy.
- Secret leakage: If a stack output contains a secret, immediately remove it from the export list and move the value back to config.
- Concurrent updates: If two developers push changes simultaneously, the stack lock will cause one to fail. Use CI gating or manual review to serialize updates.
- Access violations: If the app stack accidentally gains write access to the core stack, restrict RBAC permissions and audit logs to detect the breach.
Concrete Example
Assume an AWS VPC is provisioned in core and an ECS service in app needs the VPC ID. The steps below illustrate the minimal commands.
# Core stack
cd core
pulumi login # authenticate to Pulumi Service
pulumi stack init prod
pulumi config set aws:region us-west-2
# Export VPC ID
export const vpcId = vpc.id;
# Deploy
pulumi up
# App stack
cd ../app
pulumi login
pulumi stack init prod
pulumi config set dbPassword mySecret --secret
# Reference core stack output
const core = pulumi.getStack();
const vpcId = pulumi.output(pulumi.output(core).apply(s => s.vpcId));
# Use vpcId when creating ECS cluster
const cluster = new ecs.Cluster('appCluster', { vpcId });
# Deploy
pulumi up
After deployment, verify:
pulumi stack --show-secretsoncoreshows the VPC ID but no secrets.pulumi stack --show-secretsonappshows the DB password masked.- In Pulumi Service UI, the
Activitytab lists the two deployments with encrypted state.
Limitations & Practical Checks
- Pulumi Service is a paid feature; free tier may limit concurrent stack updates.
- Secrets are encrypted at rest, but transit encryption depends on your network configuration.
- Cross‑region deployments require careful handling of stack references; the example assumes same region.
- Always keep the Pulumi CLI up to date to benefit from the latest security patches.
To check that the design is still sound, run the following quick sanity test:
# Simulate a state corruption
cd core
sed -i 's/"id": "vpc-123"/"id": "vpc-999"/' .pulumi/stack.yaml
pulumi refresh
# Pulumi will report a drift and the app stack will fail if it tries to use the stale ID.
If drift is detected, remediate by restoring the correct state or recreating the stack. This demonstrates the importance of regular pulumi refresh checks.
Conclusion
By separating shared infrastructure into a core stack, exporting only non‑secret identifiers, and keeping sensitive data in config secrets, you maintain clear data boundaries while leveraging Pulumi Service’s secure remote state. The minimal two‑stack design scales to larger environments by adding additional dependent stacks that reference the core stack’s outputs, all while keeping operational complexity low and auditability high.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.