Usage 

Backend module 

Access the vault through the TYPO3 backend:

  1. Go to Admin Tools > Vault.
  2. The overview shows statistics and quick-start examples.
  3. Navigate to Secrets to manage your secrets.
Vault module overview showing statistics and quick start guide

The vault overview displays key metrics and provides quick-start code examples

The overview also carries a Security Readiness panel: the active security profile, an "N of M controls passed" ratio, and the open findings with the risk each one carries and the command that fixes it. The detailed finding list requires the vault.configure permission, because it names this installation's concrete weak points; everyone else sees the profile badge and the ratio.

The panel and vault:doctor evaluate the same controls, so the module and a CI gate cannot disagree. Use the command for anything scheduled or automated — see Deployment gate.

Creating secrets 

  1. Click Create Secret (+ button).
  2. Fill in the form:

    Identifier
    Unique identifier for the secret (e.g., stripe_api_key).
    Value
    The secret value to encrypt.
    Description
    Optional description for documentation.
    Context
    Optional context for organization (e.g., payment).
    Allowed groups
    Backend user groups that can access this secret.
    Expiration
    Optional expiration date after which the secret becomes inaccessible.
  3. Click Save.

Viewing and editing secrets 

Secrets are displayed with their metadata but not their values. Click Reveal to temporarily show a secret value.

Secrets list view showing secret identifiers, contexts, and metadata

The secrets list provides filtering, bulk actions, and quick access to secret operations

Analytics 

The Analytics submodule gives administrators an at-a-glance view of secret usage and highlights secrets that may be safe to remove. Choose the evaluation window with the 30d / 90d / 180d / 365d selector at the top.

Vault Analytics module with KPI cards, usage distribution, and a redaction-candidates table

The analytics dashboard summarises usage and flags redaction candidates

Key metrics 

Total secrets
Active (non-deleted) secrets in the vault.
Expired
Secrets whose expiration date has passed but that still exist.
Redaction candidates
Secrets flagged by at least one staleness rule (see below).
Frontend-accessible
Secrets marked frontend_accessible - review these with extra care.
Never rotated
Secrets that have never been rotated within the configured threshold.
Reads in window (automated / manual)
Read activity for the selected window, split into automated reads (CLI, scheduler, API) and manual reveals performed in the backend.

Redaction candidates 

The table lists every flagged secret together with the rule(s) that flagged it, its last read of any kind, the automated / manual read split for the window, and its age in days. Open deep-links straight to the secret record so you can review or delete it.

Secrets are flagged by these rules:

Dead
Never read and older than the threshold, or not read for a long time - the primary deletion candidate.
Expired
Past its expiration date but still present in the vault.
Never rotated
Older than the rotation threshold without ever having been rotated.
Automation-stale
Revealed manually but never read by automation. This is a review signal rather than a deletion signal: the secret may legitimately be used only through manual workflows. It is therefore never combined with Dead.

Site configuration 

Reference secrets in your site configuration files using the %vault(identifier)% syntax:

config/sites/mysite/config.yaml
settings:
  payment:
    stripePublicKey: 'pk_live_...'
    stripeSecretKey: '%vault(stripe_secret_key)%'
  email:
    mailchimpApiKey: '%vault(mailchimp_api_key)%'
    sendgridToken: '%vault(sendgrid_token)%'
Copied!

References are not resolved automatically when TYPO3 loads the site configuration. Resolve them explicitly, at the point of use, with SiteConfigurationVaultProcessor :

Resolve site-configuration secrets at read time
use Netresearch\NrVault\Configuration\SiteConfigurationVaultProcessor;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$site = $request->getAttribute('site');
$processor = GeneralUtility::makeInstance(SiteConfigurationVaultProcessor::class);
$config = $processor->processConfiguration($site->getConfiguration(), $site);
$stripeSecret = $config['settings']['payment']['stripeSecretKey'];
Copied!

This keeps sensitive values out of your version control while still allowing you to configure them through the familiar site settings.

TypoScript integration 

Use vault references in TypoScript for frontend-accessible secrets:

