---
title: "ADR-030: Read-time resolution of site-configuration vault references"
manual: "Secrets Vault"
version: "1.2"
permalink: "https://docs.typo3.org/permalink/netresearch/nr-vault:adr-030-site-config-vault-read-time-resolution@1.2"
source: "Developer/Adr/ADR-030-SiteConfigVaultReadTimeResolution.rst"
rendered: "2026-10-31T22:33:18+00:00"
---

# ADR-030: Read-time resolution of site-configuration vault references {#adr-030-site-config-vault-read-time-resolution}

**Table of contents**

-   [Status](https://docs.typo3.org/permalink/netresearch/nr-vault:status@1.2)
-   [Date](https://docs.typo3.org/permalink/netresearch/nr-vault:date@1.2)
-   [Context](https://docs.typo3.org/permalink/netresearch/nr-vault:context@1.2)
-   [Decision](https://docs.typo3.org/permalink/netresearch/nr-vault:decision@1.2)
-   [Consequences](https://docs.typo3.org/permalink/netresearch/nr-vault:consequences@1.2)
-   [Amendment: canonical namespace creation and fallback](https://docs.typo3.org/permalink/netresearch/nr-vault:amendment-canonical-namespace-creation-and-fallback@1.2)

## Status {#status}

Amended by [ADR-043](https://docs.typo3.org/permalink/netresearch/nr-vault:adr-043-site-secret-namespaces@1.2)

## Date {#date}

2026-07-23

## Context {#context}

Site configuration may reference secrets with the
`%vault(identifier)%` syntax (see
[Site configuration](https://docs.typo3.org/permalink/netresearch/nr-vault:configuration-site@1.2)). The extension originally
shipped an auto-registered event listener,
`SiteConfigurationVaultListener`, that resolved those references on
`SiteConfigurationLoadedEvent` and wrote the resolved array back onto the
event.

That eager path is unsafe because of how TYPO3 core loads site
configuration. `SiteConfiguration::getAllSiteConfigurationFromFiles()`
dispatches `SiteConfigurationLoadedEvent` **only on a cache miss** and then
`var_export()`s the post-event array — the array the listener has just
filled with decrypted plaintext — into the `core` cache, which defaults to a
file-backed backend on disk. Every later load is a cache hit that returns the
pre-resolved array without re-dispatching the event. Two guarantees break:

-   **Encryption at rest.** The decrypted secret is written verbatim into
    `var/cache/code/cache_core` in cleartext, so a filesystem read (backup,
    misconfigured permissions, a second tenant) recovers it without the master
    key, the database, or any vault access.
-   **Per-reader access control.** `VaultService::retrieve()` runs its
    `canRead()` / expiry / audit checks exactly once — in whichever context
    warms the cache (an authenticated admin request, or a CLI run). A secret
    that is not `frontend_accessible` is then served from the cache to
    anonymous frontend requests and to lower-privileged backend users, because
    the gate never runs again.

The listener also called the processor without the `Site` object, so it
always took the global-identifier branch and never distinguished
site-scoped secrets.

## Decision {#decision}

Remove `SiteConfigurationVaultListener`. Resolution of site-configuration
vault references is **caller-driven and happens at read time**, through
`SiteConfigurationVaultProcessor` (the entry point already documented in the
class and exposed as a public service):

```php
$site = $request->getAttribute('site');
$processor = GeneralUtility::makeInstance(SiteConfigurationVaultProcessor::class);
$config = $processor->processConfiguration($site->getConfiguration(), $site);
$apiKey = $config['settings']['payment']['apiKey'];
```

The cached site-configuration array keeps the literal `%vault(...)%`
placeholders; the plaintext exists only within the request that resolves it,
and `canRead()` runs for the actual reader on every resolution. Passing the
`$site` object enables site-scoped identifiers
(`site:<siteIdentifier>:<secret>`).

## Consequences {#consequences}

-   Plaintext secrets are never persisted to the on-disk `core` cache, and
    the access decision is enforced per reader rather than once per cache
    generation.
-   **Behaviour change.** Consumers that relied on transparent resolution —
    reading `$site->getConfiguration()[...]` and receiving a decrypted value —
    must now call `processConfiguration()` at the point of use. That
    transparent behaviour was the unsafe path; the explicit call is the
    supported one.
-   The processor, its interface, and its public service registration are
    unchanged; only the eager listener and its tests are removed.
-   TypoScript resolution (`TypoScriptVaultListener`) is unaffected: it is
    gated on `frontend_accessible` and documented as making a secret
    frontend-readable by design.

## Amendment: canonical namespace creation and fallback {#amendment-canonical-namespace-creation-and-fallback}

2026-10-10

[ADR-043](https://docs.typo3.org/permalink/netresearch/nr-vault:adr-043-site-secret-namespaces@1.2) completes the namespace through
the existing storage API, CLI and specific Vault table creation path.
The configured site is required only for a new namespace; existing encrypted
rows retain their original authenticated identifier after site rename/delete.
Global fallback is permitted only for an actually missing namespaced row.
Denial, expiry, disabled state, corruption or operational failure keeps the
placeholder unresolved, rather than substituting another credential.
The explicit read-time resolution and core-cache boundary of this ADR remain.
