Skip to content

Encryption

Procella encrypts Pulumi secrets at rest using AES-256-GCM with per-stack key derivation. When you run pulumi config set --secret, the CLI sends the plaintext to the server, which encrypts it before storing.

Master Key (32 bytes, from PROCELLA_ENCRYPTION_KEY)
├── HKDF(masterKey, salt="org/project/stack", info="procella-encrypt")
│ └── Stack-specific key (32 bytes) → AES-256-GCM
├── HKDF(masterKey, salt="org/project/other-stack", info="procella-encrypt")
│ └── Different stack-specific key (32 bytes) → AES-256-GCM
└── ... one derived key per stack

A single master key derives unique encryption keys per stack using HKDF (HMAC-based Key Derivation Function):

  • Hash: SHA-256
  • Input Key Material (IKM): The master key (32 bytes)
  • Salt: The stack’s fully qualified name (org/project/stack)
  • Info: "procella-encrypt" (fixed context string)
  • Output: 32-byte AES-256 key unique to each stack
  1. Derive the stack-specific key via HKDF
  2. Generate a random 12-byte nonce
  3. Encrypt plaintext with AES-256-GCM using the derived key and nonce
  4. Return nonce || ciphertext+tag as the ciphertext blob
  1. Derive the same stack-specific key via HKDF
  2. Split the blob: first 12 bytes = nonce, remainder = ciphertext+tag
  3. Decrypt with AES-256-GCM
  4. GCM’s authentication tag verifies integrity — tampered ciphertext is rejected
POST /api/stacks/{org}/{project}/{stack}/encrypt
  • Request: {"plaintext": "<base64>"} — The plaintext field is a byte array, JSON-encoded as base64
  • Response: {"ciphertext": "<base64>"}
POST /api/stacks/{org}/{project}/{stack}/decrypt
  • Request: {"ciphertext": "<base64>"}
  • Response: {"plaintext": "<base64>"}
POST /api/stacks/{org}/{project}/{stack}/batch-decrypt

Decrypts multiple values in a single request. Used by the CLI when displaying stack outputs or config values.

If PROCELLA_ENCRYPTION_KEY is not set and PROCELLA_AUTH_MODE=dev, a deterministic key is auto-generated:

import { createHash } from "node:crypto";
const key = createHash("sha256").update("procella-dev-encryption-key").digest("hex");
// key is used as the 64-char hex master key

This means all dev instances with no explicit key will share the same encryption key — convenient for development, but not safe for production.

Generate a random 32-byte key and set it as 64 hex characters:

Terminal window
export PROCELLA_ENCRYPTION_KEY="$(openssl rand -hex 32)"
PropertyGuarantee
ConfidentialityAES-256-GCM encryption
IntegrityGCM authentication tag detects tampering
Key isolationHKDF ensures each stack has a unique key — compromising one stack’s ciphertext doesn’t help with another
Nonce uniqueness12-byte random nonce per encryption; 96-bit random nonce has negligible collision probability under normal usage
Timing safetyNode.js crypto module handles constant-time operations internally

If no encryption key is configured and the server is not in dev mode, a NopCryptoService is used that returns errors for all encrypt/decrypt operations. This prevents accidental plaintext storage — the Pulumi CLI will fail with a clear error when trying to set secrets.