ADR-003: Master key management 

Status 

Accepted

Date 

2026-01-03

Context 

The envelope encryption system (see ADR-002: Envelope encryption) requires a master key to encrypt Data Encryption Keys (DEKs). The master key management approach must:

  • Work in various deployment environments (development, production, cloud)
  • Support key rotation without service interruption
  • Integrate with existing TYPO3 security infrastructure
  • Allow external secret management systems for enterprise deployments

Problem statement 

How should the master key be stored, retrieved, and rotated across different deployment scenarios?

Decision drivers 

  • Flexibility: Support multiple key sources (file, environment, external)
  • Zero-config default: Work out-of-the-box using TYPO3's encryption key
  • Security: Keys should never be logged or exposed
  • Rotation: Support key rotation with atomic switchover
  • Extensibility: Allow custom providers for enterprise needs

Considered options 

Option 1: Single hardcoded source 

Always derive from TYPO3's encryption key.

Pros:

  • Zero configuration
  • Always available

Cons:

  • No separation between TYPO3 and vault security
  • Cannot use external key management

Option 2: Pluggable provider system 

Interface-based providers with factory pattern for selection.

Pros:

  • Flexible deployment options
  • Enterprise integration (HashiCorp Vault, AWS KMS)
  • Testable with mock providers

Cons:

  • More complex configuration
  • Multiple code paths to maintain

Decision 

We chose a pluggable provider system with three built-in providers:

  1. typo3 (default): Derives key from TYPO3's encryption key using HKDF
  2. file: Reads key from filesystem with strict permissions
  3. env: Reads key from environment variable

This provides zero-config operation while enabling enterprise deployments.

Implementation 

Provider interface 

Classes/Crypto/MasterKeyProviderInterface.php
interface MasterKeyProviderInterface
{
    public function getIdentifier(): string;
    public function isAvailable(): bool;
    public function getMasterKey(): string;
    public function storeMasterKey(string $key): void;
    public function generateMasterKey(): string;
}
Copied!

TYPO3 provider (default) 

Uses HKDF-SHA256 to derive a vault-specific key from TYPO3's encryption key:

Classes/Crypto/Typo3MasterKeyProvider.php
final class Typo3MasterKeyProvider implements MasterKeyProviderInterface
{
    private const int KEY_LENGTH = 32;
    private const string HKDF_INFO = 'nr-vault-master-key';

    public function getMasterKey(): string
    {
        $encryptionKey = $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'];

        return hash_hkdf(
            'sha256',
            $encryptionKey,
            self::KEY_LENGTH,
            self::HKDF_INFO,
        );
    }
}
Copied!

The HKDF context string nr-vault-master-key ensures the derived key is unique to nr-vault even if other extensions use the same derivation pattern.

File provider 

Reads a 32-byte key from a file with strict permission requirements:

Classes/Crypto/FileMasterKeyProvider.php
public function getMasterKey(): string
{
    $key = file_get_contents($this->keyPath);
    $key = trim($key);  // Remove trailing newlines

    // Handle base64-encoded keys
    if (strlen($key) !== self::KEY_LENGTH) {
        $decoded = base64_decode($key, true);
        if ($decoded !== false && strlen($decoded) === self::KEY_LENGTH) {
            return $decoded;
        }
    }

    return $key;
}

public function storeMasterKey(string $key): void
{
    file_put_contents($this->keyPath, base64_encode($key));
    chmod($this->keyPath, 0o400);  // Read-only for owner
}
Copied!

Environment provider 

Reads key from environment variable (default: NR_VAULT_MASTER_KEY):

Classes/Crypto/EnvironmentMasterKeyProvider.php
public function getMasterKey(): string
{
    $key = getenv($this->envVarName);

    if ($key === false || $key === '') {
        throw MasterKeyException::environmentVariableNotSet($this->envVarName);
    }

    // Handle base64-encoded keys
    $decoded = base64_decode($key, true);
    if ($decoded !== false && strlen($decoded) === self::KEY_LENGTH) {
        return $decoded;
    }

    return $key;
}
Copied!

Factory with auto-detection 

Classes/Crypto/MasterKeyProviderFactory.php
public function getAvailableProvider(): MasterKeyProviderInterface
{
    // 1. Try explicitly configured provider
    $configured = $this->configuration->getMasterKeyProvider();
    if ($configured && $this->providers[$configured]->isAvailable()) {
        return $this->providers[$configured];
    }

    // 2. Fallback chain: typo3 -> env -> file
    foreach (['typo3', 'env', 'file'] as $id) {
        if ($this->providers[$id]->isAvailable()) {
            return $this->providers[$id];
        }
    }

    // 3. Return TYPO3 provider (will fail with clear error)
    return $this->providers['typo3'];
}
Copied!

Configuration 

Extension configuration options
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['nr_vault'] = [
    'masterKeyProvider' => 'typo3',  // typo3, file, or env
    'masterKeySource' => 'NR_VAULT_MASTER_KEY',  // env var or file path
    'autoKeyPath' => 'var/secrets/vault-master.key',  // auto-generated key
];
Copied!

Key rotation command 

Rotate master key
# Dry run first
vendor/bin/typo3 vault:rotate-master-key --dry-run

# Execute rotation
vendor/bin/typo3 vault:rotate-master-key \
    --old-key=/path/to/old.key \
    --new-key=/path/to/new.key \
    --confirm
Copied!

The rotation process:

  1. Verify old key can decrypt existing secrets
  2. Re-encrypt all DEKs with new master key (transactional)
  3. Dispatch MasterKeyRotatedEvent
  4. Update configuration to use new key

Consequences 

Positive 

  • Zero-config default: Works immediately with TYPO3 installation
  • Deployment flexibility: File/env for containers, external for enterprise
  • Key separation: HKDF ensures vault key is distinct from TYPO3 key
  • Atomic rotation: Database transaction ensures consistency
  • Extensibility: Custom providers via interface implementation

Negative 

  • Configuration complexity: Multiple options to understand
  • Key synchronization: Multi-server deployments need key distribution

Risks 

  • TYPO3 provider: Changing encryptionKey breaks vault access
  • File provider: Key file backup and distribution challenges
  • All providers: Master key loss = permanent data loss

Mitigation 

  • Document backup procedures prominently
  • Provide key export command for disaster recovery
  • Log warnings when using derived keys in production

References