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.
How It Works
Section titled “How It Works”Key Hierarchy
Section titled “Key Hierarchy”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 stackA 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
Encryption (AES-256-GCM)
Section titled “Encryption (AES-256-GCM)”- Derive the stack-specific key via HKDF
- Generate a random 12-byte nonce
- Encrypt plaintext with AES-256-GCM using the derived key and nonce
- Return
nonce || ciphertext+tagas the ciphertext blob
Decryption
Section titled “Decryption”- Derive the same stack-specific key via HKDF
- Split the blob: first 12 bytes = nonce, remainder = ciphertext+tag
- Decrypt with AES-256-GCM
- GCM’s authentication tag verifies integrity — tampered ciphertext is rejected
API Endpoints
Section titled “API Endpoints”Encrypt
Section titled “Encrypt”POST /api/stacks/{org}/{project}/{stack}/encrypt- Request:
{"plaintext": "<base64>"}— Theplaintextfield is a byte array, JSON-encoded as base64 - Response:
{"ciphertext": "<base64>"}
Decrypt
Section titled “Decrypt”POST /api/stacks/{org}/{project}/{stack}/decrypt- Request:
{"ciphertext": "<base64>"} - Response:
{"plaintext": "<base64>"}
Batch Decrypt
Section titled “Batch Decrypt”POST /api/stacks/{org}/{project}/{stack}/batch-decryptDecrypts multiple values in a single request. Used by the CLI when displaying stack outputs or config values.
Master Key Configuration
Section titled “Master Key Configuration”Development Mode
Section titled “Development Mode”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 keyThis means all dev instances with no explicit key will share the same encryption key — convenient for development, but not safe for production.
Production
Section titled “Production”Generate a random 32-byte key and set it as 64 hex characters:
export PROCELLA_ENCRYPTION_KEY="$(openssl rand -hex 32)"Security Properties
Section titled “Security Properties”| Property | Guarantee |
|---|---|
| Confidentiality | AES-256-GCM encryption |
| Integrity | GCM authentication tag detects tampering |
| Key isolation | HKDF ensures each stack has a unique key — compromising one stack’s ciphertext doesn’t help with another |
| Nonce uniqueness | 12-byte random nonce per encryption; 96-bit random nonce has negligible collision probability under normal usage |
| Timing safety | Node.js crypto module handles constant-time operations internally |
NopCryptoService
Section titled “NopCryptoService”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.