TypoScript vault reference
lib.googleMapsKey = TEXT
lib.googleMapsKey {
  value = %vault(google_maps_api_key)%
  stdWrap.cache.disable = 1
}

page.headerData.10 = TEXT
page.headerData.10 {
  value = <script>var API_KEY = '%vault(public_api_key)%';</script>
  stdWrap.cache.disable = 1
}
Copied!

Which placeholders resolve in the frontend 

The listener that expands %vault(...)% runs on the output of every stdWrap call. That includes strings the integrator did not author: an editor-written tt_content field rendered with `stdWrap.field = bodytext``, or a request parameter rendered with ``data = GP:q`. Without a restriction, anyone who can type into a content element — or append a query string — could name any frontend-accessible secret and have it expanded into the (cacheable) page.

In a frontend request — and on the command line — the extension therefore resolves an identifier only when it was published through a source an editor cannot write:

A1 — frontend TypoScript. The identifier appears in the setup array, i.e. somewhere in the site's TypoScript. sys_template is admin-only and site TypoScript lives on disk. This covers every documented example on this page: writing lib.apiKey.value = %vault(my_api_key)% publishes my_api_key.

A2 — site configuration and site settings. An identifier used anywhere in config/sites/<site>/config.yaml or in the site settings is published for that site. This is what keeps site configuration values usable in content.

A3 — the explicit list. For an identifier that is used only in a Fluid template file, a userFunc or a DataProcessor — that is, nowhere in the setup array — name it once per site:

Publish identifiers that appear nowhere else in TypoScript
plugin.tx_nrvault.frontendResolvableIdentifiers = my_api_key, public_widget_token
Copied!

A4 — integrator PHP. In an eID handler or any other entry point that has no TypoScript and no site attribute, publish the identifier for the current request:

Publish an identifier from PHP
use Netresearch\NrVault\Security\FrontendPlaceholderPolicyInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;

// $request is the PSR-7 request your handler received.
GeneralUtility::makeInstance(FrontendPlaceholderPolicyInterface::class)
    ->allowIdentifier('my_api_key', $request);
Copied!

The request argument is not decoration, and it is not a freshness hint either: it is the key. The policy is a shared service that lives for the whole PHP process, so the grant is stored against that request object in a \WeakMap , and a later request — an anonymous frontend render in the same worker process, possibly on another site — holds a different object and cannot address it.

Failure is loud, but quiet in production. An unpublished identifier leaves the literal %vault(identifier)% in the output. In Development context a single notice per request names the first rejected identifier; in every other context the skip path writes nothing, so unauthenticated input cannot drive log volume.

Scope of the restriction. It applies to frontend requests, to any web request whose type cannot be established (eID among them, where $GLOBALS['TYPO3_REQUEST'] does not exist), and to the command line — scheduler, Symfony Messenger, console commands. Backend requests are unaffected, a backend request being recognised by the request the renderer itself carries and never by what an earlier request left in $GLOBALS['TYPO3_REQUEST'] . The check is on the identifier, not on where the placeholder sits: an identifier this site already publishes stays resolvable wherever it appears.

The command line is covered because scheduler:run authenticates the _cli_ administrator: the admin bypass grants the read, so this allow-set is the only gate left on editor-authored content that a scheduled newsletter or export job renders. A deployment whose internal render jobs genuinely need the old behaviour opts back into it with frontendPlaceholderLegacyCli; publishing the identifiers is the narrower remedy.

One further case is worth naming: on a fully cached page hit with no USER_INT or COA_INT object, core's frontend TypoScript factory returns before the setup array is built, so A1 and A3 are empty for that request and only A2 and A4 apply. No documented example is affected, and the direction is fail-closed. See ADR-035.

CLI commands 

vault:init 

Initialize the vault and generate a master key:

Initialize vault
vendor/bin/typo3 vault:init

# Output as environment variable format
vendor/bin/typo3 vault:init --env

# Specify custom output location
vendor/bin/typo3 vault:init --output=/secure/path/vault.key
Copied!

vault:store 

Create or update a secret:

