ADR-020: Master key request-lifetime caching
Table of contents
Status
Accepted
Date
2026-03-28
Context
Master key providers re-read the key material from disk, environment variables, or re-derived it via HKDF on every decrypt operation. In requests that decrypt multiple secrets (e.g., frontend rendering with several vault-backed content elements), this caused repeated filesystem reads or HKDF computations for the same key material.
The master key does not change within a single HTTP request, so repeated derivation is pure overhead.
Decision
Cache the derived master key in memory for the lifetime of the current request:
- On first access, the master key provider reads/derives the key and stores it in a request-lifetime cache slot.
- Subsequent decrypt operations within the same request reuse the cached key without additional I/O or derivation.
- The cache lives in the shared
AbstractMasterKeyProviderbase class, keyed by concrete provider class, so each provider keeps an isolated slot andclearCachedKey()on one provider never wipes another's. - All providers expose a static
clearCachedKey()(declared onMasterKeyProviderInterface) that wipes the cached key material viasodium_memzero().
This follows the principle of minimizing key material exposure: the key exists in memory only for the duration of the request and is actively cleared rather than left for garbage collection.
Wipe lifecycle differs by provider
The cache is wiped by two distinct mechanisms depending on the provider:
- FileMasterKeyProvider / EnvironmentMasterKeyProvider define a
__destruct()that callsclearCachedKey(). When the provider instance is garbage-collected (typically at end of request), the cached key is zeroed. - Typo3MasterKeyProvider (the default) deliberately has no
__destruct(). Its cache slot is shared across instances, so wiping on the first instance's destruction would break the rest of the request. For this provider the cache is zeroed only by an explicitclearCachedKey()call or implicitly when PHP frees statics at script shutdown. Long-running processes (scheduler tasks, daemons) that must observe a rotated TYPO3encryptionKeyshould callclearCachedKey()explicitly.
Consequences
Positive
- One key derivation per request instead of per-decrypt, eliminating redundant I/O and HKDF computations.
- Secure cleanup:
sodium_memzero()wipes key material — via__destruct()for the File/Env providers and via an explicitclearCachedKey()for the default Typo3 provider — so it does not persist in memory beyond the request (at the latest, until PHP frees statics at shutdown for the Typo3 provider). - Transparent: Read-path callers are unaware of the caching; the
cache-wipe seam is now uniform via
clearCachedKey()on the interface.
Negative
- Memory residency: The master key remains in process memory for the
full request duration rather than being immediately discarded after each
use. For the default Typo3 provider, residency is "until an explicit
clearCachedKey()call or PHP shutdown" because it has no destructor. - Lifecycle dependency: File/Env providers rely on PHP object lifecycle
(
__destruct) for cleanup; the default Typo3 provider relies on an explicitclearCachedKey(). Long-running processes (e.g., workers, scheduler tasks) must callclearCachedKey()to bound residency and to observe a rotated source key.