Custom secret vault
Custom secret vault
The token vault is bound contract-first, so you can replace the default database-backed implementation without touching callers.
The contract
Cbox\Id\TokenVault\Contracts\SecretVault is the whole seam:
public function store(string $name, string $provider, string $secret, ?string $ownerType = null, ?string $ownerId = null, ?DateTimeInterface $expiresAt = null): VaultSecret;
public function rotate(string $secretId, string $newSecret): VaultSecret;
public function revoke(string $secretId): void;
public function grant(string $secretId, string $clientId, ?int $maxTtlSeconds = null): VaultGrant;
public function revokeGrant(string $secretId, string $clientId): void;
public function lease(string $secretId, string $clientId, string $purpose): SecretLease;
The default DatabaseSecretVault seals values with the Crypto kernel's SecretBox
and stores them in vault_secrets / vault_grants. Swap it when you want the
credential to live somewhere else — an HSM, AWS Secrets Manager, HashiCorp Vault —
while keeping the same deny-by-default grant model and audit contract.
Bind your implementation
use Cbox\Id\TokenVault\Contracts\SecretVault;
$this->app->singleton(SecretVault::class, MyHsmBackedVault::class);
What your implementation MUST preserve
These are the guarantees callers rely on — not optional:
- Deny-by-default.
lease()returns a value only when a live grant exists for the exact(secret, client)pair and the secret is neither revoked nor expired. - Uniform denial. Every lease failure raises
LeaseDenied(not a distinguishing error), so the vault is never an enumeration oracle. Management ops may throwSecretNotFound. - Audited, value-free. Record store / rotate / revoke / grant / lease on the audit trail with the actor and purpose — and never the secret value.
- Never persist the plaintext recoverably outside your sealed store. If you keep it server-side, seal it (recoverable), never a plain hash — the vault must replay it.
Test against the contract
Dogfood Cbox\Id\TokenVault\Testing\InteractsWithTokenVault and the shipped
FakeTokenVault mirror the deny-by-default semantics — write your implementation's
tests against the same behaviours (grant required, revoked/expired refused, uniform
LeaseDenied).
See Security: token vault for the full threat model.