Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Azure Key Vault Client Laravel Package

keboola/azure-key-vault-client

A lightweight PHP client for Azure Key Vault that lets applications fetch secrets securely at runtime. Simple configuration, a clean API, and easy integration for managing credentials and sensitive configuration in Laravel or any Composer-based project.

View on GitHub
Deep Wiki
Context7

Getting Started

What it is. A small, single-purpose PHP client for reading secrets from Azure Key Vault over the REST API. It is maintained by Keboola (it powers their own connection stack), it is not a Laravel package in the usual sense — no service provider, no facade, no config file. You own the integration; the package owns the auth handshake and the HTTP plumbing.

Reality check before you commit. 1 star, 0 dependents. This is essentially an internal Keboola library published for compliance, so:

  • Pin an exact version in composer.json and treat major upgrades as a migration, not a composer update.
  • Read README.md → src/Client.php → src/Authentication/ → tests/ in that order. The tests are the real documentation for constructor signatures and response shapes, which have churned across majors.
  • The wrapper classes (RequestFactory, ResponseParser) are internal. Depend only on Client, the exception classes, and the credentials objects.

The one-hour setup.

composer require keboola/azure-key-vault-client guzzlehttp/guzzle

On the Azure side you need, at minimum:

  • A vault URL: https://<vault-name>.vault.azure.net
  • Either an app registration (tenant ID + client ID + client secret) or a managed identity on the host
  • The Key Vault Secrets User RBAC role (or a legacy access policy granting Get on secrets) assigned to that principal

Add the bootstrap credentials to config/services.php:

'azure_key_vault' => [
    'vault_url'               => env('AZURE_KEY_VAULT_URL'),
    'tenant_id'               => env('AZURE_KEY_VAULT_TENANT_ID'),
    'client_id'               => env('AZURE_KEY_VAULT_CLIENT_ID'),
    'client_secret'           => env('AZURE_KEY_VAULT_CLIENT_SECRET'),
    'use_managed_identity'    => env('AZURE_KEY_VAULT_USE_MANAGED_IDENTITY', false),
    'timeout'                 => env('AZURE_KEY_VAULT_TIMEOUT', 5),
],

Then bind the client as a singleton. The pattern is: build a Guzzle HandlerStack, push the package's authentication middleware onto it, and hand that stack to both the authenticator and the client — they must share the stack or requests go out unauthenticated.

// app/Providers/AzureKeyVaultServiceProvider.php
use GuzzleHttp\{Client as GuzzleClient, HandlerStack};
use Keboola\AzureKeyVaultClient\Client as KeyVaultClient;
use Keboola\AzureKeyVaultClient\Authenticator;
use Keboola\AzureKeyVaultClient\GuzzleMiddleware\AuthenticatorMiddleware;

public function register(): void
{
    $this->app->singleton(KeyVaultClient::class, function (array $cfg = null) {
        $cfg ??= config('services.azure_key_vault');

        $authenticator = new Authenticator(/* credentials object, see README */, new GuzzleClient());
        $stack = HandlerStack::create();
        $stack->push(new AuthenticatorMiddleware($authenticator));

        return new KeyVaultClient(
            new GuzzleClient(['handler' => $stack, 'timeout' => $cfg['timeout']]),
            $cfg['vault_url'],
        );
    });
}

First use case — stop putting the DB password in .env. In AppServiceProvider::register():

$secret = $this->app->make(KeyVaultClient::class)->getSecret('db-password')->getValue();
config(['database.connections.mysql.password' => $secret]);

That single line is 80% of what most Laravel teams use this package for. Everything below is about doing it without shooting yourself in the foot.

Implementation Patterns

Wrap it — never inject the raw client into application code. A SecretStore that owns caching, retries and error mapping keeps Azure's semantics out of your domain:

final class SecretStore
{
    public function __construct(
        private KeyVaultClient $client,
        private Repository $cache,
    ) {}

    public function get(string $name, ?string $version = null): string
    {
        $key = "azure-kv:{$name}" . ($version ? ":{$version}" : '');

        return $this->cache->remember($key, now()->addMinutes(30), function () use ($name, $version) {
            try {
                return $this->client->getSecret($name, $version)->getValue();
            } catch (ClientException $e) {
                throw new SecretUnavailable("Key Vault secret [{$name}] is not readable.", 0, $e);
            }
        });
    }
}

