Mattschwarze Kombinationsschloss-Scheibe mit gebürstetem Aluminium-Ring auf Eichentisch, drei kleine Indikator-Markierungen, kühles gerichtetes Tageslicht.
Extension · moselwal/secret-resolver

secret-resolver — secrets in site configs, at runtime.

Write %secret(API_KEY)% directly in TYPO3 site configs — resolved at runtime: first KEY_FILE env (file path), then /run/secrets/ mount. Extensible via SecretProviderInterface for Vault, AWS Secrets Manager, and other backends. Exactly what container deployments need.

  • Composer package:composer require moselwal/secret-resolver
  • TYPO3: ^14.0 · PHP: ^8.3 · MIT
Das Problem

Secrets in site configs are plain text — or not loaded at all.

With secret-resolver

  • %secret(API_KEY)% syntax directly in site config YAMLs
  • Cascading lookup: API_KEY_FILE/run/secrets/api_keyAPI_KEY env
  • Compatible with Docker and Kubernetes secrets out of the box
  • Custom providers via SecretProviderInterface (e.g. Vault, AWS SM)
  • Cache-aware — no re-lookups on every request

Until now

  • API keys hard-coded in YAML configs (in the Git repo)
  • Or: referenced via ENV variables in TypoScript (clunky)
  • Docker/K8s secrets via /run/secrets/ not usable out of the box
  • Multi-provider lookup (Vault, AWS Secrets Manager) as custom boilerplate

Four building blocks

Extensibility

Implement SecretProviderInterface — and Vault, AWS Secrets Manager, or Bitwarden Secrets are usable as an additional lookup step.

Container-ready

Works seamlessly with Docker secrets and Kubernetes mounted secrets — no image-build adjustment required.

Cascading lookup

Priority cascade for simple keys: {KEY}_FILE env (priority 30), then /run/secrets/{key} mount (priority 20). Extended keys route directly to the named provider — no cascade pass.

%secret() syntax

Usable directly in YAML site configs — no custom bootstrap logic required.

Usage

Simple keys (cascade resolution)

 

# config/sites/main/config.yaml
apiKey: '%secret(API_KEY)%'
dbPassword: '%secret(DB_PASSWORD)%'

# Inline in strings:
dsn: 'mysql://user:%secret(DB_PASSWORD)%@db:3306/app'

 

The key is resolved through all registered providers in priority order — first match wins.

Extended keys (provider-targeted)

 

# Direct vault lookup — bypasses cascade, goes to the "vault" provider
dbPassword: '%secret(vault:kv-v2/database.password)%'
apiToken: '%secret(vault:transit/api_token)%'

# AWS Secrets Manager
dbPassword: '%secret(aws-sm:prod/database.password)%'

 

Format: %secret(provider:path/to/secret.subKey)%

PartRequiredDescription
providerYesProvider name (vault, aws-sm …) — routes directly
pathNoSecret path with / separators
subKeyNoJSON sub-key after the last . in the final path segment

Sub-key extraction: if a provider returns e.g. {"password":"s3cret","username":"admin"}, the sub-key password automatically extracts "s3cret". Simple keys (without :) keep working as before — fully backwards-compatible.

Works in all TYPO3 YAML files

%secret()% hooks into TYPO3’s central YamlFileLoader — not just site configurations. The placeholder works everywhere TYPO3 loads YAML:

Packagist

composer require moselwal/secret-resolver

Package on Packagist →
Packagist

GitHub

Source code, issues, and changelogs. MIT-licensed.

View on GitHub →
GitHub

Resolution-Cascade & Priorities

Bei einfachen Schlüsseln wie %secret(DB_PASSWORD)% wird der Schlüssel durch alle registrierten Provider in absteigender Priority-Reihenfolge gereicht. Der erste Treffer gewinnt; leere Werte und Dateien, die nur Whitespace enthalten, werden übersprungen.

Built-in Cascade

PriorityProviderQuelleBeispiel
30FileEnvSecretProviderDB_PASSWORD_FILE-Env → Datei lesenDB_PASSWORD_FILE=/vault/secrets/db-pass
20RunSecretsSecretProvider/run/secrets/db_passwordDocker- oder K8s-Secret-Mount

Erweiterte Schlüssel im Format %secret(provider:path/to/secret.subKey)% umgehen die Cascade und werden direkt an den genannten Provider geroutet. Rückgabewerte in JSON-Form lassen sich über den Sub-Key extrahieren: aus {"password":"s3cret","username":"admin"} liefert der Sub-Key password automatisch s3cret.

Priority-Guidelines

PriorityAnwendungsfall
50+Override für alles (z. B. lokaler Dev-Mock-Provider)
40Primäres Backend (Vault, AWS Secrets Manager, Azure Key Vault)
30Datei-basierte Env-Variablen (built-in)
20Docker- oder K8s-Secret-Mounts (built-in)
10Fallback / Last Resort

