Skip to content

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.

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.

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"
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.

Parameter Value
Algorithm AES-256-GCM
Key size 256 bits
IV 12 bytes (random, per encryption)
Auth tag 16 bytes
Terminal window
# Set passphrase
export 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 outputs
kyku output --show-secrets
Source Example Priority
Env var KYKU_PASSPHRASE=... Highest
Docker secret file KYKU_PASSPHRASE_FILE=/run/secrets/kyku-passphrase Medium
CLI flag --passphrase "..." Lowest

If no passphrase source is found, Kyku prompts for it interactively during apply.

{
"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"
}
}
}
}

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.

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
};
Mode Passphrase Required Secret Display
plan No [encrypted]
apply Yes Decrypted for provider calls
output No [redacted]
output --show-secrets Yes Decrypted
  • 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_PASSPHRASE in 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.