The cache is not an optimisation, it is the design. Key Vault is a network round trip in a different cloud region from your DB; a vault call on every request is a latency tax you will notice in the p95.

Config injection, done at the right moment. Prefer a deploy-time artisan command that warms the cache, then a cheap Cache::remember read on boot:

// app/Console/Commands/WarmSecrets.php
foreach (['db-password', 'stripe-secret', 'mail-password'] as $name) {
    $this->line("{$name}: " . (strlen($store->get($name)) > 0 ? 'ok' : 'EMPTY'));
}

Managed identity — same code, one branch. On App Service, Container Apps, AKS with workload identity, or an Azure VM, you have no client secret to bootstrap. Detect it and swap the credentials object (ManagedCredentials / ManagedIdentityCredentials depending on major; check src/Authentication/). Everything downstream is unchanged. This is the configuration worth aiming for — with a client secret still living in .env, you have moved the secret, not removed it.

Name mapping. Key Vault secret names allow only [a-zA-Z0-9-]. Adopt a translation convention and enforce it in code, e.g. db-password → database.connections.mysql.password, stripe-secret → services.stripe.secret.

Version pinning. getSecret($name, $version) accepts an explicit version. Omit it to always follow the latest (good for rotation, bad for reproducibility across a rolling deploy), pass it to pin. A sane middle ground: read latest, log the version ID at deploy time so you can correlate an incident with a rotation.

Testing. Never let the vault be a hard dependency of the test suite.

$this->app->instance(SecretStore::class, new InMemorySecretStore([
    'db-password' => 'test',
    'stripe-secret' => 'sk_test',
]));

If you need to test the client wiring itself, inject a Guzzle MockHandler. The token request is issued before the secret request, so your queued responses must be in that order — auth token first, then the secret payload. Getting that backwards produces a confusing 401 that looks like a credential bug.

Gotchas and Tips

Don't call the network inside register() unguarded. php artisan config:cache, migrate, queue:work on a cold box, and your CI pipeline all boot the app. A 5-second timeout on an unreachable vault turns a 2-second command into a hang. Guard on config('services.azure_key_vault.vault_url') and on the cache being warm, and fail fast with a clear message rather than falling through to a null password.

Octane and long-running workers. The singleton and any in-memory token live for the worker's lifetime. A rotated secret will not be picked up until the worker restarts. Either keep the cache TTL well under your rotation cadence, or listen for the relevant Octane/queue events and flush.

Throttling is real and it is per-vault. Key Vault enforces request-rate limits per vault, per region, and the client's retry middleware will happily turn a burst of 429s into a slow boot. Combined with the cache this is a non-issue; combined with a per-request fetch it is an outage waiting for a traffic spike.

Exception types carry information — use both. ClientException is a 4xx (403 = wrong RBAC role or a firewall/private-endpoint block, 404 = secret name typo or a soft-deleted secret), ServerException is a 5xx after retries. Catching \Exception and defaulting to '' is how you end up with an app that boots fine and can't connect to MySQL.

Secret names are case-insensitive and hyphen-only. DB_PASSWORD will not be found. DBPASSWORD will be. This bites everyone once.

Never log the value. Neither the Secret object nor its getValue() should touch Log::debug(), dump(), or a Sentry breadcrumb. If you must log, log getId() (which includes name and version) only.

Set a short timeout and connect_timeout on the Guzzle client. The package's default is not tuned for a web request path. 2s connect / 5s total is a reasonable starting point.

The package reads only. There is no write, list, or delete API here — rotation stays in az keyvault secret set, Terraform, or CI. Design your runbook around that rather than looking for a method that doesn't exist.

Extension points are Guzzle-shaped. Need request logging, a private-endpoint proxy, or a custom retry policy? Push another middleware onto the same HandlerStack. Need to test without the network? MockHandler. That is the whole extension surface — there is no event dispatcher and no Laravel hook.

If you're not running on Azure, seriously question the trade. You still need a client secret in .env to authenticate, you've added a network hop and a new failure mode, and you've gained only audit logging and rotation convenience. The calculus flips hard in favour of managed identity on Azure; off-Azure, a sealed secrets / SOPS / Vault setup is usually the better fit.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin