Usage
Backend module
Access the vault through the TYPO3 backend:
- Go to Admin Tools > Vault.
- The overview shows statistics and quick-start examples.
- Navigate to Secrets to manage your secrets.
Creating secrets
- Click Create Secret (+ button).
-
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.
- Click Save.
Viewing and editing secrets
Secrets are displayed with their metadata but not their values. Click Reveal to temporarily show a secret value.
Note
Revealing a secret creates an audit log entry.
Site configuration
Reference secrets in your site configuration files using the
%vault syntax:
settings:
payment:
stripePublicKey: 'pk_live_...'
stripeSecretKey: '%vault(stripe_secret_key)%'
email:
mailchimpApiKey: '%vault(mailchimp_api_key)%'
sendgridToken: '%vault(sendgrid_token)%'
Secrets are resolved at runtime when the site configuration is loaded. This keeps sensitive values out of your version control while still allowing you to configure them through the familiar site settings.
Important
Site configuration secrets are resolved on every request. Ensure your vault storage is performant (the default local adapter caches lookups).
TypoScript integration
Use vault references in TypoScript for frontend-accessible secrets:
lib.googleMapsKey = TEXT
lib.googleMapsKey.value = %vault(google_maps_api_key)%
page.headerData.10 = TEXT
page.headerData.10.value = <script>var API_KEY = '%vault(public_api_key)%';</script>
Warning
Security considerations:
- Only secrets marked as
frontend_accessiblecan be resolved. - Resolved values may be cached - use
cache.disable = 1for secrets that should not be cached. - Consider using
USER_INTfor content containing secrets.
Example with caching disabled:
lib.apiKey = TEXT
lib.apiKey {
value = %vault(my_api_key)%
stdWrap.cache.disable = 1
}
CLI commands
vault:init
Initialize the vault and generate a master key:
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
vault:store
Create or update a secret:
# Interactive (prompts for value)
vendor/bin/typo3 vault:store stripe_api_key
# With all options
vendor/bin/typo3 vault:store payment_key \
--value="sk_live_..." \
--description="Stripe production key" \
--context="payment" \
--expires="+90 days" \
--groups="1,2"
vault:retrieve
Retrieve a secret value:
vendor/bin/typo3 vault:retrieve stripe_api_key
# Quiet mode for scripting
API_KEY=$(vendor/bin/typo3 vault:retrieve -q stripe_api_key)
vault:list
List all accessible 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
vault:rotate
Rotate a secret with a new value:
vendor/bin/typo3 vault:rotate stripe_api_key \
--reason="Scheduled quarterly rotation"
vault:delete
Delete a secret:
vendor/bin/typo3 vault:delete old_api_key \
--reason="Service deprecated" \
--force
vault:audit
View the audit log:
# View recent entries
vendor/bin/typo3 vault:audit --days=7
# Filter by secret
vendor/bin/typo3 vault:audit --identifier=stripe_api_key
# Export to JSON
vendor/bin/typo3 vault:audit --format=json > audit.json
vault:rotate-master-key
Rotate the master encryption key (re-encrypts all DEKs):
# 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
vault:scan
Scan for potential plaintext secrets in database:
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
vault:migrate-field
Migrate existing plaintext field values 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
vault:cleanup-orphans
Remove orphaned secrets from deleted records:
vendor/bin/typo3 vault:cleanup-orphans --dry-run
vendor/bin/typo3 vault:cleanup-orphans --retention-days=30
PHP API
VaultService
Inject the VaultService to access secrets programmatically:
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');
}
}
Storing secrets
$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
],
);
Checking existence
if ($this->vaultService->exists('stripe_api_key')) {
$value = $this->vaultService->retrieve('stripe_api_key');
}
Listing secrets
// Get all accessible secrets
$secrets = $this->vaultService->list();
// Filter by pattern
$paymentSecrets = $this->vaultService->list(pattern: 'payment_*');
Vault HTTP client
Make authenticated API calls without exposing secrets to your code.
The HTTP client is PSR-18 compatible. Configure authentication with
with, then use standard
send.
Inject
Vault directly:
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);
}
}
Or access 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);
Authentication options
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));
For a complete real-world example combining TCA vault fields with the HTTP client, see Example: API endpoint management.