Aufgelöste Werte werden in cache.core abgelegt — identisch zu %env()%. Nach einer Secret-Rotation räumt vendor/bin/typo3 cache:flush den Cache.

Eigene SecretProvider entwickeln

Die Erweiterung ist auf Erweiterbarkeit ausgelegt. Jede TYPO3-Extension kann einen eigenen Provider beisteuern, ohne das Core-Paket zu ändern. Drei Schritte genügen.

Step 1: SecretProviderInterface implementieren

Der Provider liefert einen eindeutigen Namen für erweiterte Schlüssel, entscheidet über supports(), ob er die Auflösung versucht, und gibt in resolve() den Wert oder null zurück. Eine statische priority() bestimmt die Position in der Cascade.

 

<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Infrastructure\Provider;

use Moselwal\SecretResolver\Domain\Contract\SecretProviderInterface;
use Moselwal\SecretResolver\Domain\ValueObject\SecretKey;

final readonly class VaultSecretProvider implements SecretProviderInterface
{
    public function __construct(
        private VaultClient $client,
    ) {}

    public function getName(): string
    {
        // Return a unique provider name for extended key format targeting.
        // Users can then write %secret(vault:kv-v2/db.password)%
        // Return '' to participate only in the cascade (simple keys).
        return 'vault';
    }

    public function supports(SecretKey $key): bool
    {
        if ($key->isExtended()) {
            $path = $key->getSecretPath() ?? $key->getKeyName();
            return $this->client->secretExists($path);
        }

        return $this->client->secretExists($key->lowerCase);
    }

    public function resolve(SecretKey $key): ?string
    {
        $path = $key->isExtended()
            ? ($key->getSecretPath() ?? $key->getKeyName())
            : $key->lowerCase;

        try {
            $value = $this->client->readSecret($path);
        } catch (\Throwable) {
            return null; // Fallback to next provider in cascade
        }

        return $value !== '' ? $value : null;
    }

    public static function priority(): int
    {
        // Higher priority = checked first.
        // Built-in: FileEnv=30, RunSecrets=20
        return 40;
    }
}

 

Step 2: Über Services.yaml registrieren

Manuelle Registrierung entfällt — TYPO3s DI-Container erkennt alle SecretProviderInterface-Implementierungen über das von dieser Extension konfigurierte _instanceof-Auto-Tagging. Voraussetzung ist, dass Ihre Extension das übliche Autowiring nutzt.

 

services:
  _defaults:
    autowire: true
    autoconfigure: true
    public: false

  MyVendor\MyExtension\:
    resource: '../Classes/*'

 

Abhängigkeiten wie VaultClient werden automatisch injiziert, wenn sie im DI-Container Ihrer Extension verfügbar sind.

Step 3: Provider verwenden

Einfache Schlüssel laufen durch die Cascade; ein Provider mit Priority 40 wird vor den built-in-Providern befragt. Erweiterte Schlüssel routen direkt zum benannten Provider und unterstützen optionale Sub-Key-Extraktion aus JSON-Antworten.

 

# Simple key - goes through cascade (Vault at priority 40 is checked first)
apiKey: '%secret(API_KEY)%'

# Extended key - routes directly to Vault, extracts "password" from JSON response
dbPassword: '%secret(vault:kv-v2/database.password)%'

# Extended key - full secret path, no sub-key extraction
certificate: '%secret(vault:pki/issue/my-cert)%'

 

SecretKey-Properties

Die folgenden Properties stehen Providern auf dem übergebenen SecretKey-Value-Object zur Verfügung:

PropertyTypBeschreibung
$key->rawstringOriginal-Eingabe (DB_PASSWORD oder vault:kv-v2/db.password)
$key->upperCasestringSchlüsselname in Großbuchstaben (ohne Provider-Präfix)
$key->lowerCasestringSchlüsselname in Kleinbuchstaben (ohne Provider-Präfix)
$key->provider?stringProvider-Name (vault) oder null bei einfachen Schlüsseln
$key->path?stringPfad-Segment (kv-v2/db) oder null
$key->subKey?stringSub-Key für JSON-Extraktion (password) oder null
$key->isExtended()booltrue, wenn Provider-Präfix vorhanden ist
$key->getKeyName()stringSchlüssel ohne Provider-Präfix (kv-v2/db.password)
$key->getSecretPath()?stringPfad ohne Sub-Key (kv-v2/db) oder null
Nächster Schritt

Tidy up your container deployment?

secret-resolver is open source and compact. For Vault or AWS Secrets Manager integration, cluster setups or migration away from plain-text secrets, we are happy to support you.

Secret-Setup besprechen

Oder direkt schreiben: kontakt@moselwal.de

Where we use this …

This package carries the secret handling in TYPO3 Kubernetes and is a mandatory building block for any setup that takes Open Source & Digital Sovereignty seriously — keys stay with you, not in the image. Managed variant: AI-Ready CMS as a Service.