fourxxi/vault-bundle
fourxxi/vault-bundle integrates HashiCorp Vault with PHP applications, offering configuration and service wiring for secure secret retrieval and credential management. Keep sensitive values out of your codebase and access Vault-backed secrets through a simple, framework-friendly API.
Reality check first. This bundle last shipped in January 2018. It targets Symfony 3.3–4.x and a pre-4.0 PHP style. Before you invest time, confirm it installs against your Symfony version:
composer require fourxxi/vault-bundle
If Composer refuses, you'll need to fork it, or accept that you're maintaining a private fork. For a day-to-day Vault integration on Symfony 5/6, the Vault HTTP API is small enough that wiring it yourself with symfony/http-client is often less work than resurrecting this bundle. Treat the bundle as a convenience wrapper over GET /v1/<path>, not as an abstraction you can't live without.
If you do install it:
Register the bundle. Symfony 3.x: add new Fourxxi\Bundle\VaultBundle\FourxxiVaultBundle() to AppKernel. Symfony 4: add Fourxxi\Bundle\VaultBundle\FourxxiVaultBundle::class => ['all' => true] to config/bundles.php.
Read the config source before writing config. The bundle's extension keys, defaults, and whether it expects a url/base_uri/address are defined in:
vendor/fourxxi/vault-bundle/DependencyInjection/Configuration.php
vendor/fourxxi/vault-bundle/DependencyInjection/FourxxiVaultExtension.php
That second file tells you the literal service IDs and any aliases the bundle registers. Do not guess them.
A plausible minimal config (verify against Configuration.php — key names have shifted across Vault bundles):
# config/packages/fourxxi_vault.yaml
fourxxi_vault:
url: '%env(VAULT_ADDR)%' # e.g. https://vault.internal:8200
token: '%env(VAULT_TOKEN)%' # a token with read on your paths
VAULT_ADDR=https://vault.internal:8200
VAULT_TOKEN=s.xxxxxxxxxxxxxxxx
First use case — read a secret at runtime. The whole point is to stop putting credentials in parameters.yml/.env. Inject the client (find the exact service via the extension file or bin/console debug:container | grep -i vault) and read:
use Fourxxi\Bundle\VaultBundle\Client\VaultClientInterface;
class DatabaseCredentials
{
public function __construct(private VaultClientInterface $vault) {}
public function get(): array
{
$response = $this->vault->read('secret/app/database');
// structure depends on KV version; usually ['data' => [...]] on v2
return $response['data'] ?? $response;
}
}
That is the 80% case: a service asks Vault for a credential, then hands it to a PDO/Redis/S3 client. Start there, not with container-time interpolation.
Pattern 1 — Runtime read inside a service (preferred). Keep Vault reads lazy and close to the consumer. Wrap them so the rest of the app never sees the Vault response envelope:
final class VaultSecretReader
{
public function __construct(private VaultClientInterface $client) {}
public function fetch(string $path): array
{
try {
return $this->client->read($path)['data'] ?? [];
} catch (\Throwable $e) {
throw new SecretNotFoundException("Vault read failed for {$path}", 0, $e);
}
}
}
Benefits: testable (mock the interface), survives Vault outages if you cache, and doesn't couple your DI graph to Vault's availability.
Pattern 2 — Deploy-time parameter resolution.
If the bundle exposes an env-var processor (vault: prefix — check for a class implementing EnvVarProcessorInterface in the bundle), you can reference secrets from YAML/.env:
parameters:
app.db_password: '%env(vault:secret/app/database:password)%'
This is convenient but drags a network dependency into cache:warmup. See Gotchas.
Pattern 3 — Cache the read, don't hammer Vault.
Vault is not a low-latency KV store for every request. Combine the reader with a short-lived cache (PSR-6/PSR-16) keyed by path, TTL matched to lease_duration if you're reading dynamic secrets:
return $this->cache->get($key, function () use ($path) {
return $this->reader->fetch($path);
});
For dynamic (leased) secrets like DB credentials from a Vault database secret engine, the TTL must be shorter than the lease or you'll serve dead credentials.
Pattern 4 — AppRole / Kubernetes auth if you have it.
Static VAULT_TOKEN in env is a startup-only convenience. In production, tokens are short-lived. If the bundle only supports a static token (likely, given its age), plan to either (a) mint a token via AppRole/Kubernetes auth in a bootstrapping step and export it, or (b) call Vault's auth endpoints via HttpClient and hand the resulting token to the bundle. Do not run long-lived workers on a token that expires.
Pattern 5 — Per-environment Vault, per-environment paths.
Point dev at a Vault dev server (vault server -dev), staging at a namespaced mount, prod at the real mount. Keep paths in configuration, not hardcoded in PHP:
parameters:
vault.paths.database: 'secret/%kernel.environment%/database'
Integration tips
App\Vault\SecretReaderInterface) in front of the bundle's client so you can swap the bundle for an HttpClient implementation later without touching consumers.debug:container and debug:config fourxxi_vault to confirm service IDs and resolved config early.VaultClientInterface returning canned responses; run one integration test against a real Vault dev container (docker hashicorp/vault) in CI.The package is effectively unmaintained. Zero dependents, last release 2018, 12 stars. Assume no bug fixes. Budget for a fork. If your team's security posture requires a maintained dependency, implement against the Vault HTTP API directly — the endpoints you need (/v1/<mount>/data/<path>, /v1/auth/approle/login) are a few dozen lines with symfony/http-client, and you control the code.
KV v1 vs KV v2 paths. The single most common failure. With KV v2 the read path is secret/data/app/db and the payload is {"data": {"data": {...}, "metadata": {...}}}. With KV v1 it's secret/app/db and {"data": {...}}. If your reads return null, check vault secrets list for the mount version. Normalize in your wrapper, not in every consumer.
Never bake the token into config. token: 's.abc...' in YAML ends up in git. Use %env(VAULT_TOKEN)% and ensure your .env isn't committed. Better: AppRole/Kubernetes auth in a bootstrap step.
Token TTL and lease expiry. A static token pasted into .env may be revoked or expired at deploy time. Long-running queue workers holding a token for hours or days are a hazard. If the bundle doesn't refresh, build token renewal outside it (a cron/command that re-auths) or keep token lifetimes aligned with process lifetimes.
Container compilation against a live Vault is fragile. If you wire secrets into parameters or an env processor, cache:clear/cache:warmup/container build now requires Vault reachability. That breaks on a fresh CI runner without network access, and it breaks your deploy if Vault is briefly down. Prefer runtime reads, or gate the compile-time fetch behind a check and fall back to a placeholder that fails loudly at first real use.
TLS and CA trust. Vault behind a private CA needs verify_peer/cafile config on whatever HTTP client the bundle uses. The bundle may not expose these options — another reason to check the extension file, and another reason to consider owning the HTTP layer. Vault dev mode is plain HTTP; production should never be.
Namespaces (Vault Enterprise) are likely unsupported. If you rely on X-Vault-Namespace, you probably need to add the header yourself via a client wrapper or fork.
Error semantics matter. A 404 means "path doesn't exist" (typo, wrong mount); a 403 means "token lacks permission" (policy problem); a 503 means "Vault is sealed." Catch and distinguish these — a blanket catch (\Exception) will send you chasing the wrong bug during an incident.
Debugging checklist when a read fails:
curl -H "X-Vault-Token: $TOKEN" $VAULT_ADDR/v1/<path> — does it work outside Symfony? If not, it's a Vault/policy/path issue, not the bundle.bin/console debug:container <service-id> — is the service using the URL you think?bin/console debug:config fourxxi_vault — did your config key get picked up, or silently ignored?vault status.Caching is your extension point. The bundle is thin; the most valuable thing you can add is a cache layer and a retry/timeout policy around it. If you need auth-method pluggability, HTTP option tuning, or KV versioning, fork and add it — upstream won't.
When to skip this bundle entirely. If you're on Symfony 5/6, or you need AppRole/Kubernetes auth, KV v2 ergonomics, namespace support, or a maintained dependency, don't fight the 2018 release. Write a 100-line VaultClient with symfony/http-client, register it as a service, and keep the same read()-style interface. You'll get modern PHP, full control, and no upgrade dead-end.
How can I help you explore Laravel packages today?