---
title: "ADR-030: Read-time resolution of site-configuration vault references"
manual: "nr-vault"
version: "1.0"
permalink: "https://docs.typo3.org/permalink/netresearch/nr-vault:adr-030-site-config-vault-read-time-resolution@1.0"
source: "Developer/Adr/ADR-030-SiteConfigVaultReadTimeResolution.rst"
rendered: "2026-09-18T07:37:50+00:00"
---

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

**Table of contents**

-   [Status](https://docs.typo3.org/permalink/netresearch/nr-vault:status@1.0)
-   [Date](https://docs.typo3.org/permalink/netresearch/nr-vault:date@1.0)
-   [Context](https://docs.typo3.org/permalink/netresearch/nr-vault:context@1.0)
-   [Decision](https://docs.typo3.org/permalink/netresearch/nr-vault:decision@1.0)
-   [Consequences](https://docs.typo3.org/permalink/netresearch/nr-vault:consequences@1.0)

## Status {#status}

Accepted

## 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.0)). 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.
