PHPackages                             gtelphp/vault - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [API Development](/categories/api)
4. /
5. gtelphp/vault

ActiveLibrary[API Development](/categories/api)

gtelphp/vault
=============

Production-ready PHP SDK for HashiCorp Vault and OpenBao, with first-class Laravel support.

02↑2900%PHP

Since Jun 30Pushed yesterdayCompare

[ Source](https://github.com/tkien/gtelphp-vault)[ Packagist](https://packagist.org/packages/gtelphp/vault)[ RSS](/packages/gtelphp-vault/feed)WikiDiscussions main Synced today

READMEChangelogDependenciesVersions (1)Used By (0)

GtelPHP Vault
=============

[](#gtelphp-vault)

A production-ready, framework-agnostic PHP SDK for [HashiCorp Vault](https://www.vaultproject.io/) and [OpenBao](https://openbao.org/) — with first-class Laravel support.

[![PHP Version](https://camo.githubusercontent.com/b1086b29eb24e037cb442a578ecba2d378a6acef9c4c2748c15e92ef0799841c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7068702d253545382e322d373737424234)](https://www.php.net/)[![PHPStan Level](https://camo.githubusercontent.com/44dc5f71fec76653887c975fe3db546a82ff603d094798eb6414a38369db1f44/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7068707374616e2d6c6576656c253230382d627269676874677265656e)](phpstan.neon)[![License](https://camo.githubusercontent.com/b8cadaa967891081f8f165695470689986c028821dd8a040132f6e661795dc0d/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d626c7565)](LICENSE)

```
composer require gtelphp/vault
```

Why this package
----------------

[](#why-this-package)

- **Core has zero Laravel dependency.** Laravel is an adapter on top of a plain PHP SDK — use it in any PHP 8.2+ project.
- **Works with both Vault and OpenBao** transparently, since they share the same HTTP API.
- **AppRole auth with automatic token renewal**, backed by a pluggable token cache (in-memory or Redis).
- **KV v2, Transit, Database secrets and PKI engines** — the ones you actually use in production.
- **`Vault::bootstrap()`** loads secrets straight into `putenv()`/`$_ENV`/`$_SERVER`, before your framework's config is even loaded.
- **`JsonEncryptedCast`** for Eloquent — encrypt *individual keys* inside a `jsonb` column with Vault Transit, while keeping the column a real JSON object.
- PSR-4, PSR-12, PHPStan level 8, full PHPUnit test suite.

Table of contents
-----------------

[](#table-of-contents)

- [Installation](#installation)
- [Quick start (plain PHP)](#quick-start-plain-php)
- [Laravel installation](#laravel-installation)
- [Configuration reference](#configuration-reference)
- [Caching (don't skip this in production)](#caching-dont-skip-this-in-production)
- [Auto-loading env + database credentials at boot (Laravel)](#auto-loading-env--database-credentials-at-boot-laravel)
- [KV v2 secrets](#kv-v2-secrets)
- [Transit (encrypt / decrypt / sign / hmac)](#transit-encrypt--decrypt--sign--hmac)
- [Database secrets engine](#database-secrets-engine)
- [PKI secrets engine](#pki-secrets-engine)
- [Env bootstrap (`Vault::bootstrap()`)](#env-bootstrap-vaultbootstrap)
- [JsonEncryptedCast — selective jsonb encryption](#jsonencryptedcast--selective-jsonb-encryption)
- [Multiple connections](#multiple-connections)
- [Token caching (memory vs Redis)](#token-caching-memory-vs-redis)
- [Exceptions](#exceptions)
- [Testing](#testing)
- [Best practices](#best-practices)
- [License](#license)

Installation
------------

[](#installation)

```
composer require gtelphp/vault
```

Requires PHP `>= 8.2`. Works with Laravel `11.x` and `12.x` if you want the optional adapter — install `illuminate/support` yourself or just use the package inside a Laravel app, where it's already present.

Quick start (plain PHP)
-----------------------

[](#quick-start-plain-php)

```
use GtelPhp\Vault\Client;
use GtelPhp\Vault\Support\VaultConfig;

$config = new VaultConfig(
    address: 'https://vault.internal:8200',
    roleId: getenv('VAULT_ROLE_ID'),
    secretId: getenv('VAULT_SECRET_ID'),
);

$vault = Client::make($config);

// AppRole login + token caching + auto-renewal all happen transparently.
$database = $vault->kv()->get('database');

echo $database['username'];
```

You never have to call `login()` yourself — every secrets-engine call asks the internal `TokenManager` for a valid token, which logs in (or renews) as needed.

Laravel installation
--------------------

[](#laravel-installation)

The package is auto-discovered. Publish the config file:

```
php artisan vendor:publish --tag=vault-config
```

Add these to your `.env`:

```
VAULT_ADDR=https://vault.internal:8200
VAULT_ROLE_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
VAULT_SECRET_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
VAULT_DATABASE_ENABLED=true
VAULT_DATABASE_ROLE=role
VAULT_ENV_PATH=env
VAULT_ENV_OVERRIDE=true
VAULT_TOKEN_CACHE_DRIVER=redis
VAULT_REDIS_CONNECTION=default
VAULT_KV_MOUNT=kv-v2
VAULT_KV_CACHE_ENABLED=true
VAULT_KV_CACHE_TTL=3600
VAULT_AUTO_BOOTSTRAP=true
VAULT_DATABASE_READ_WRITE=true
```

Then use the facade anywhere:

```
use GtelPhp\Vault\Laravel\Facades\Vault;

$secret = Vault::kv()->get('database');

Vault::transit()->encrypt('shipment', $plaintext);
```

Or inject `GtelPhp\Vault\Client` / `GtelPhp\Vault\Manager` via the container as you would with any other service.

Configuration reference
-----------------------

[](#configuration-reference)

`config/vault.php` (see the published file for full inline docs):

KeyDescriptionDefault`default`Name of the default connection`default``connections.{name}.address`Vault/OpenBao base URL`http://127.0.0.1:8200``connections.{name}.role_id` / `secret_id`AppRole credentials—`connections.{name}.kv_mount` / `transit_mount` / `database_mount` / `pki_mount`Secrets engine mount paths`secret`, `transit`, `database`, `pki``connections.{name}.namespace`Vault Enterprise / HCP namespace header`null``connections.{name}.token_cache.driver``memory` or `redis``redis``connections.{name}.token_renew_threshold`Renew once this fraction of the TTL has elapsed`0.7``connections.{name}.kv_cache.enabled` / `.ttl`Read-through cache for `kv()->get()` (Redis)`false` / `300``connections.{name}.database_cache.enabled`Cache for `database()->credentials()`, TTL'd to the lease itself`true``casts.transit_key`Default Transit key used by `JsonEncryptedCast``app``env.path`Default secret path for `Vault::bootstrap()``env``auto.enabled`Auto-load env secrets + DB credentials during boot, no manual `bootstrap/app.php` edits needed`false``auto.env_path`KV path to load when `auto.enabled` is true`env``auto.database`Map of `{laravel_connection: vault_database_role}` to auto-inject`[]`In plain PHP, build a `GtelPhp\Vault\Support\VaultConfig` directly (constructor or `VaultConfig::fromArray()`), no Laravel config required.

Caching (don't skip this in production)
---------------------------------------

[](#caching-dont-skip-this-in-production)

Two things are cached automatically once Redis is available, to avoid hammering Vault and (for database credentials) avoid minting a brand new database user on every single call:

- **`Vault::kv()->get()`** — opt-in via `VAULT_KV_CACHE_ENABLED=true` / `VAULT_KV_CACHE_TTL=300`. Any write (`put`/`patch`/`delete`/`destroy`/`undelete`) immediately invalidates that path's cache entry.
- **`Vault::database()->credentials($role)`** — **enabled by default**. Cached for the lease's own `lease_duration` (minus a small safety margin), never longer, so it's always refreshed before the underlying credentials actually expire. Disable with `VAULT_DATABASE_CACHE_ENABLED=false` if you really want a fresh lease on every call; use `Vault::database()->freshCredentials($role)` to force a refresh on demand instead, or `Vault::database()->forget($role)` to drop the cached entry.

```
VAULT_KV_CACHE_ENABLED=true
VAULT_KV_CACHE_TTL=300
VAULT_DATABASE_CACHE_ENABLED=true
```

In plain PHP, pass a Redis client into `Client::make()`:

```
$vault = Client::make($config, redis: $redisClient);
```

Auto-loading env + database credentials at boot (Laravel)
---------------------------------------------------------

[](#auto-loading-env--database-credentials-at-boot-laravel)

Instead of manually editing `bootstrap/app.php`, you can have `VaultServiceProvider` pull a KV secret into the environment *and* inject dynamic database credentials automatically, controlled entirely by `.env`:

```
VAULT_AUTO_BOOTSTRAP=true
VAULT_ENV_PATH=oms
```

```
// config/vault.php
'auto' => [
    'enabled' => env('VAULT_AUTO_BOOTSTRAP', false),
    'env_path' => env('VAULT_ENV_PATH', 'env'),
    'database' => [
        'pgsql' => 'oms', // Laravel connection 'pgsql' database()->freshCredentials('postgres'); // bypass cache, force a brand new lease
$vault->database()->forget('postgres');            // manually invalidate the cached entry
```

PKI secrets engine
------------------

[](#pki-secrets-engine)

```
$cert = $vault->pki()->issue('web-server', 'app.example.com', ['ttl' => '720h']);
// $cert['certificate'], $cert['private_key'], $cert['serial_number'], ...

$signed = $vault->pki()->sign('web-server', $csrPem);

$vault->pki()->revoke($cert['serial_number']);

$pem = $vault->pki()->readCertificate($cert['serial_number']);
```

Env bootstrap (`Vault::bootstrap()`)
------------------------------------

[](#env-bootstrap-vaultbootstrap)

Load an entire KV v2 secret into the process environment, early enough that your framework's own config files (which usually read `env('SOME_KEY')`) pick the values up:

```
// Plain PHP, very top of your entrypoint:
$vault->bootstrap('env'); // reads connections.default kv_mount + 'env' path
```

In Laravel, call this from `bootstrap/app.php` (or a custom bootstrapper that runs before config is cached) — **not** from a `ServiceProvider::boot()`, which runs too late for config files that call `env()` directly:

```
// bootstrap/app.php
use GtelPhp\Vault\Laravel\Facades\Vault;

Vault::bootstrap(); // uses config('vault.env.path'), defaults to "env"
```

Options:

```
Vault::bootstrap('env', [
    'override' => true,        // overwrite vars that are already set (default: false)
    'cache' => true,           // reuse the result for the lifetime of the process (default: true)
    'prefix' => 'APP_',        // only import keys with this prefix (stripped before applying)
    'mutateKey' => fn ($k) => strtoupper($k),
]);
```

JsonEncryptedCast — selective jsonb encryption
----------------------------------------------

[](#jsonencryptedcast--selective-jsonb-encryption)

Your PostgreSQL columns stay `jsonb`. Only the keys you list are ever encrypted — everything else in the structure (and the column's JSON type) is left untouched.

```
use GtelPhp\Vault\Laravel\Casts\JsonEncryptedCast;

class Shipment extends Model
{
    protected $casts = [
        'sender_info' => JsonEncryptedCast::class . ':name,phone,address.street,address.detail',
        'receiver_info' => JsonEncryptedCast::class . ':name,phone,address.street,address.detail',
    ];
}
```

```
$shipment->sender_info = [
    'name' => 'John',
    'phone' => '0909000000',
    'province_code' => '01',
    'address' => ['street' => '123 Main St', 'detail' => 'Floor 2'],
];
$shipment->save();
```

What actually lands in `jsonb`:

```
{
  "name": "vault:v1:AAAAAQobNX...",
  "phone": "vault:v1:AAAAAQobNY...",
  "province_code": "01",
  "address": {
    "street": "vault:v1:AAAAAQobNZ...",
    "detail": "vault:v1:AAAAAQobNa..."
  }
}
```

Reading the attribute back transparently decrypts only those keys:

```
$shipment->sender_info['name']; // "John"
$shipment->sender_info['province_code']; // "01" - was never touched
```

### Per-cast Transit key

[](#per-cast-transit-key)

By default every cast uses the single Transit key configured globally via `config('vault.casts.transit_key')` / `VAULT_CAST_TRANSIT_KEY` (default `app`). For data with different sensitivity levels or compliance requirements (e.g. payment data vs. general PII), give different casts their own Transit key with a `key=` token — it can appear anywhere in the argument list:

```
class Shipment extends Model
{
    protected $casts = [
        'sender_info'  => JsonEncryptedCast::class . ':name,phone,address.street,key=oms-pii',
        'payment_info' => JsonEncryptedCast::class . ':card_number,cvv,key=oms-payments',
    ];
}
```

Each Transit key must exist in Vault before use:

```
Vault::transit()->createKey('oms-pii');
Vault::transit()->createKey('oms-payments');
```

```
vault write -f transit/keys/oms-pii
vault write -f transit/keys/oms-payments
```

Separate keys let you scope AppRole policies per key (e.g. only the payments service can `encrypt`/`decrypt` with `oms-payments`) and rotate/revoke each one independently of the others.

Notes:

- Dot notation (`address.street`) targets nested keys; everything not listed is left exactly as-is.
- Values are recognised as already-encrypted by their `vault:v` ciphertext prefix, so re-saving a freshly-loaded model never double-encrypts.
- Without a `key=...` token, the cast falls back to `config('vault.casts.transit_key')` (default `app`).
- This cast requires the Laravel container (it resolves `GtelPhp\Vault\Client` via `app()`); it is not usable outside of Laravel.

Multiple connections
--------------------

[](#multiple-connections)

```
use GtelPhp\Vault\Manager;

$manager = new Manager(connections: config('vault.connections'));

$manager->connection('default')->kv()->get('database');
$manager->connection('payments')->kv()->get('stripe');
```

In Laravel, `Vault::connection('payments')->kv()->get(...)` works the same way through the facade.

Token caching (memory vs Redis)
-------------------------------

[](#token-caching-memory-vs-redis)

```
use GtelPhp\Vault\Auth\MemoryTokenCache; // per-process only, fine for CLI/queue workers
use GtelPhp\Vault\Auth\RedisTokenCache;  // shared across web workers, recommended in production

$cache = new RedisTokenCache($redisClient); // accepts ext-redis \Redis or any PSR-16 CacheInterface

$vault = Client::make($config, tokenCache: $cache);
```

In Laravel, set `VAULT_TOKEN_CACHE_DRIVER=redis` and the `VaultServiceProvider` wires up your existing `redis` connection automatically.

Exceptions
----------

[](#exceptions)

Every exception extends `GtelPhp\Vault\Exceptions\VaultException`, so you can always catch that as a fallback:

ExceptionWhen`AuthenticationException`AppRole login fails`TokenExpiredException`Token can't be renewed and needs a fresh login`ConnectionException`Network/transport failure reaching Vault/OpenBao`KvException`KV v2 operation failed`TransitException`Transit operation failed`DatabaseSecretsException`Database secrets engine operation failed`PkiException`PKI operation failed```
use GtelPhp\Vault\Exceptions\VaultException;

try {
    $vault->kv()->get('database');
} catch (VaultException $e) {
    logger()->error($e->getMessage(), $e->context());
}
```

Testing
-------

[](#testing)

```
composer install
composer test    # PHPUnit
composer stan     # PHPStan level 8
composer cs       # PHP_CodeSniffer (PSR-12)
```

The test suite uses a fake `HttpClientInterface` implementation, so it never touches the network or a real Vault/OpenBao server.

Best practices
--------------

[](#best-practices)

- **Always use Redis (or another shared) token cache in production web apps.** With `MemoryTokenCache`, every PHP-FPM worker logs in independently, which is wasteful and can exhaust AppRole secret ID usage limits.
- **Make sure Redis is actually reachable for the database credentials cache.** It's enabled by default, but silently falls back to "no cache" (a fresh lease every call) if Redis isn't bound — verify with `redis-cli KEYS "*gtelphp_vault*"` after a request.
- **Scope AppRole policies tightly.** Give each application only the KV paths / Transit keys / database roles it actually needs.
- **Use separate Transit keys for data with different sensitivity/compliance needs** (see `key=...` in [JsonEncryptedCast](#jsonencryptedcast--selective-jsonb-encryption)), so each can be rotated, revoked, and policy-scoped independently.
- **Call `Vault::bootstrap()` as early as possible** in your request lifecycle, before any config relying on those env vars is read — or use `auto.enabled` to have `VaultServiceProvider` do it for you.
- **Rotate Transit keys periodically** with `rotateKey()` and let `rewrap()` upgrade old ciphertexts lazily on read, rather than re-encrypting everything at once.
- **Prefer short TTLs with auto-renewal** over long-lived tokens — this SDK's `TokenManager` makes that essentially free.

License
-------

[](#license)

MIT

###  Health Score

21

—

LowBetter than 18% of packages

Maintenance65

Regular maintenance activity

Popularity3

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity11

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

### Community

Maintainers

![](https://www.gravatar.com/avatar/3dbf47b879684e141e3b81a02cbed9cebdbfb8b32bdcfbb86ebab83954d71946?d=identicon)[tkien](/maintainers/tkien)

---

Top Contributors

[![tkien](https://avatars.githubusercontent.com/u/2233345?v=4)](https://github.com/tkien "tkien (3 commits)")

### Embed Badge

![Health badge](/badges/gtelphp-vault/health.svg)

```
[![Health](https://phpackages.com/badges/gtelphp-vault/health.svg)](https://phpackages.com/packages/gtelphp-vault)
```

###  Alternatives

[exsyst/swagger

A php library to manipulate Swagger specifications

35816.3M7](/packages/exsyst-swagger)[hubspot/api-client

Hubspot API client

24015.5M18](/packages/hubspot-api-client)[pocketmine/bedrock-protocol

An implementation of the Minecraft: Bedrock Edition protocol in PHP

172437.8k11](/packages/pocketmine-bedrock-protocol)[botman/driver-telegram

Telegram driver for BotMan

94452.6k6](/packages/botman-driver-telegram)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