Store a secret
# Interactive (prompts for value)
vendor/bin/typo3 vault:store stripe_api_key

# With options (arbitrary metadata via repeatable --metadata key=value)
vendor/bin/typo3 vault:store payment_key \
  --value="sk_live_..." \
  --metadata="description=Stripe production key" \
  --metadata="context=payment" \
  --groups="1,2"
Copied!

vault:retrieve 

Retrieve a secret value:

Retrieve a secret
vendor/bin/typo3 vault:retrieve stripe_api_key

# Quiet mode for scripting
API_KEY=$(vendor/bin/typo3 vault:retrieve -q stripe_api_key)
Copied!

vault:list 

List all accessible secrets:

List secrets
vendor/bin/typo3 vault:list

# Filter by pattern
vendor/bin/typo3 vault:list --pattern="payment_*"

# JSON output for automation
vendor/bin/typo3 vault:list --format=json
Copied!

vault:rotate 

Rotate a secret with a new value:

Rotate a secret
vendor/bin/typo3 vault:rotate stripe_api_key \
  --reason="Scheduled quarterly rotation"
Copied!

vault:delete 

Delete a secret:

Delete a secret
vendor/bin/typo3 vault:delete old_api_key \
  --reason="Service deprecated" \
  --force
Copied!

vault:audit 

View the audit log:

View audit log
# View entries since a given date
vendor/bin/typo3 vault:audit --since=2026-05-01

# Filter by secret
vendor/bin/typo3 vault:audit --identifier=stripe_api_key

# Export to JSON
vendor/bin/typo3 vault:audit --format=json > audit.json
Copied!

Verify the tamper-evident chain:

Verify audit chain integrity
vendor/bin/typo3 vault:audit --verify
Copied!

The output ends with a Tip anchor: line. The anchor is what detects removal of the end of the log — a truncation leaves no gap and no broken link, so the chain walk alone cannot see it (see Tip anchor (truncation detection)).

ok
The anchored entry is still present and unchanged.
NOT ARMED
No anchor recorded yet. It arms itself on the next audit log write.
VIOLATED
The anchored entry is gone or was replaced — the log was truncated or wiped. Snapshot the table before changing anything else.
UNREADABLE
The stored anchor is malformed or its MAC does not verify — a tampered value, or a master key changed without a re-seal. Treated as critical regardless of auditAnchorRequired, and never resolved by clearing the anchor before you know which of the two it was.

Three further states appear in narrower situations: not checked (a range-bounded verification, which does not evaluate the anchor at all), disabled (auditHmacEpoch = 0, where the anchor does not run), and inconclusive (a re-seal committed while verification was reading — re-run it).

After a wipe or purge you performed deliberately, clear the anchor so it can arm again. The reset is itself written into the chain:

Re-arm the anchor after a deliberate wipe
vendor/bin/typo3 vault:audit --reset-anchor
Copied!
Audit log showing secret access history with timestamps, actors, and IP addresses

The audit log tracks all secret operations with tamper-evident hash chains

vault:rotate-master-key 

Rotate the master encryption key (re-encrypts all DEKs):

Rotate master key
# Using old key from file, new key from current config
vendor/bin/typo3 vault:rotate-master-key \
  --old-key=/path/to/old.key \
  --confirm

# Dry run to simulate
vendor/bin/typo3 vault:rotate-master-key \
  --old-key=/path/to/old.key \
  --dry-run
Copied!

vault:scan 

Scan for potential plaintext secrets in database:

Scan for plaintext secrets
vendor/bin/typo3 vault:scan

# Only critical issues
vendor/bin/typo3 vault:scan --severity=critical

# JSON for CI/CD
vendor/bin/typo3 vault:scan --format=json
Copied!

vault:migrate-field 

Migrate existing plaintext field values to vault:

Migrate field to vault
# Preview
vendor/bin/typo3 vault:migrate-field tx_myext_settings api_key --dry-run

# Execute
vendor/bin/typo3 vault:migrate-field tx_myext_settings api_key
Copied!

vault:cleanup-orphans 

Remove orphaned secrets from deleted records:

Clean up orphaned secrets
vendor/bin/typo3 vault:cleanup-orphans --dry-run
vendor/bin/typo3 vault:cleanup-orphans --retention-days=30
Copied!

PHP API 

VaultService 

Inject the VaultService to access secrets programmatically:

Inject and use VaultService
use Netresearch\NrVault\Service\VaultServiceInterface;

final class PaymentService
{
    public function __construct(
        private readonly VaultServiceInterface $vaultService,
    ) {}

    public function getApiKey(): ?string
    {
        return $this->vaultService->retrieve('stripe_api_key');
    }
}
Copied!

Storing secrets 

Store secret with options
$this->vaultService->store(
    identifier: 'payment_api_key',
    secret: 'sk_live_...',
    options: [
        'description' => 'Stripe production API key',
        'context' => 'payment',
        'groups' => [1, 2], // Backend user group UIDs
        'expiresAt' => time() + (86400 * 90), // 90 days
    ],
);
Copied!

Checking existence 

Check if secret exists
if ($this->vaultService->exists('stripe_api_key')) {
    $value = $this->vaultService->retrieve('stripe_api_key');
}
Copied!

Listing secrets 

List secrets programmatically
// Get all accessible secrets
$secrets = $this->vaultService->list();

// Filter by pattern
$paymentSecrets = $this->vaultService->list(pattern: 'payment_*');
Copied!

Vault HTTP client 

Make authenticated API calls without exposing secrets to your code. The HTTP client is PSR-18 compatible. Configure authentication with withAuthentication() , then use standard sendRequest() .

Inject VaultHttpClientInterface directly:

HTTP client with vault authentication
use GuzzleHttp\Psr7\Request;
use Netresearch\NrVault\Http\SecretPlacement;
use Netresearch\NrVault\Http\VaultHttpClientInterface;

final class ExternalApiService
{
    public function __construct(
        private readonly VaultHttpClientInterface $httpClient,
    ) {}

    public function fetchData(): array
    {
        // Configure authentication, then use PSR-18
        $client = $this->httpClient->withAuthentication(
            'api_token',
            SecretPlacement::Bearer,
        );

        $request = new Request('GET', 'https://api.example.com/data');
        $response = $client->sendRequest($request);

        return json_decode($response->getBody()->getContents(), true);
    }
}
Copied!

Or access via VaultService:

HTTP client via VaultService
use GuzzleHttp\Psr7\Request;
use Netresearch\NrVault\Http\SecretPlacement;

$client = $this->vaultService->http()
    ->withAuthentication('stripe_api_key', SecretPlacement::Bearer);

$request = new Request(
    'POST',
    'https://api.stripe.com/v1/charges',
    ['Content-Type' => 'application/json'],
    json_encode($payload),
);

$response = $client->sendRequest($request);
Copied!

Authentication options 

Authentication placement examples
use GuzzleHttp\Psr7\Request;
use Netresearch\NrVault\Http\SecretPlacement;

// Bearer token
$client = $vault->http()
    ->withAuthentication('api_token', SecretPlacement::Bearer);
$response = $client->sendRequest(new Request('GET', $url));

// API key header (X-API-Key)
$client = $vault->http()
    ->withAuthentication('api_key', SecretPlacement::ApiKey);
$response = $client->sendRequest(new Request('GET', $url));

// Custom header
$client = $vault->http()
    ->withAuthentication('api_key', SecretPlacement::Header, [
        'headerName' => 'X-Custom-Auth',
    ]);
$response = $client->sendRequest(new Request('GET', $url));

// Basic authentication with separate secrets
$client = $vault->http()
    ->withAuthentication('service_password', SecretPlacement::BasicAuth, [
        'usernameSecret' => 'service_user',
    ]);
$response = $client->sendRequest(new Request('GET', $url));

// Query parameter
$client = $vault->http()
    ->withAuthentication('api_key', SecretPlacement::QueryParam, [
        'queryParam' => 'key',
    ]);
$response = $client->sendRequest(new Request('GET', $url));
Copied!

For a complete real-world example combining TCA vault fields with the HTTP client, see Example: API endpoint management.