State Encryption
Kyku encrypts sensitive fields in state files using AES-256-GCM with PBKDF2 key derivation via the Web Crypto API. No external dependencies required.
Why Encryption
Section titled “Why Encryption”State files contain sensitive data — database passwords, API tokens, secret keys. Kyku encrypts these fields so state files are safe to commit to version control or share with CI systems.
Architecture
Section titled “Architecture”Passphrase (KYKU_PASSPHRASE) │ ▼PBKDF2 (600,000 iterations, SHA-256, random salt) │ ▼256-bit AES key │ ▼AES-256-GCM (random IV per encryption) │ ▼Ciphertext + auth tag → stored in state as "ENC:base64-ciphertext:base64-tag"Key Derivation
Section titled “Key Derivation”| Parameter | Value |
|---|---|
| Algorithm | PBKDF2 |
| Hash | SHA-256 |
| Iterations | 600,000 |
| Output | 256-bit key |
Why PBKDF2 over Argon2id? Bun’s
Bun.password.hash()returns a formatted Argon2id hash string, not raw key bytes. PBKDF2 is a NIST-standard KDF natively available in Web Crypto, making it the correct tool for deriving AES-256-GCM keys from passphrases.
Encryption
Section titled “Encryption”| Parameter | Value |
|---|---|
| Algorithm | AES-256-GCM |
| Key size | 256 bits |
| IV | 12 bytes (random, per encryption) |
| Auth tag | 16 bytes |
# Set passphraseexport KYKU_PASSPHRASE="your-secure-passphrase"
# Plan (works without passphrase — secrets shown as [encrypted])kyku plan
# Apply (requires passphrase for decryption)kyku apply --auto-approve
# Show decrypted outputskyku output --show-secretsPassphrase Sources
Section titled “Passphrase Sources”| Source | Example | Priority |
|---|---|---|
| Env var | KYKU_PASSPHRASE=... |
Highest |
| Docker secret file | KYKU_PASSPHRASE_FILE=/run/secrets/kyku-passphrase |
Medium |
| CLI flag | --passphrase "..." |
Lowest |
Interactive Mode
Section titled “Interactive Mode”If no passphrase source is found, Kyku prompts for it interactively during apply.
State File Format
Section titled “State File Format”{ "version": "2.0", "encrypted": true, "encryptionMeta": { "algorithm": "aes-256-gcm", "kdf": "pbkdf2", "kdfParams": { "iterations": 600000, "hash": "SHA-256", "salt": "base64-encoded-salt" }, "iv": "base64-encoded-iv" }, "resources": { "db-123": { "type": "Database", "config": { "name": "app-db", "password": "ENC:base64-ciphertext:base64-tag" } } }}Encryption Metadata
Section titled “Encryption Metadata”The encryptionMeta block is stored once per state file. All encrypted fields use the same key derivation (same salt). Each field gets its own random IV.
Which Fields Are Encrypted
Section titled “Which Fields Are Encrypted”Fields are encrypted if they match these name patterns (case-insensitive):
*password*secret*token*credential
Or if the field type is Secret<T>:
import { Secret } from '@kykucloud/types';
const config = { apiKey: 'sk-...' as Secret<string>, // Will be encrypted};Plan Mode vs Apply Mode
Section titled “Plan Mode vs Apply Mode”| Mode | Passphrase Required | Secret Display |
|---|---|---|
plan |
No | [encrypted] |
apply |
Yes | Decrypted for provider calls |
output |
No | [redacted] |
output --show-secrets |
Yes | Decrypted |
Security Best Practices
Section titled “Security Best Practices”- Strong passphrase: Use at least 16 characters with mixed case, numbers, and symbols
- Never hardcode: Use env vars, Docker secrets, or CI secret stores
- Rotate periodically: Change the passphrase by re-encrypting state
- SSH keys: Only public keys in config; private keys never touch Kyku
- CI: Store
KYKU_PASSPHRASEin your CI platform’s secret store
Can I commit state to git? Yes. Encrypted state files are safe to commit. Only the passphrase holder can decrypt secrets.
What if I lose the passphrase? Encrypted secrets are unrecoverable. You would need to recreate the resources with new credentials.
Does encryption affect plan diffs? No. The diff engine compares config shapes, not encrypted values. Plan output shows [encrypted] for secret fields regardless of changes.
Can I use my own encryption? Not currently. Kyku handles encryption internally. CustomResource handlers can use setSecret() to mark values for encryption.