NAME
WWW::OpenBao - HTTP client for OpenBao / HashiCorp Vault API
VERSION
version 0.003
SYNOPSIS
use WWW::OpenBao;
my $bao = WWW::OpenBao->new(
endpoint => $ENV{OPENBAO_ADDR} // 'http://127.0.0.1:8200',
token => $ENV{OPENBAO_TOKEN} // '',
kv_mount => 'secret',
);
$bao->write_secret('app/db', { user => 'app', pass => 'hunter2' });
my $creds = $bao->read_secret('app/db');
my $meta = $bao->read_secret_metadata('app/db'); # version, created_time, ...
my $keys = $bao->list_secrets('app/');
$bao->delete_secret('app/db');
$bao->login_k8s( role => 'my-app' ); # sets $bao->token
DESCRIPTION
WWW::OpenBao is a minimal HTTP client for OpenBao and HashiCorp Vault. It covers the day-to-day surface used by application code: KV v2 secret read/write/list/delete, Kubernetes ServiceAccount login, and a handful of sys/* bootstrap helpers (health, init, unseal, enable_engine).
It is intentionally small — no caching, no lease renewal, no policy management. If you need those, reach for a heavier client; if you just want to talk to Vault/OpenBao from Perl, this is enough.
Every request goes through one shared seam, and its error contract applies to every method: a non-2xx response croaks with the status and response body, except 404, which is never a croak. A 404 comes back as a soft miss:
"read_secret" and "read_secret_metadata" return
undef. KV v2 answers404both for an absent path and for a soft-deleted version, soundefmeans "no readable value", not "never existed"."list_secrets" returns an empty arrayref.
"secret_exists" returns false. Only a
404does that; a403(permission denied) croaks like any other non-2xx."write_secret", "delete_secret", "soft_delete_secret", "undelete_secret", "destroy_secret", "init", "unseal" and "enable_engine" return
undef."login_k8s" does not croak either: it returns an empty hashref and leaves "token" undefined, since no
client_tokencame back.
"health" is the one method that never croaks: it returns undef whenever the server gives no usable answer, see there.
endpoint
Required. Base URL of the Vault/OpenBao server, e.g. http://127.0.0.1:8200. No trailing slash.
token
Vault token used for the X-Vault-Token header. Writable — "login_k8s" overwrites it on success.
kv_mount
Mount path of the KV v2 engine. Defaults to secret.
k8s_auth_mount
Mount path of the Kubernetes auth method. Defaults to kubernetes, the OpenBao/Vault default. Set it when the method is mounted elsewhere — e.g. k8s_auth_mount => 'kubernetes-prod' makes "login_k8s" post to v1/auth/kubernetes-prod/login.
read_secret($path, version => $n)
Returns the data.data hashref for a KV v2 secret, or undef if the path does not exist. By default it reads the latest version; pass version => $n to read a specific earlier version instead, which appends ?version=N to the GET .../data/... request. undef (a soft-deleted or absent version both answer 404) still means "no readable value".
read_secret_metadata($path, version => $n)
Returns the data.metadata hashref that KV v2 returns alongside the value on the same GET .../data/... read — the version number, created_time, destroyed flag and custom_metadata — or undef on a 404. Like "read_secret" it reads the latest version by default and takes an optional version => $n to read a specific version's metadata via ?version=N.
This is a separate entry point on purpose: "read_secret" keeps returning the bare data.data hashref, so callers that only want the values are unaffected. Note it reads the data/ endpoint (the metadata that accompanies a value read), not the metadata/ version-history endpoint.
write_secret($path, \%data)
Writes (creates a new version of) a KV v2 secret. Returns the decoded response.
delete_secret($path)
Delete ladder level 3 — irreversible, destroys everything. Removes the key together with all of its versions and history via DELETE /<mount>/metadata/.... This is the most destructive of the KV v2 delete operations and there is no undo: despite the plain name it is not the soft delete a caller might expect. For the reversible level-1 soft delete of the latest (or of named) versions use "soft_delete_secret", reversed by "undelete_secret"; to permanently destroy specific versions while keeping the key and its metadata use "destroy_secret" (level 2).
soft_delete_secret($path, @versions)
Delete ladder level 1 — reversible. Soft-deletes KV v2 versions: they then read back as 404, but the data is retained and can be restored with "undelete_secret". Called with no @versions it soft-deletes the latest version via DELETE /<mount>/data/...; called with one or more version numbers it soft-deletes exactly those via POST /<mount>/delete/....
undelete_secret($path, @versions)
Restores versions previously soft-deleted by "soft_delete_secret", via POST /<mount>/undelete/.... At least one version number is required (it croaks otherwise). This reverses a level-1 soft delete only — it cannot bring back versions removed by "destroy_secret" or "delete_secret".
destroy_secret($path, @versions)
Delete ladder level 2 — irreversible. Permanently destroys the named versions via PUT /<mount>/destroy/...: their contents are gone for good and cannot be undeleted. The key itself and its metadata survive, so this is narrower than "delete_secret" (level 3) but just as final for the versions it names. At least one version number is required (it croaks otherwise).
list_secrets($path)
Returns an arrayref of keys at the given KV v2 metadata path. Empty arrayref if the path is missing.
secret_exists($path)
True if the given path exists, false if it answers 404. Does not fetch the secret data. A 403 (permission denied) or any other non-2xx croaks, so a path the token may not see is not reported as absent.
login_k8s(role => $role, jwt => $jwt)
Performs a Kubernetes ServiceAccount login. Posts to v1/auth/<k8s_auth_mount>/login, i.e. v1/auth/kubernetes/login by default; see "k8s_auth_mount" to reach a method mounted elsewhere. role is required. jwt defaults to the in-pod ServiceAccount token at /var/run/secrets/kubernetes.io/serviceaccount/token. On success the returned client_token is stored in "token" and the full auth hashref is returned.
health
Returns the decoded /v1/sys/health response as a hashref for every reachable server, whatever its seal, standby or init state. The request is made with standbyok=true&perfstandbyok=true&sealedcode=200&uninitcode=200, so OpenBao/Vault answers 200 — and therefore a body — for the states that otherwise carry their answer only in a non-2xx status code: standby (429), performance standby, sealed (503) and uninitialised (501). Read the state out of the returned fields — initialized, sealed, standby, performance_standby, version and the rest of the health payload.
Because of this, a sealed, uninitialised or standby server yields an inspectable hashref rather than undef. undef means only that the server gave no usable answer — a network-level failure, a 404, or another non-2xx status returned in spite of the flattening parameters. health never croaks.
init(secret_shares => $n, secret_threshold => $n)
Initialises an uninitialised server. Both arguments default to 1. Use this for dev/test only.
unseal($key)
Submits a single unseal key share.
enable_engine($path, $type)
Mounts a secrets engine at $path with the given $type (e.g. kv-v2).
SUPPORT
Issues
Please report bugs and feature requests on GitHub at https://github.com/Getty/p5-www-openbao/issues.
CONTRIBUTING
Contributions are welcome! Please fork the repository and submit a pull request.
AUTHOR
Torsten Raudssus <getty@cpan.org>
COPYRIGHT AND LICENSE
This software is copyright (c) 2026 by Torsten Raudssus <torsten@raudssus.de> https://raudssus.de/.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.