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:

"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.