Multi-Environment Key Management & Production Workflows#
This guide covers advanced multi-environment architectures for Easy Encryption. It details how to set up production environments where private decryption keys are restricted to DevOps, how developers work in "encrypt-only" local environments without access to production decryption keys, and how customers can self-rotate third-party credentials without exposing plaintext secrets in database dumps, Git repositories, or server environment variables.
1. Architectural Principles#
Easy Encryption is designed around asymmetric public-key cryptography (Libsodium sealed box via crypto_box_seal / crypto_box_seal_open). Understanding how asymmetric encryption intersects with Drupal's Configuration Synchronization (config/sync) is essential for multi-environment deployments.
┌──────────────────────────────────────────────┐
│ DevOps / Infra │
│ Generates Master Libsodium Key Pair (X25519) │
└──────┬────────────────────────────────┬──────┘
│ │
Private Key │ │ Public Key
(Secret Hex) │ │ (Safe for Git)
▼ ▼
┌───────────────────────────────┐ ┌──────────────────────────────┐
│ PRODUCTION SERVER │ │ GIT REPOSITORY │
│ EASY_ENCRYPTION_PRIVATE_KEY │ │ (config/sync) │
│ (Environment Var) │ │ - Public Key Entity │
└──────────────┬────────────────┘ │ - Private Key Entity Def │
│ │ (provider: env) │
│ Decrypts │ - Application Key Stubs │
│ Credentials └──────────────┬───────────────┘
▼ │
┌───────────────────────────────┐ │ Cloned by Devs / CI
│ RUNTIME DECRYPTION │ ▼
│ Third-party credentials │ ┌──────────────────────────────┐
│ available ephemerally in │ │ DEVELOPER / CI / QA │
│ memory only │ │ "Encrypt-Only" Mode │
└───────────────────────────────┘ │ - Can encrypt new keys │
│ - CANNOT decrypt production │
│ - Uses local mock overrides │
└──────────────────────────────┘
Master Key Pair vs. Application Data Keys#
- Master Encryption Key Pair (Easy Encryption): A single Libsodium key pair (X25519). The public key encrypts data; the private key decrypts data.
- Application Data Keys (Third-Party Credentials): Sensitive credentials (e.g., Stripe, OpenAI, Mailchimp, CRM API tokens) stored in Drupal
Keyentities configured with theeasy_encryptedprovider. These values are stored as encrypted ciphertexts.
Encrypt-Only Environments#
Because sealed-box encryption requires only the public key to encrypt values, environments that do not need to execute live third-party integrations (such as local development, staging/QA, and CI runners) do not need the private key. They can run in an "encrypt-only" state, safely authoring, testing, and exporting encrypted configuration.
The Drupal Configuration Sync Boundary#
In Drupal, Key entities are configuration entities. Both the public key and any encrypted third-party credentials live in config/sync. Because the public key is cryptographically bound to its matching private key, the same public key must correspond to the private key deployed on the environment responsible for decryption.
2. Scenario 1: DevOps-Led Production Setup (Private Key via Environment Variable)#
Threat Model & Objectives#
- Developers must never know or possess the production private decryption key.
- The production private key must be injected via a server environment variable (
EASY_ENCRYPTION_PRIVATE_KEY). - The public key and Key entity metadata are tracked in version control (
config/sync).
Step 1: DevOps Generates the Key Pair Out-of-Band#
To ensure the private key never touches developer machines, authorized DevOps personnel generate the key pair in a secure environment.
Using standard PHP CLI (requires ext-sodium):
php -r '
$kp = sodium_crypto_box_keypair();
echo "PUBLIC KEY (hex):\n" . sodium_bin2hex(sodium_crypto_box_publickey($kp)) . "\n\n";
echo "PRIVATE KEY (hex):\n" . sodium_bin2hex(sodium_crypto_box_secretkey($kp)) . "\n";
'
Step 2: Inject Private Key on Production#
Place the raw 64-character hex string into the production server's runtime environment (Kubernetes Secret, AWS Secrets Manager, systemd environment, .env, etc.):
EASY_ENCRYPTION_PRIVATE_KEY="c4b2...<64-hex-characters>...81a0"
Avoid Copying PHP Wrappers
If inspecting a private key file generated by Easy Encryption's default file provider (.easy_encryption/<id>.php), the key is wrapped in PHP code: <?php return 'hex...';.
When setting an environment variable for Key module's env provider, extract only the raw hex string. Do not include <?php return '...';.
Step 3: Configure Drupal Key Entities for the env Provider#
Drupal's Key module separates key configuration metadata from the actual secret value. The configuration entity itself contains no secret data and can be safely committed to Git.
Create or update the following configuration files in your project's config/sync directory (replacing <key_id> with your chosen machine name, e.g., site_encryption_key):
key.key.<key_id>_private_key.yml:
langcode: en
status: true
dependencies:
module:
- key
id: <key_id>_private_key
label: 'Easy Encryption: Site private key'
description: "The site's private key used to decrypt (read) secure credentials."
key_type: encryption
key_type_settings:
key_size: 512
key_provider: env
key_provider_settings:
env_variable: EASY_ENCRYPTION_PRIVATE_KEY
base64_encoded: false
key.key.<key_id>_public_key.yml:
langcode: en
status: true
dependencies:
module:
- key
id: <key_id>_public_key
label: 'Easy Encryption: Site public key'
description: "The site's public key used to encrypt (store) sensitive credentials."
key_type: encryption
key_type_settings:
key_size: 512
key_provider: config
key_provider_settings:
key_value: '<paste-public-key-hex-here>'
easy_encryption.keys.yml:
active_encryption_key_id: '<key_id>'
encryption_keys:
- encryption_key_id: '<key_id>'
Step 4: Commit and Deploy Configuration#
- Commit the three configuration files above to Git.
- Deploy to production and run
drush config:import. - Verify the setup on the Drupal Status Report (
/admin/reports/status). The check for Easy Encryption: Active Sodium key pair should report green, indicating both encryption and decryption capabilities are active.
3. Scenario 2: Developer Workflow & Local Sandbox Keys#
When developers pull the repository, their local environments have the public key in configuration, but lack EASY_ENCRYPTION_PRIVATE_KEY.
The Problem: Decryption Failures on Local Environments#
If application code attempts to fetch a key that was encrypted with the production public key:
$key_value = \Drupal::service('key.repository')->getKey('payment_gateway')->getKeyValue();
Drupal invokes EasyEncryptedKeyProvider::getKeyValue(). Because the local machine does not have the production private key, Libsodium cannot open the sealed box and throws an EncryptionException.
Solution Part A: Use Stub Keys in Git#
Easy Encryption supports stub (placeholder) keys. When a Key entity has no ciphertext value (value is omitted or set to ''):
- Easy Encryption skips encryption entirely.
- Calling
$key->getKeyValue()on a stub key returns''without attempting decryption or logging errors.
In Git (config/sync), define the application key as an unpopulated stub referencing your active encryption key ID:
# key.key.payment_gateway.yml
id: payment_gateway
label: 'Payment Gateway API Key'
key_type: authentication
key_provider: easy_encrypted
key_provider_settings:
encryption_key_id: '<key_id>'
value: ''
Solution Part B: Local Development Overrides in settings.local.php#
To test integrations locally, developers need valid sandbox/test credentials.
Do Not Override Only key_provider_settings.value
If you set $config['key.key.payment_gateway']['key_provider_settings']['value'] = 'sk_test_123';, Easy Encryption will treat 'sk_test_123' as Libsodium ciphertext, attempt to decrypt it, and throw a fatal decryption error.
To provide a local mock value, override both the key provider and the provider settings:
// In settings.local.php (or environment-specific settings):
$config['key.key.payment_gateway']['key_provider'] = 'config';
$config['key.key.payment_gateway']['key_provider_settings'] = [
'key_value' => 'sk_test_51Mz...sandbox_token',
];
Alternatively, override the provider to use a local environment variable:
$config['key.key.payment_gateway']['key_provider'] = 'env';
$config['key.key.payment_gateway']['key_provider_settings'] = [
'env_variable' => 'LOCAL_PAYMENT_SANDBOX_KEY',
'base64_encoded' => false,
];
Why this works:
- Drupal configuration overrides run in-memory for the current request.
$key->getKeyProvider()resolves toConfigKeyProvider(orEnvKeyProvider), completely bypassing Easy Encryption.$key->getKeyValue()returns the plaintext sandbox token directly.- No private key is required, and
config/syncremains clean.
4. Scenario 3: Zero Plaintext in Production & Customer Self-Service Rotation#
Threat Model & Objectives#
- Zero Plaintext at Rest: Third-party credentials must never appear in plaintext anywhere on the production server—not in database tables, not in configuration YAML files, and not in server environment variables.
- Customer Self-Service Rotation: Business users or site administrators must be able to rotate API keys via the Drupal Admin UI without developer or DevOps intervention.
- Deployment Stability: Automated CI/CD deployments (
drush config:import) must not overwrite or revert customer-rotated keys in production.
Step 1: Customer Rotates Key via Drupal Admin UI#
- The customer navigates to Configuration > System > Keys (
/admin/config/system/keys). - They edit the target key (e.g.,
Payment Gateway API Key). - Under Key provider,
Easy Encryptedis selected. - The customer pastes the new API secret into the Key value field and clicks Save.
- Under the hood:
- Easy Encryption encrypts the value in memory using the active public key.
- Only the hex ciphertext and the active encryption key ID are stored in Drupal's active configuration database table (
config). - The plaintext is immediately cleared from memory using
sodium_memzero(). - The new credential is live immediately—no server restart, cache rebuild, or pipeline trigger required.
Step 2: Prevent CI/CD Overwrite with Config Ignore#
Because the customer saved the key directly in production, the active database configuration now contains ciphertext that differs from the stub key committed to Git.
On the next CI/CD deployment, standard drush config:import would detect this difference and revert the active configuration back to the stub key in Git.
To prevent this, install and configure the Config Ignore module (drupal/config_ignore):
In config_ignore.settings.yml:
ignored_config_entities:
# Protect the specific application keys managed directly in production:
- 'key.key.payment_gateway'
- 'key.key.openai_api_key'
# Alternatively, ignore only the provider settings across all keys:
- 'key.key.*:key_provider_settings'
With Config Ignore active:
- Developers can deploy core updates, module updates, and general site configuration safely.
drush config:importignores the customer-rotated Key entities.- The customer retains full autonomy over credential lifecycle management.
5. Troubleshooting & Operational Footguns#
| Symptom | Root Cause | Resolution |
|---|---|---|
SodiumException: sodium_crypto_box_seal_open(): invalid ciphertext |
settings.local.php overrode key_provider_settings.value with a plaintext sandbox string while keeping key_provider: easy_encrypted. |
Change key_provider to 'config' (or 'env') in the $config override array alongside the value. |
| Decryption fails immediately on Production after setting env var. | The environment variable contains the PHP wrapper <?php return '...'; instead of raw hex. |
Ensure EASY_ENCRYPTION_PRIVATE_KEY contains only the 64-character hexadecimal string. |
| Status report shows: "Sodium key pair is incomplete (can encrypt, cannot decrypt)". | The environment variable is missing, empty, or misnamed on the production server. | Verify that EASY_ENCRYPTION_PRIVATE_KEY is loaded into the PHP process environment (check phpinfo() or getenv()). |
| Customer rotated a key in Production, but after the next deployment the integration broke. | drush config:import reverted active configuration to the stub key stored in config/sync. |
Add the key entity to config_ignore.settings.yml under ignored_config_entities. |
Decryption fails with Key pair not found after key rotation. |
The key was encrypted with an older key pair that was deleted or whose private key was removed. | Ensure previous private keys remain available, or run drush easy-encryption:rotate --reencrypt during rotation to migrate existing ciphertexts to the new active key. |