Key custody 

Where the master key lives under each provider, who can read it, and how it is rotated. The one question that decides everything on this page: who else runs as the PHP process user? Anything readable by that user is readable by the vault's attacker in a process compromise.

Design rationale: ADR-003: Master key management. Request-lifetime caching: ADR-020: Master key request-lifetime caching.

Provider comparison 

Provider Key at rest Who can read it Rotation Hardened profile
typo3 Nowhere — derived on demand from SYS/encryptionKey with HKDF-SHA256. Anyone who can read config/system/settings.php, plus every backup of it. Only by rotating TYPO3's encryptionKey, which orphans every stored secret unless the vault is rotated in the same operation. Rejected (1753900002).
file A file outside the web root, 0400, path in masterKeySource. The file's owner, and root. Not the database. vault:rotate-master-key writes the new key in place. Permitted.
env An environment variable named by masterKeySource (default NR_VAULT_MASTER_KEY). Anything that can read the process environment: the process itself, root, and whatever injected it. Out of band — the provider cannot persist a value. Set the new variable, restart, then rotate. Permitted.
transit Only the wrapped ciphertext (vault:v1:…) locally, at hashicorp.transitWrappedKeyPath. Unwrapping is a live Vault call. Anyone holding the Vault token and the wrapped blob. Neither alone suffices. Two independent rotations — see Transit key rotation is a different operation. Permitted; it is the kind of external custody the profile asks for.

typo3 — derived from the TYPO3 encryption key 

The default, and the only zero-configuration option. HKDF-SHA256 over SYS/encryptionKey with the domain-separation string nr-vault-master-key, yielding 32 bytes. Source keys shorter than 32 characters are refused outright — they would give HKDF far less than 256 bits of entropy.

What it does not do is separate the vault from TYPO3. The derivation is sound; the custody is not improved at all. See The typo3 provider does not separate the vault from TYPO3.

Two operational consequences that catch people out:

  • encryptionKey is vault key material for as long as this provider is configured. Rotating it orphans every secret.
  • The provider defines no destructor, so its request-lifetime cache slot survives individual instances. Long-running processes — scheduler daemons, messenger workers — must call Typo3MasterKeyProvider::clearCachedKey() to observe a rotated encryptionKey.

Acceptable for: development, staging, and installations where the secrets are no more sensitive than the rest of settings.php. Not acceptable for a hardened deployment, and the profile enforces that.

file — a key file outside the web root 

Extension configuration
masterKeyProvider = file
masterKeySource = /var/lib/typo3-secrets/vault-master.key
Copied!

The path must be outside the document root. The file is written with the umask tightened to 0o077 before the write and then chmod to 0400, so there is no window in which it is world-readable — an ordering that matters on hosts with permissive umasks.

Recommended ownership
# Owned by the user the PHP process runs as; nothing else needs it.
install -d -m 0700 -o www-data -g www-data /var/lib/typo3-secrets
chown www-data:www-data /var/lib/typo3-secrets/vault-master.key
chmod 0400 /var/lib/typo3-secrets/vault-master.key
Copied!

The provider accepts the file as raw 32 bytes or as base64 (trailing newlines are tolerated). Anything else is rejected on length rather than padded.

Checklist: outside the web root; 0400; owned by the PHP user; excluded from application backups and included in a separate key backup (Backup and restore); on a filesystem whose snapshots you also control; access-logged if the platform allows it.

env — an environment variable 

Extension configuration
masterKeyProvider = env
masterKeySource = NR_VAULT_MASTER_KEY
Copied!

The natural fit for containers and any platform with a secret-injection mechanism (Kubernetes secrets, systemd LoadCredential, a supervisor's environment file). Raw 32 bytes or base64; the provider zeroes the base64 string once decoded.

Where environment variables leak. Process listings on some platforms, phpinfo(), crash dumps, /proc/<pid>/environ for the same user or root, child processes, and — commonly — CI logs and container inspection output. Verify that yours does not, rather than assuming.

The provider cannot persist a value, so vault:rotate-master-key cannot write the new key for you: set the variable in the injection mechanism, restart the process, and rotate with the new value supplied explicitly.

transit — wrapped by HashiCorp Vault 

Extension configuration
masterKeyProvider = transit
hashicorp.address = https://vault.example.internal:8200
hashicorp.authMethod = token
hashicorp.tokenEnvVar = VAULT_TOKEN
hashicorp.transitMount = transit
hashicorp.transitKeyName = nr-vault-master
hashicorp.transitWrappedKeyPath = /var/lib/typo3-secrets/vault-master.wrapped
Copied!

The master key is generated once, wrapped by Vault's transit engine, and only the ciphertext is stored locally. Every unwrap is a POST to /v1/{mount}/decrypt/{key}.

Custody notes.

  • Token, not configuration. The token is read from hashicorp.tokenEnvVar in preference to hashicorp.token, because a token in the extension configuration is readable in the Install Tool and lands in configuration exports. Leave hashicorp.token empty in production.
  • Token auth only. approle and kubernetes are rejected rather than silently downgraded to something weaker.
  • Least-privilege policy. The token needs update on transit/decrypt/<key>, plus update on transit/encrypt/<key> only for initialisation and rotation. Nothing else. Withhold transit/keys/* so the vault cannot delete or export its own key.
  • Path safety is enforced. Mount segments and the key name must match [A-Za-z0-9._-]+ and must not be . or .., so a configured mount cannot traverse the API path. A nested mount such as platform/transit stays usable.
  • The wrapped file is written safely. 0600 (rotation must overwrite it) via write-to-temp-then-rename, because a half-written wrapped key is an unrecoverable vault rather than a failed write.
  • Errors are redacted. Token-shaped strings are stripped from transport error messages, and non-2xx response bodies are never surfaced — Vault echoes the submitted ciphertext on some error paths.

What it buys and what it does not. Custody, rotation and central, revocable audit of every unwrap; a stolen database plus a stolen webroot is useless. It does not protect a live attacker inside the request — see A KMS protects custody, not runtime. And it makes Vault an availability dependency: isAvailable() performs no network call so an outage does not become a per-request timeout, but the first real key load will fail.

HSM and cloud KMS 

nr-vault has no direct HSM or cloud-KMS integration. The transit provider is the supported indirection: HashiCorp Vault can itself be backed by an HSM or a cloud KMS for its own seal, which puts the vault's master key under that custody transitively without nr-vault needing a provider per KMS vendor.

Anything else — AWS KMS, Azure Key Vault, GCP KMS directly — would need a new MasterKeyProviderInterface implementation. The interface is a single method, and AbstractMasterKeyProvider already implements the caching and wiping contract, so the surface is small; but it does not exist today, and no configuration setting will produce it.

What custody cannot fix 

Regardless of provider, the master key is present in the PHP process for the duration of a request that touches a secret. It is cached in a static keyed by provider class, and wiped with sodium_memzero() when the slot is cleared — but while it is there, code running in that process can read it.

Custody decides who can obtain the key outside the request. It has no opinion about the request itself. Everything on this page is about the former.