itk-dev/vault
A Laravel package that provides a secure vault for storing and retrieving sensitive values such as secrets, tokens, and credentials, with a simple API for encrypting configuration data and managing secrets in your applications.
itk-dev/vault is a thin PHP client for HashiCorp Vault, published by ITK Development (Aarhus Kommune). It is a small, low-adoption package — one star, no dependents, a single tagged release — so treat it as a readable wrapper rather than a framework-grade abstraction. The upside: you can audit the entire source in ten minutes, which is exactly what you should do before wiring it into a Laravel app.
Prerequisites
docker run --cap-add=IPC_LOCK -e VAULT_DEV_ROOT_TOKEN_ID=root -p 8200:8200 hashicorp/vault).Install and inspect
composer require itk-dev/vault
ls vendor/itk-dev/vault/src && cat vendor/itk-dev/vault/README.md
Look first at: (1) the README for the constructor signature and supported auth, (2) src/ for the exact method names (read/write/list/delete are the likely surface), and (3) composer.json for the PHP constraint and whether it depends on hashicorp/vault-php-sdk or raw Guzzle. The class and method names in the examples below are illustrative — confirm them against src/ before copying.
Configuration
// config/vault.php
return [
'address' => env('VAULT_ADDR', 'https://vault.internal:8200'),
'token' => env('VAULT_TOKEN'), // dev / short-lived only
'role_id' => env('VAULT_ROLE_ID'), // AppRole
'secret_id' => env('VAULT_SECRET_ID'),
'namespace' => env('VAULT_NAMESPACE'), // Vault Enterprise only
'verify' => env('VAULT_VERIFY', true), // false ONLY for self-signed dev
];
First use case — read one KV secret and prove the round trip from php artisan tinker:
$vault = app(\ItkDev\Vault\Vault::class); // or `new Vault(...)` per the README
$secret = $vault->read('secret/data/myapp'); // note the `data/` segment for KV v2
dump($secret['data']['db_password'] ?? $secret);
Once that returns, you have the whole package working. Everything after this is plumbing: binding it as a singleton, caching the result, and keeping secrets out of bootstrap/cache.
1. Bind it as a singleton in a service provider. Vault is a network dependency; you do not want a new client per injection.
// app/Providers/VaultServiceProvider.php
public function register(): void
{
$this->app->singleton(\ItkDev\Vault\Vault::class, fn () => new \ItkDev\Vault\Vault(
address: config('vault.address'),
token: config('vault.token'),
));
}
2. Wrap it, don't sprinkle it. Because the package is one release old with no dependents, define your own narrow interface (SecretsRepository with get(string $key): ?string) and implement it with the package. If the package is abandoned or you outgrow it, you swap one adapter class instead of grepping 40 call sites. This is the single highest-value pattern in this assessment.
3. Cache with a TTL, never per-request. Vault calls are HTTP round trips with token validation. Read-once-per-boot is fine; read-per-request is not.
public function get(string $path): array
{
return Cache::remember("vault:{$path}", now()->addMinutes(5), function () use ($path) {
return $this->vault->read($path);
});
}
Set the TTL below the token/lease TTL so you never serve data backed by an expired credential.
4. Inject secrets at config time for credentials Laravel resolves once. The classic case is a rotating database password:
// config/database.php
'mysql' => [
// ...
'password' => env('DB_PASSWORD') ?: app(SecretsRepository::class)->get('secret/data/db')['password'],
],
Guard this behind a config-cache check (see Gotchas) — this pattern and php artisan config:cache are natural enemies.
5. Use AppRole in CI and Kubernetes auth in production, tokens only locally. Never commit a VAULT_TOKEN. If the package exposes only token auth, authenticate out-of-band (a small curl in your deploy script or an init container) and hand the client a wrapped/short-lived token via env.
6. Dynamic credentials for the real win. If you need a strong justification for the dependency, it is Vault's database secrets engine: short-lived, auto-expiring DB users instead of a static password in .env. This requires a lease-refresh loop the package almost certainly does not provide — you own the scheduler job.
7. Test against a fake at the interface boundary. Bind a stub SecretsRepository in TestCase::setUp(); do not require a live Vault in the unit suite. Keep one integration test against the Docker dev server, tagged and excluded from default runs.
secret/data/myapp; the CLI and UI show secret/myapp. Getting this wrong yields a confusing 404 rather than an auth error. KV v1 has no data/ segment at all — check which engine version your mount uses.php artisan config:cache freezes secrets to disk. bootstrap/cache/config.php is world-readable on many deploys and is not rotated when Vault rotates. If you inject secrets into config, either skip config caching or inject at runtime via a provider/binding instead. This is the most common production footgun with any Vault wrapper in Laravel.403 = policy/permission or an expired token; 404 = wrong path (see above); 503/connection refused = Vault sealed or unreachable. A sealed Vault at boot will take your whole app down — decide explicitly whether missing secrets should fail fast or fall back.Log::debug($response) or Guzzle middleware that dumps bodies is a leak. If you add HTTP middleware for retries, scrub the body.verify => false; make sure that flag is env-driven and defaults to true so it cannot follow you to production.X-Vault-Namespace header; custom secret-engine mount points (kv/, apps/) must be reflected in every path string. Centralize path building in your adapter.^1.0 constraint plus a committed composer.lock is the safe posture; expect no security patch cadence.dcarbone/php-vault-client and hashicorp/vault-php-sdk cover the same ground with more surface area, or a 40-line Guzzle wrapper covers KV v2 read/write entirely. Keeping the package behind your own interface (pattern 2) makes that a one-file change.How can I help you explore Laravel packages today?